Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解JavaScript执行上下文 #44

Open
guapi233 opened this issue Dec 18, 2020 · 0 comments
Open

理解JavaScript执行上下文 #44

guapi233 opened this issue Dec 18, 2020 · 0 comments
Labels

Comments

@guapi233
Copy link
Owner

一直对JavaScript执行的环境不太理解,只知道一些什么变量提升呀,闭包呀表面概念,一问为什么脑子当场就断开连接,所以在参考了大大的文章后,整理一份自己关于执行上下文的理解。

什么是执行上下文

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。个人理解的是执行上下文就是一个隐形的对象,这个对象上面记录了程序当前执行所依赖的环境因素。

有些地方,比起上下文,我觉得环境这个词更容易让人理解,所以我在下文也会在合适的地方使用环境这个词语来代替上下文,因为任何一句代码执行,都需要一个执行时的环境,这个环境提供了一些代码执行所需的条件。就好像分析历史事件时,我们必不可少得要分析它所处的时代环境一样,对于代码也是如此。

执行上下文的分类

  • 全局执行上下文:是程序最外层的执行环境,当程序开始执行之前,这个执行环境就会被创建,直到程序执行结束才会被释放,且一个程序至多有一个全局执行环境

  • 函数执行上下文:每当遇到一个函数调用语句时,就会创建一个对应于该函数的执行环境,并且在该函数执行完毕后,该函数的执行环境就会被销毁,一个程序可以同时存在多个函数执行环境

  • eval执行上下文eval函数能将一段字符串文本当做程序语句执行,其内部有独特的执行环境(非严格模式下会直接作用于外部作用域,严格模式下则会创建一个内部作用域),这里不展开讨论

    "use strict"; // 如果关闭严格模式会打印2
    
    var a = "aa";
    eval("var a = 2");
    console.log(a); // "aa"
  • 模块执行上下文:存在与nodejs中,我们使用的module,exports就存在于此,这里不做讨论

执行上下文中的成员

  • 变量对象VO(variable object):

    VO用于存储当前执行环境中所拥有的变量以及函数,这些变量函数可以被在当前环境中执行的代码所调用

  • 作用域链 (scope chain):

    作用域链用于规定当前环境下的变量以什么样的方式及顺序进行查找

  • this

    this用于记录调用方法的对象

执行上下文栈

在了解执行执行上下文栈之前,先来看一道题目,请说出下面两段代码的执行结果,以及它们有何不同:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

你可能很轻易得看出它们其实最后返回的都是local scope,那么它们到底有什么不同呢(老师,我知道,它们的写法不同,老师:爬),我们先来看一下执行栈这个玩意到底是啥。

我们知道万物运动发展总有个顺序,浏览器分析和执行代码也一样,总要按着个顺序来,最容易让人想到的是浏览器是按照行一行一行的分析代码并执行它们的,不过仔细一想这个结论就错了,因为语言中是存在作用域的:

var a = 12;

function b () {
    var a = 13;
    console.log(a); // 浏览器咋知道这个a是函数b内的
}
console.log(a); // 浏览器咋知道这个a是全局的

如果想当然的按照一行一行的去分析和执行代码,那么代码中作用域早就乱套了,其实小伙伴们可能已经知道了,浏览器一行一行代码执行不假,但是在那之前浏览器还会按照以为单位去分析代码,也就是在代码执行之前,浏览器已经将代码分为了一段一段的代码片段,然后有序的组织这些代码片段来完成程序运作。

那么这一个一个的代码块是啥呢,没错,其实就是我们上面提到的不同类型的执行上下文:全局执行上下文就是一个最大的代码片段,这个代码片段包括了程序中所有的代码,并且它其中还包括了以函数执行上下文分割出的一个一个的小的代码片段。这些代码片段身上都有一个执行上下文,而执行栈就是存放执行上下文的地方,浏览器按照执行顺序依次将代码片段对应的执行上下文放入执行栈中,并在一个代码片段执行完毕后将其弹出。

知道了执行顺序的原理,下面按照这样的原理分析一段js代码:

