必威-必威-欢迎您

必威,必威官网企业自成立以来,以策略先行,经营致胜,管理为本的商,业推广理念,一步一个脚印发展成为同类企业中经营范围最广,在行业内颇具影响力的企业。

原文出处必威:,原文出处

2019-09-22 02:16 来源:未知

JavaScript 深入之执行上下文栈

2017/05/13 · JavaScript · 执行上下文

原文出处: 冴羽   

JavaScript 深入之闭包

2017/05/21 · JavaScript · 闭包

原文出处: 冴羽   

JavaScript 深入之执行上下文

2017/05/18 · JavaScript · 执行上下文

原文出处: 冴羽   

Q1函数声明和函数表达式有什么区别

JavaScript 深入之词法作用域和动态作用域

2017/05/04 · JavaScript · 作用域

原文出处: 冴羽   

顺序执行?

如果要问到JavaScript代码执行顺序的话,想必写过JavaScript的开发者都会有个直观的印象,那就是顺序执行,毕竟

var foo = function () { console.log('foo1'); } foo(); // foo1 var foo = function () { console.log('foo2'); } foo(); // foo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var foo = function () {
 
    console.log('foo1');
 
}
 
foo();  // foo1
 
var foo = function () {
 
    console.log('foo2');
 
}
 
foo(); // foo2

然而去看这段代码:

function foo() { console.log('foo1'); } foo(); // foo2 function foo() { console.log('foo2'); } foo(); // foo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
 
    console.log('foo1');
 
}
 
foo();  // foo2
 
function foo() {
 
    console.log('foo2');
 
}
 
foo(); // foo2

打印的结果却是两个foo2。

刷过面试题的都知道这是因为JavaScript引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如第一个例子中的变量提升,和第二个例子中的函数提升。

但是本文真正想让大家思考的是:这个”一段一段”中的“段”究竟是怎么划分的呢?

到底JavaScript引擎遇到一段怎样的代码时才会做’准备工作’呢?

定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1; function foo() { console.log(a); } foo();

1
2
3
4
5
6
7
var a = 1;
 
function foo() {
    console.log(a);
}
 
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

还真是这样的!

所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?

别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

接下来就来讲讲实践上的闭包。

前言

在《JavaScript深入之执行上下文栈》中讲到,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

然后分别在《JavaScript深入之变量对象》、《JavaScript深入之作用域链》、《JavaScript深入之从ECMAScript规范解读this》中讲解了这三个属性。

阅读本文前,如果对以上的概念不是很清楚,希望先阅读这些文章。

因为,这一篇,我们会结合着所有内容,讲讲执行上下文的具体处理过程。

函数声明 VS 函数表达式

JavaScript 中需要创建函数的话,有两种方法:函数声明、函数表达式,各自写法如下:
<pre>// 方法一:函数声明
function foo() {}
// 方法二:函数表达式
var foo = function () {};</pre>
另外还有一种自执行函数表达式,主要用于创建一个新的作用域,在此作用域内声明的变量不会和其它作用域内的变量冲突或混淆,大多是以匿名函数方式存在,且立即自动执行:
<pre>(function () {
// var x = ...
})();</pre>
此种自执行函数表达式归类于以上两种方法的第二种,也算是函数表达式。

方法一和方法二都创建了一个函数,且命名为 foo
,但是二者还是有区别的。JavaScript 解释器中存在一种变量声明被提升(hoisting)的机制,也就是说变量(函数)的声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。

例如以下代码段:
alert(foo); // function foo() {}
alert(bar); // undefined
function foo() {}
var bar = function bar_fn() {};
alert(foo); // function foo() {}
alert(bar); // function bar_fn() {}
输出结果分别是function foo() {}、undefined、function foo() {}和function bar_fn() {}。

可以看到 foo的声明是写在 alert 之后,仍然可以被正确调用,因为 JavaScript 解释器会将其提升到 alert 前面,而以函数表达式创建的函数 bar则不享受此待遇。
那么bar究竟有没有被提升呢,其实用 var 声明的变量都会被提升,只不过是被先赋值为 undefined罢了,所以第二个 alert 弹出了 undefined。
所以,JavaScript 引擎执行以上代码的顺序可能是这样的:
1.创建变量 foo和 bar,并将它们都赋值为 undefined。
2.创建函数 foo的函数体,并将其赋值给变量 foo。
3.执行前面的两个 alert。
4.创建函数 bar_fn,并将其赋值给 bar。
5.执行后面的两个 alert。

