为JavaScript里面的概念,温故而知新。
概念:Hositing是什么
在JavaScript中,如果试图使用尚未声明的变量,会出现ReferenceError
错误。毕竟变量都没有声明,JavaScript也就找不到这变量。加上变量的声明,可正常运行:
console.log(a);
// Uncaught ReferenceError: a is not defined
var a;
console.log(a); // undefined
考虑下如果是这样书写:
console.log(a); // undefined
var a;
直觉上,程序是自上向下逐行执行的。使用尚未声明的变量a,按理应该出现ReferenceError
错误,而实际上却输出了undefined
。这种现象,就是Hoisting。var a
由于某种原因被"移动"到最上面了。可以理解为如下形式:
var a;
console.log(a); // undefined
需要注意:
- 实际上声明在代码里的位置是不会变的。
hoisting只是针对声明,赋值并不会。
console.log(a); // undefined var a = 2015; // 理解为如下形式 var a; console.log(a); // undefined a = 2015;
这里
var a = 2015
理解上可分成两个步骤:var a
和a = 2015
。函数表达式不会
hoisting
。fn(); // TypeError: fn is not a function var fn = function () {} // 理解为如下形式 var fn; fn(); fn = function () {};
这里
fn()
对undefined
值进行函数调用导致非法操作,因此抛出TypeError
错误。
函数声明和变量声明,都会hoisting
,需要注意的是,函数会优先hoisting
:
console.log(fn);
var fn;
function fn() {}
// 理解为如下形式
function fn() {}
var fn; // 重复声明,会被忽略
console.log(fn);
对于有参数的函数:
fn(2016);
function fn(a) {
console.log(a); // 2016
var a = 2015;
}
// 理解为如下形式
function fn(a) {
var a = 2016; // 这里对应传参,值为函数调用时候传进来的值
var a; // 重复声明,会被忽略
console.log(a);
a = 2015;
}
fn(2016);
总结一下,可以理解Hoisting
是处理所有声明的过程。需要注意赋值及函数表达式不会hoisting。
意义:为什么需要Hoisting
可以处理函数互相调用的场景:
function fn1(n) {
if (n > 0) fn2(n);
}
function fn2(n) {
console.log(n);
fn1(n - 1);
}
fn1(6);
按逐行执行的观念来看,必然存在先后顺序,像fn1
与fn2
之间的相互调用,如果没有hoisting
的话,是无法正常运行的。
规范:Hoisting的运行规则
具体可以参考规范ECMAScript 2019 Language Specification。与Hoisting相关的,是在8.3 Execution Contexts
。
一篇很不错的文章参考Understanding Execution Context and Execution Stack in Javascript
参考里面的例子:
var a = 20;
var b = 40;
let c = 60;
function foo(d, e) {
var f = 80;
return d + e + f;
}
c = foo(a, b);
创建的Execution Context
像这样:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: < uninitialized >,
foo: < func >
}
outer: ,
ThisBinding:
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: undefined,
b: undefined,
}
outer: ,
ThisBinding:
}
}
在运行阶段,变量赋值已经完成。因此GlobalExectionContext
在执行阶段看起来就像是这样的:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: 60,
foo: < func >,
}
outer: ,
ThisBinding:
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 40,
}
outer: ,
ThisBinding:
}
当遇到函数foo(a, b)
的调用时,新的FunctionExectionContext
被创建并执行函数中的代码。在创建阶段像这样:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 40, length: 2},
},
outer: ,
ThisBinding: ,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
f: undefined
},
outer: ,
ThisBinding: ,
}
}
执行完后,看起来像这样:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 40, length: 2},
},
outer: ,
ThisBinding: ,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
f: 80
},
outer: ,
ThisBinding: ,
}
}
在函数执行完成以后,返回值会被存储在c
里。因此GlobalExectionContext
更新。在这之后,代码执行完成,程序运行终止。
细节:var、let、const在hoisting上的差异
回顾规范:Hoisting的运行规则
,可以注意到在创建阶段,不管是用let
、const
或var
,都会进行hoisting
。而差别在于:使用let
和const
进行声明的时候,设置为uninitialized
(未初始化状态),而var
会设置为undefined
。所以在let
或const
声明的变量之前访问时,会抛出ReferenceError: Cannot access 'c' before initialization
错误。对应的名词为Temporal Dead Zone
(暂时性死区)。
function demo1() {
console.log(c); // c 的 TDZ 开始
let c = 10; // c 的 TDZ 结束
}
demo1();
function demo2() {
console.log('begin'); // c 的 TDZ 开始
let c; // c 的 TDZ 结束
console.log(c);
c = 10;
console.log(c);
}
demo2();
总结
果真是温故而知新,发现自己懂得其实好少。鞭策自己,后续对this
、prototype
、closures
及scope
等,进行温故。