function fun3() {
    return "没了,哈哈";
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

按照全局执行上下文和函数执行上下文的定义,我们发现上面的代码中有3处函数调用,那么就表明这段代码一共产生了3 + 1(全局)个执行上下文,根据上面的执行顺序原理,来看看浏览器如何处理这些执行上下文:

// 伪代码,由于我们还不知道执行上下文的内容是啥,所以用文字表示

// 创建执行栈(execution stack => ECStack)
const ECStack = [];

// 先将全局执行上下文放入执行栈,然后开始执行代码
ECStack.push(全局执行上下文)

// 遇到fun1(),创建fun1的上下文放入执行栈,然后开始执行fun1()
ECStack.push(fun1的执行上下文)

// 执行fun1时发现里面有fun2(),创建fun2的上下文放入执行栈,然后开始执行fun2()
ECStack.push(fun2的执行上下文)

// 执行fun2时发现里面有fun3(),创建fun3的上下文放入执行栈,然后开始执行fun3()
ECStack.push(fun3的执行上下文)

// 执行fun3时没有发现里面有函数调用,遇到return语句,fun3函数执行完毕,弹出执行栈
ECStack.pop()

// 因为fun3()执行完了,fun2中也没有其它代码,fun2函数执行完毕,弹出执行栈
ECStack.pop()

// 因为fun2()执行完了,fun1中也没有其它代码,fun1函数执行完毕,弹出执行栈
ECStack.pop()

// 因为fun1()执行完了,全局中耶没有其它代码,程序执行完毕,弹出执行栈
ECStack.pop()

那么最后我们再回到本小节开始提到的问题,以执行栈变化的角度来看一下两段代码的执行过程

第一段代码:

// 执行checkscope函数
ECStack.push(checkscope的上下文)

// 发现checkscope函数最后执行了f函数
ECStack.push(f的上下文)

// f函数执行完毕
ECStack.pop()

// 因为f函数执行完毕,return语句成功返回,checkscope执行完毕
ECStack.pop()
// 执行checkscope函数
ECStack.push(checkscope的上下文)

// return语句成功返回f函数,checkscope函数执行完毕
ECStack.pop()

// checkscope()执行完毕,后面的"()"开始执行checkscope函数返回的f函数
ECStack.push(f的上下文)

// f函数执行完毕
ECStack.pop()

是不是有些不一样呢?

理解了执行上下文栈其实就是理解了程序的运行流程,上面我们简单得介绍了执行上下文中的“成员”,下面我们就具体得介绍它们是什么,起到什么作用,如何运作。

VO(variable object)

上面我们提到:

VO用于存储当前执行环境中所拥有的变量以及函数,这些变量函数可以被在当前环境中执行的代码所调用

在正式介绍VO之前,我们先了解一个老朋友,全局对象,以下内容引自W3school

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

“通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性”,例如:

console === window.console; // true
var a = 12;
function b() { console.log(a) };
console.log(this.a, window.a); // 12 12
this.b(); // 12
this === window; // true

我们可以通过window访问定义在全局的变量以及函数,这不正是VO所具有的特性吗,没错,全局对象其实就是全局执行上下文的VO(变量对象)

函数上下文中的变量对象(AO)

函数上下文中的变量对象有点特殊,不是称为variable object,而是activation object,简称AO,其实二者没有本质区别,作用也相同,叫法不同将二者区别开来的原因是因为AO是一种特殊的VO,通过AO,不仅能访问函数中定义的变量和函数,还可以访问传进来的参数和特殊对象arguments,但是它们并不是一开始就能访问的,而是需要等到该函数开始执行时,函数上下文中的VO才会activation化为AO,简单得说:

AO = VO + arguments + params,AO就是函数上下文的变量对象,VO就是全局上下文的变量对象。

VO的初始化流程

我们仍然将程序分为分析执行两个阶段,VO的初始化是在代码的分析阶段:

  1. 如果当前上下文是函数上下文(再次提醒遇到函数执行语句时才会创建上下文),分析函数的所有形参:
    • 将形参名称与对应的值绑定到AO身上,并将值挂到对应位置的arguments上
    • 对于没对应实参的形参,值设为undefined
  2. 函数声明
    • 如果遇到函数声明语句,将函数的名称与该函数的引用挂到当前上下文的VO身上
    • 如果VO身上已经存在与该函数名称相同的标识符,覆盖
  3. 变量声明
    • 如果遇到var变量声明,将变量名与undefined挂到当前上下文的VO身上
    • 如果VO身上已经存在与该变量名称相同的标识符,则忽略该var声明

举个例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

在执行完分析阶段后,函数foo执行上下文身上的AO为:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c() {},
    d: undefined
}

foo函数开始执行时,上面的对象就是其AO的初始状态,然后根据代码的执行,发生状态变化,比如当foo函数执行完毕后,AO对象最终的结果为:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c() {},
    d: reference to FunctionExpression "d"
}