注:
严格地说,再 JavaScript 中创建函数的话,还有另外一种方法,称为“函数构造法”:
<pre>var foo = Function('alert("hi!");');
var foo = new Function('alert("hi!");'); // 等同于上面一行</pre>
此方法以一个字符串作为参数形成函数体。但是用这种方法,执行效率方面会打折扣,且似乎无法传递参数,所以少用为妙。
翻译整理自:http://www.reddit.com/r/javascript/comments/v9uzg/the_different_ways_to_write_a_function/

作用域

作用域是程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

ECMAScript6之前只有全局作用域和函数作用域。

JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

可执行代码

这就要说到JavaScript的可执行代码(executable code)的类型有哪些了?

其实很简单,就三种,全局代码、函数代码、eval代码。

举个例子,当执行到一个函数的时候,就会进行准备工作,这里的’准备工作’,让我们用个更专业一点的说法,就叫做”执行上下文(execution contexts)”。

分析

让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:

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

1
2
3
4
5
6
7
8
9
10
11
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
 
var foo = checkscope();
foo();

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

另一个与这段代码相似的例子,在《JavaScript深入之执行上下文》中有着非常详细的分析。如果看不懂以下的执行过程,建议先阅读这篇文章。

这里直接给出简要的执行过程:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链、this等
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)

然而 JavaScript 却是可以的!

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:

fContext = { Scope: [AO, checkscopeContext.AO, globalContext.VO], }

1
2
3
fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。

思考题

在《JavaScript深入之词法作用域和动态作用域》中,提出这样一道思考题:

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

1
2
3
4
5
6
7
8
9
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
2
3
4
5
6
7
8
9
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

两段代码都会打印’local scope’。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

紧接着就在下一篇《JavaScript深入之执行上下文栈》中,讲到了两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。

Q2什么是变量的声明前置?什么是函数的声明前置

什么是变量的声明前置?

JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,然后给他初始值undefined,然后才逐句执行程序,这就叫做“变量提升”,也即“变量的声明前置”。

必威 1

什么是函数的声明前置?

和变量的声明会前置一样,函数声明同样会前置,如果我们使用函数表达式那么规则和变量一样,如下图:

必威 2

如果我们使用函数声明的方式,那么即使函数写在最后也可以在前面语句调用,前提是函数声明部分已经被下载到本地。

必威 3

静态作用域与动态作用域

因为采用词法作用域,函数的作用域在函数定义的时候就决定了。

与词法作用域相对的是动态作用域,函数的作用域在函数调用的时候才决定。

让我们认真看个例子就能明白之间的区别:

var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar();

1
2
3
4
5
6
7
8
9
10
11
12
var value = 1;
 
function foo() {
    console.log(value);
}
 
function bar() {
    var value = 2;
    foo();
}
 
bar();

当采用静态作用域时,执行foo函数,先从foo函数内部查找是否有局部变量value,如果没有,就根据书写的位置,查找上面一层的代码,在这里是全局作用域,也就是value等于1,所以最后会打印1

当采用动态作用域时,执行foo函数,依然是从foo函数内部查找是否有局部变量value。如果没有,就从调用函数的作用域,也就是bar函数内部查找value变量,所以最后会打印2

执行上下文栈

接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?

所以js引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

1
    ECStack = [];

试想当JavaScript开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,让我们用globalContext表示它,并且只有当整个应用程序结束的时候,ECStack才会被清空,所以ECStack最底部永远有个globalContext:

ECStack = [ globalContext ];

1
2
3
    ECStack = [
        globalContext
    ];

现在JavaScript遇到下面的这段代码了:

function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1();

1
2
3
4
5
6
7
8
9
10
11
12
13
function fun3() {
    console.log('fun3')
}
 
function fun2() {
    fun3();
}
 
function fun1() {
    fun2();
}
 
fun1();

当遇到函数执行的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:

// 伪代码 // fun1() ECStack.push(fun1> functionContext); // fun1中竟然调用了fun2,还要创建fun2的执行上下文 ECStack.push(fun2> functionContext); // 擦,fun2还调用了fun3! ECStack.push(fun3> functionContext); // fun3执行完毕 ECStack.pop(); // fun2执行完毕 ECStack.pop(); // fun1执行完毕 ECStack.pop(); // javascript接着执行下面的代码,但是ECStack底层用于有个globalContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 伪代码
 
// fun1()
ECStack.push(fun1> functionContext);
 
// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(fun2> functionContext);
 
// 擦,fun2还调用了fun3!
ECStack.push(fun3> functionContext);
 
// fun3执行完毕
ECStack.pop();
 
// fun2执行完毕
ECStack.pop();
 
// fun1执行完毕
ECStack.pop();
 
// javascript接着执行下面的代码,但是ECStack底层用于有个globalContext

必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = []; for (var i = 0; i 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();

1
2
3
4
5
6
7
8
9
10
11
var data = [];
 
for (var i = 0; i  3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
 
data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = { VO: { data: [...], i: 3 } }

1
2
3
4
5
6
globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = { Scope: [AO, globalContext.VO] }

1
2
3
data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = []; for (var i = 0; i 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2]();

1
2
3
4
5
6
7
8
9
10
11
12
13
var data = [];
 
for (var i = 0; i  3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}
 
data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = { VO: { data: [...], i: 3 } }

1
2
3
4
5
6
globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = { Scope: [AO, 匿名函数Context.AO globalContext.VO] }

1
2
3
data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的AO为:

匿名函数Context = { AO: { arguments: { 0: 1, length: 1 }, i: 0 } }

1
2
3
4
5
6
7
8
9
匿名函数Context = {
    AO: {
        arguments: {
            0: 1,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

data[1] 和 data[2] 是一样的道理。

具体执行分析

我们分析第一段代码:

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

1
2
3
4
5
6
7
8
9
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

执行过程如下:

1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈

ECStack = [ globalContext ];

1
2
3
    ECStack = [
        globalContext
    ];

2.全局上下文初始化

globalContext = { VO: [global, scope, checkscope], Scope: [globalContext.VO], this: globalContext.VO }

1
2
3
4
5
    globalContext = {
        VO: [global, scope, checkscope],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

2.初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]

checkscope.[[scope]] = [ globalContext.VO ];

1
2
3
    checkscope.[[scope]] = [
      globalContext.VO
    ];

3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ECStack = [ checkscopeContext, globalContext ];

1
2
3
4
    ECStack = [
        checkscopeContext,
        globalContext
    ];

4.checkscope 函数执行上下文初始化:

  1. 复制函数 [[scope]] 属性创建作用域链,
  2. 用 arguments 创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入 checkscope 作用域链顶端。

同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }

1
2
3
4
5
6
7
8
9
10
11
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }

5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈

ECStack = [ fContext, checkscopeContext, globalContext ];

1
2
3
4
5
    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];

6.f 函数执行上下文初始化, 以下跟第 4 步相同:

  1. 复制函数 [[scope]] 属性创建作用域链
  2. 用 arguments 创建活动对象
  3. 初始化活动对象,即加入形参、函数声明、变量声明
  4. 将活动对象压入 f 作用域链顶端

fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }

1
2
3
4
5
6
7
8
9
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }

7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [ checkscopeContext, globalContext ];

1
2
3
4
    ECStack = [
        checkscopeContext,
        globalContext
    ];

9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack = [ globalContext ];

1
2
3
    ECStack = [
        globalContext
    ];

第二段代码就留给大家去尝试模拟它的执行过程。

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

1
2
3
4
5
6
7
8
9
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

不过,在下一篇《JavaScript深入之闭包》中也会提及这段代码的执行过程。

Q3arguments 是什么

是一个长的很像数组的对象,可以通过该对象获取到函数的所有传入参数。

必威 4

动态作用域

也许你会好奇什么语言是动态作用域?

bash就是动态作用域,不信的话,把下面的脚本存成例如scope.bash,然后进入相应的目录,用命令行执行 bash ./scope.bash,看看打印的值是多少

value=1 function foo () { echo $value; } function bar () { local value=2; foo; } bar

1
2
3
4
5
6
7
8
9
value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar

这个文件也可以在demos/scope/中找到。

TAG标签:
版权声明:本文由必威发布于必威-前端,转载请注明出处:原文出处必威:,原文出处