JavaScript 基础知识
IIFE(立即调用函数表达式)
IIFE(Immediately Invoked Function Expression)是定义完立刻执行的函数表达式。
它不是一种新语法,而是「写成表达式的函数 + 后面紧跟调用括号」。
(function () {
const name = "Lizhao";
return name;
})();
func; // "Lizhao"
赋给变量的不是函数本身,而是这次调用的返回值。上面的 func 是字符串 "Lizhao",不能再当函数调用。
执行时会新建一个函数作用域。里面的 var、函数声明出不去,从而避免污染外层(在 let、const 和 ES 模块普及以前,这是隔离作用域的主要手段)。
特性:
- 内部用
var声明的变量,外面访问不到。 - 赋给变量时,存的是返回值,不是函数。
- 每次写出并执行一次 IIFE,就是一次独立的作用域;不会自动「再执行一遍」,除非代码再次跑到这句。
closure(闭包)
闭包(closure)是一个函数里面嵌套另外一个函数,被嵌套(内部)的函数自身形成了一个闭包,这个闭包可以自己拥有独立的词法环境,也就意味着,被嵌套函数的作用域除了自身的作用域,还维持了其容器函数的作用域。
一句话概括: 闭包就是一个拥有自身作用域和父级作用域(由外层函数提供,即便外层函数销毁依旧可以访问)的特殊函数。
注意: 闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。
闭包有以下特性:
- 闭包是一个函数,它是另一个函数(称容器函数)的执行结果。
- 闭包可以拥有其容器函数的作用域。
const func = function () {
const name = "Lizhao";
return function () {
return name;
};
};
const closureFunc = func();
closureFunc(); // "Lizhao"
闭包不是「外层去读内层变量」,而内层函数被暴露到外面之后,外面通过调用这个函数,间接用到内层还记着的外层变量。
闭包会延长变量寿命。内部函数一直活着,它引用的外层变量就不会回收;大对象、DOM 节点被闭包抓住,就是常见泄漏路径。
Object.keys 返回的属性顺序
现行引擎里,普通对象的 Object.keys 顺序是确定的,可以按规范来预期。 它只返回对象自身的、可枚举的字符串键,不含 Symbol,也不含原型链上的键。
顺序与 [[OwnPropertyKeys]] 里字符串那两段一致:
- 数组索引键:值为规范说的 array index(
0到2^32 - 2的整数,且键名是该整数的规范字符串形式),按数值从小到大。 - 其余字符串键(含
'3.1'、'-1'、'01'、过长数字串等):按属性创建先后。
Object.keys 不会列出 Symbol 键。要连 Symbol 一起看,用 Reflect.ownKeys(字符串规则同上,最后再按创建顺序追加 Symbol)或 Object.getOwnPropertySymbols。
const symbol1 = Symbol("symbol1");
const symbol2 = Symbol("symbol2");
const obj = {
d: "d",
[symbol1]: "symbol1",
a: "a",
c: "c",
b: "b",
1: 1,
"3.1": 3.1,
2: 2,
[symbol2]: "symbol2",
};
Object.keys(obj);
// ["1", "2", "d", "a", "c", "b", "3.1"]
// 整数索引 1、2 在前;其余字符串按写入顺序;两个 Symbol 不出现
Reflect.ownKeys(obj);
// ["1", "2", "d", "a", "c", "b", "3.1", symbol1, symbol2]
对象字面量里写成 symbol1: "symbol1"(没有方括号)时,键是字符串 "symbol1",不是上面的 Symbol。旧示例把这两种写法混在一起,注释里的顺序不能当作 Symbol 键的行为。
for...in 的字符串键顺序与 Object.keys 相同,但 for...in 还会继续走原型链上可枚举的字符串键。两者都不是「随便排」,也不是「完全一样的一份列表」。
变量提升
变量提升(hoisting)指:进入一个作用域时,引擎先处理该作用域里的声明,再从上到下执行语句。看起来像声明被「挪到了顶部」,赋值和函数体里的语句仍留在原处。
var 只提升声明,初始化相当于 undefined。赋值留在原来那一行:
console.log(a); // undefined
var a = 1;
console.log(a); // 1
等价于:
var a;
console.log(a);
a = 1;
console.log(a);
函数声明:具名 function foo() {} 会把整个函数提升到当前作用域顶部(函数体一起上去)。函数表达式、箭头函数只提升左边的绑定(若是 var),右边的函数对象要等执行到那一行才创建:
foo(); // 能调用,函数声明已提升
function foo() {
return 1;
}
bar(); // TypeError: bar is not a function
var bar = function () {
return 2;
};
同一作用域里同名时
函数声明先就位;随后的 var 同名不会把函数清掉,只是重复声明被忽略。赋值仍按源码顺序执行,所以后面的 var c = 1 会把函数盖掉:
var c = 1;
function c(c) {
console.log(c);
var c = 3;
}
c(2);
// TypeError: c is not a function
预解析后相当于:
function c(c) {
console.log(c);
var c = 3;
}
var c; // 与上面的函数同名,声明被忽略
c = 1; // 把函数换成数字 1
c(2); // 对 1 做调用 → TypeError
let、const、class也会在进入作用域时被创建,但在初始化之前处于暂时性死区(TDZ):提前访问会 ReferenceError,不是 undefined。不要把这理解成「和 var 一样提升后就能用」。
console.log(x); // ReferenceError
let x = 1;