这就是上下文中变量对象(VO/AO)的真面目了,正是因为它们的存在,我们才可以通过它们来访问我们定义的变量,同时也解释了为什么会有变量提升,以及为什么函数声明可以先调用后赋值的原因,最后总结本小节的内容,概括如下:

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

##作用域链

在开始正式解释作用域链之前,还是要提一些前置知识,我们知道函数在声明赋值的那一刻其作用域就已经决定了,而不是根据调用语句动态变化的,根据这一特性也拓展出了闭包这一概念,举个例子:

var inner;

function wrapA() {
  var item = "我在wrapA中";

  inner = function () {
    console.log(item);
  };
}

function wrapB() {
  var item = "我在wrapB中";

  inner();
}

wrapA();
wrapB(); // 我在wrapA中

很显然,上面的例子就是一个典型的闭包代码,inner函数没有去拿warpB中的item就说明inner函数的父亲一开始就已经决定好了,完全不受inner()调用位置的影响。

综上所述,JavaScript中的函数是典型的静态作用域,或者说叫做词法作用域

作用域链的构建过程

了解词法作用域对了解作用域链创建的过程有重大帮助,我们现在知道了函数的作用域受函数静态声明赋值的地方影响,那么js是如何创建出每一个函数中的作用域链,使得这些函数可以按着这条链条向上查询变量的呢?

托词法作用域的福,其实每个函数在声明赋值时身上都有一个[[Scopes]]隐藏属性,你可以见过这个属性,因为使用console.dir打印函数时可以看到这个属性,这个属性其实就记录了所有父变量的引用层级链,但是注意:[[Scopes]]并不代表完整的作用域链,具体的原因等我们待会再说。

下面我们举个例子来展示下[[Scopes]]大概的样子:

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[Scopes]]为:

// 伪代码

foo.[[Scopes]] = [
    globalContext.VO
];

bar.[[Scopes]] = [
    fooContext.AO,
    globalContext.VO
];

上面我们提到了,[[Scopes]]并不是完整的作用域链,那么什么时候完整的作用域链会产生呢?其实不难想到,当我们在一个函数中使用一个变量时,会如何查找该变量的值呢?肯定是先去自身AO上找,找不到在沿着作用域链向上找,那么要构建完整的作用域链,就必须等到自身AO创建完毕后,下面我们就一起来看一下一条完整的作用域链是如何构建的:

// 示例函数

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. 浏览器遇到function checkscope() {}声明语句,保存当前的作用域链到其内部属性[[Scopes]]

    checkscope.[[Scopes]] = [
        globalContext.VO
    ];
  2. 浏览器遇到checkscope()执行语句,创建该函数的执行上下文,激活AO,并将上下文压入执行栈

    ECStack = [
        checkscopeContext,
        globalContext
    ];
  3. 开始进行分析AO对象内容前的准备工作,使用[[Scopes]]属性创建上下文中的作用域链

    checkscopeContext = {
        Scope: checkscope.[[Scopes]],
    }
  4. 开始创建AO对象中的内容

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: undefined
        }
        Scope: checkscope.[[Scopes]],
    }
  5. 将AO压入该函数的作用域链顶端

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: undefined
        },
        Scope: [AO, [[Scopes]]]
    }
  6. 分析工作结束,开始执行函数,并根据函数内部代码,修改对应的AO内容

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: 'local scope'
        },
        Scope: [AO, [[Scopes]]]
    }
  7. 根据Scope找到scope2的值,并将其返回,函数上下文从执行栈中弹出

    ECStack = [
        globalContext
    ];

这样一条作用域链从构建到销毁的全过程就结束了,上面的过程虽然有好多细节上的不同,但是足够我们了解作用域链的大概了,比如为什么外面的代码没法访问函数中的变量,其实就是外面的执行上下文中的作用域链没有该函数的AO引用,理所当然也就拿不到AO中的变量引用了。

V8优化细节:V8为了优化闭包,不会在[[Scopes]]存储一条完整的作用域链,而是只会将在内部函数中使用到,也就是产生了闭包的变量引用存下来,其他的变量都会虽然外部函数的结束而销毁。换句话说,如果你没有利用闭包引用外部函数中的变量,那么无论你这个函数嵌套了多少层,它的作用域链总是只有自身的AO和全局对象。

this

this内容略,这里推荐一篇文章,作者冴羽 ,大大通过ECMA规范实现的角度,讲解了一些例如Reference、MemberExpression等规范内属性,ECMA正是对MemberExpression进行求值,判断结果是否为Reference

