为JavaScript外面的概念,温故而知新。
概念:Hositing是什么
在JavaScript中,如果试图应用尚未申明的变量,会呈现ReferenceError
谬误。毕竟变量都没有申明,JavaScript也就找不到这变量。加上变量的申明,可失常运行:
console.log(a);// Uncaught ReferenceError: a is not defined
var a;console.log(a); // undefined
思考下如果是这样书写:
console.log(a); // undefinedvar a;
直觉上,程序是自上向下逐行执行的。应用尚未申明的变量a,按理应该呈现ReferenceError
谬误,而实际上却输入了undefined
。这种景象,就是Hoisting。var a
因为某种原因被"挪动"到最下面了。能够了解为如下模式:
var a;console.log(a); // undefined
须要留神:
- 实际上申明在代码里的地位是不会变的。
hoisting只是针对申明,赋值并不会。
console.log(a); // undefinedvar a = 2015;// 了解为如下模式var a;console.log(a); // undefineda = 2015;
这里
var a = 2015
了解上可分成两个步骤:var a
和a = 2015
。函数表达式不会
hoisting
。fn(); // TypeError: fn is not a functionvar 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: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here a: undefined, b: undefined, } outer: <null>, ThisBinding: <Global Object> }}
在运行阶段,变量赋值曾经实现。因而GlobalExectionContext
在执行阶段看起来就像是这样的:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", c: 60, foo: < func >, } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here a: 20, b: 40, } outer: <null>, ThisBinding: <Global Object> }
当遇到函数foo(a, b)
的调用时,新的FunctionExectionContext
被创立并执行函数中的代码。在创立阶段像这样:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", Arguments: {0: 20, 1: 40, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", f: undefined }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }}
执行完后,看起来像这样:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", Arguments: {0: 20, 1: 40, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", f: 80 }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }}
在函数执行实现当前,返回值会被存储在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
等,进行温故。
参考资料
- 我晓得你懂 hoisting,可是你理解到多深?
- MDN: Hoisting
- You-Dont-Know-JS 2nd-ed
- ECMAScript 2019 Language Specification
- Understanding Execution Context and Execution Stack in Javascript
- JavaScript高级程序设计