类型,进行对应的this绑定。

大大这种写法的动机是这样的一句函数调用(false || foo.bar)(),我一开始认为最终调用中的thisfoo,但其实它内部是window,其实(false || foo.bar)部分就是MemberExpression,对其求值后结果不为Reference类型,所以this隐式处理(严格为undefined,非严格为window)。

个人理解是||foo.bar进行求值操作了,直接拿到了bar()的引用,包括,已经=这两个运算符,都对右侧操作对象进行了求值,而(foo.bar)()中的小括号运算符没有对其中内容运算的必要,所以foo.bar这条引用路径仍被保留,不过这些都是个人通过表象推测的,真正原理还是要通过规范来获取。

捋一下

还是下面这个例子,我们从头捋一下它们的执行上下文的构建过程(注意,下面的流程只是大概的过程,例如作用域链方面由于浏览器优化可能会与实际流程不同):

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

第一个

  1. 首先创建全局执行上下文,将全局对象绑定到VO,并压入执行栈

    ECStack = [
        globalContext
    ];
  2. 遇到function checkscope(){}声明语句,记录该函数存在的词法作用域链

    checkscope.[[Scopes]] = [
        globalContext.VO
    ]
  3. 遇到checkscope()执行语句,压入执行栈并开始构建该函数的AO对象:

    ECStack = [
        checkscopeContext,
        globalContext
    ]
    
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){},
        }
    }
  4. 构建AO的途中发现function f(){}声明语句,记录该函数的词法作用域链:

    f.[[Scopes]] = [
        checkscopeContext.AO,
        globalContext.VO
    ]
  5. checkscope函数的AO构建完毕,开始通过[[Scopes]]连接作用域链:

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){},
        },
        scope: [AO, [[Scopes]]] // [[Scopes]] = globalContext.VO
    }
  6. 开始执行checkscope()this设置为window(非严格),并根据其中的代码更新AO对象:

    AO.scope: undefined => "local scope";
  7. 执行到return部分发现f()执行语句,压入执行栈并开始构建该函数的AO对象:

    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ]
    
    fContext = {
        AO: {
            arguments: {
                length: 0
            },
        }
    }
  8. f函数的AO构建完毕,开始通过[[Scopes]]连接作用域:

    fContext = {
        AO: {
            arguments: {
                length: 0
            },
        },
        scope: [AO, [[Scopes]]] // [[Scopes]] = checkscopeContext.AO, globalContext.VO
    }
  9. f函数开始执行,this设置为window,执行f时发现自身的AO中没有scope变量,开始向上去checkscopeContext.AO中寻找,发现值为local scope,返回并结束函数执行,从执行栈中弹出

    ECStack = [
        checkscopeContext,
        globalContext
    ]
  10. f()执行完毕,checkscope顺利返回结果,执行完毕并弹出执行栈:

    ECStack = [
        globalContext
    ]

第二个

第二段代码的执行过程和第一段代码的前6步相同,在第7步开始发生变化:

  1. 执行到返回语句,顺利将函数f返回,结束执行并弹出执行栈:

    ECStack = [
        globalContext
    ]
  2. 返回的f函数继续通过后面的()开始调用,压入执行栈并开始构建该函数的AO对象:

    ECStack = [
        fContext,
        globalContext
    ]
    
    fContext = {
        AO: {
            arguments: {
                length: 0
            },
        }
    }
  3. f函数的AO构建完毕,开始通过[[Scopes]]连接作用域:

    fContext = {
        AO: {
            arguments: {
                length: 0
            },
        },
        scope: [AO, [[Scopes]]] // [[Scopes]] = checkscopeContext.AO, globalContext.VO
        // 这里的checkscopeContext.AO通过闭包的形式被引用着(其实只有scope被引用)
    }
  4. f函数开始执行,this设置为window,执行f时发现自身的AO中没有scope变量,开始向上去checkscopeContext.AO中寻找,发现值为local scope,返回并结束函数执行,从执行栈中弹出:

    ECStack = [
        globalContext
    ]

ES6之后的执行上下文

在ES6,执行上下文内部结构发生了很大的变化,因为出现了有较强颠覆性的特性内容:let/const/class以及block/caseBlock作用域,let/const不能出现提升现象,并且还需要具备块级作用域的特性,这和上面执行上下文支持的var所具有的行为完全不同,这可怎么办呢?于是ECMA规范干脆变更了执行上下文的结构实现规范:

新版执行上下文中,有两个重要的概念:

  • 词法环境:LexicalEnvironment
  • 变量环境:VariableEnvironment

词法环境(LexicalEnvironment)

词法环境由三个部分构成:

  • 环境记录EnvironmentRecord:用于存放变量和函数声明的地方,分为objectdeclarative两种类型,简单理解就是前者是全局上下文中的ER类型,后者是函数上下文中的ER类型
  • 外层引用Outer:提供了访问父词法环境的引用,全局上下文这里为null,简单理解就是原来的作用域链,不过现在只存父级的,是真正的一级一级向上找
  • this绑定ThisBinding:确定当前环境中this的指向

速记:

词法环境分类 = 全局 / 函数 / 模块
词法环境 = ER + outer + this
ER分类 = declarative(DER) + object(OER)
全局ER = DER + OER

变量环境(VariableEnvironment)

在ES6前,声明变量都是通过var关键词声明的,在ES6中则提倡使用letconst来声明变量,为了兼容var的写法,于是使用变量环境来存储var声明的变量。

var关键词有个特性,会让变量提升,而通过let/const声明的变量则不会提升。为了区分这两种情况,就用不同的词法环境去区分。

变量环境本质上仍是词法环境,但它只存储var声明的变量,这样在初始化变量时可以赋值为undefined

函数环境记录更新

ECMA针对函数环境记录额外添加了一些内部属性,用于辨别不同的种类或不同调用方式的函数:

内部属性 Value 说明 补充
[[ThisValue]] Any 函数内调用this时引用的地址,我们常说的函数this绑定就是给这个内部属性赋值
[[ThisStatus]] "lexical" / "initialized" / "uninitialized" 若等于lexical,则为箭头函数,意味着this是空的; 强行new箭头函数会报错TypeError错误
[[FunctionObject]] Object 在这个对象中有两个属性[[Call]][[Construct]],它们都是函数,如何赋值取决于如何调用函数 正常的函数调用赋值[[Call]],而通过newsuper调用函数则赋值[[Construct]]
[[HomeObject]] Object / undefined 如果该函数(非箭头函数)有super属性(子类),则[[HomeObject]]指向父类构造函数 若你写过extends就知道我在说什么
[[NewTarget]] Object / undefined 如果是通过[[Construct]]方式调用的函数,那么[[NewTarget]]非空 在函数中可以通过new.target读取到这个内部属性。以此来判断函数是否通过new来调用的

[ThisStatus]]全称为[[ThisBindingStatus]]

例子

let a = 10;
const b = 20;
var sum;

function add(e, f){
    var d = 40;
    return d + e + f 
}

let utils = {
    add
}

sum = utils.add(a, b)

完整的执行上下文如下所示:

GlobalExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'object',
            add: <function>,
            a: <uninitialized>,
            b: <uninitialized>,
            utils: <uninitialized>,
        },
        outer: null,
        this: <globalObject>
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            type: 'object',
            sum: undefined
        },
        outer: null,
        this: <globalObject>
    },
}

// 当运行到函数add时才会创建函数执行上下文
FunctionExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'declarative',
            arguments: {0: 10, 1: 20, length: 2},
            [[ThisValue]]: <utils>,
            [[NewTarget]]: undefined,
            ...
        },
        outer: <GlobalLexicalEnvironment>,
        this: <utils>
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            type: 'declarative',
            d: undefined
        },
        outer: <GlobalLexicalEnvironment>,
        this: <utils>
    },
}

执行上下文创建后,进入到执行环节,变量在执行过程中赋值、读取、再赋值等。直至程序运行结束。 我们注意到,在执行上下文创建时,变量a、b都是<uninitialized>的,而sum则被初始化为undefined。这就是为什么你可以在声明之前访问var定义的变量(变量提升),而访问let/const定义的变量就会报引用错误的原因。

函数与块级作用域

我们上面提到除了通过var关键字声明的变量会存放在变量环境以外,其它的都会存放于词法环境,那么理论上function 函数声明也会享有词法环境的特性——拥有块级作用域,但是如果你去支持ES6的浏览器进行尝试后会发现事实并非如此,块级作用域中的函数声明依然能被外面访问到,这是为什么呢?

其实,在阮老师的ES6入门中已经给出了答案:

原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

简单来说,就是因为老代码兼容问题,如果贸然修改{}内函数的行为,那么很可能会导致大批量老代码崩溃,因此才有的这种妥协做法,总之就是不要在{}书写函数声明,要写也要写函数表达式,避免不必要的麻烦。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant