js执行上下文、作用域链、闭包

执行上下文

是评估和执行 JavaScript 代码的环境的抽象概念,每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
执行上下文包括:

  • 1、全局执行上下文
  • 2、函数执行上下文
  • 3、Eval函数执行上下文

三个重要属性

  • 变量对象
  • 作用域链
  • this

执行栈

用来存储代码运行时创建的所有上下文(先进后出)
但javascript引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文,并压入到当前的执行栈中,每当遇到函数调用会创建一个新的执行上下文并压入到栈顶,当函数执行结束时,执行上下文会从栈中弹出,控制流程到下一个执行上下文,直到所以代码执行完毕后从当前栈中移除全局执行上下文。
创建执行上下文有两个阶段

1) 创建阶段

  • This 绑定
  • 创建词法环境组件
    内部有两个组件: (1) 环境记录器和 (2) 一个外部环境的引用。
    两种类型:全局和函数(this不同,外部环境引用不同)
    在全局环境中,环境记录器是对象环境记录器。
    在函数环境中,环境记录器是声明式环境记录器。
  • 创建变量环境组件
    2) 执行阶段

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

词法(静态)作用域与动态作用域区别
javascript采用的是词法作用域,函数的作用域在函数定义的时候就确定了
动态作用域的函数作用域取决于函数调用的时候。

变量对象

每一个执行上下文都会分配一个变量对象(variable object),变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。在函数上下文情况下,参数列表(parameter list)也会被加入到变量对象(variable object)中作为属性。变量对象与当前作用域息息相关。不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。
有一点特殊就是只有 函数声明(function declaration) 会被加入到变量对象中,而 函数表达式(function expression)则不会。看代码:

1
2
3
4
5
function a() {}
console.log(typeof a) // function
var a = function _a() {}
console.log(typeof a) // function
console.log(typeof _a) // undefined

活动对象

当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化

作用域链

由多个执行上下文的变量对象构成的链表就叫做作用域链。
以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

  • 函数创建
    函数的作用域在函数定义的时候就决定了。

这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

  • 函数激活
    当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端
    Scope = [AO].concat([[Scope]]);

闭包

闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束
闭包的外部作用域是在其定义的时候已决定,而不是执行的时候(词法作用域)

当外部作用域执行完后,内部函数还存活时,闭包才真正发挥它的作用
譬如一下几种情况:

  • 异步任务(定时器,事件处理,ajax请求中的回调)
  • 被外部函数作为返回结果,或者返回结果对象中引用该内部函数

闭包与封装性
封装意味着信息隐藏
函数与私有状态:通过闭包,可以创建拥有私有状态的函数,闭包使得状态被封装起来。

工厂模式与私有原型对象
我们可以通过闭包,只用创建原型对象一次,也能够被所有 Todo 函数调用所公用,并且保证其私有性。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
let Todo = (function createTodoFactory(){
let todoPrototype = {
toString : function() {
return this.id + " " + this.userName + ": " + this.title;
}
}
return function(todo){
let newTodo = Object.create(todoPrototype);
Object.assign(newTodo, todo);
return newTodo;
}
})();
let todo = Todo({id : 1, title: "This is a title", userName: "Cristi", completed: false });

工厂模式与私有构造函数

1
2
3
4
5
6
7
8
9
10
let Todo = (function createTodoFactory(){
function Todo(spec){
Object.assign(this, spec);
}

return function(spec){
let todo = new Todo(spec);
return Object.freeze(todo);
}
})();

这里,Todo() 工厂函数就是一个闭包。通过它,不管是否使用 new ,我们都可以创建不可变对象,原型对象也只用创建一次,并且它是私有的。

垃圾回收

在 Javascript中,局部变量会随着函数的执行完毕而被销毁,除非还有指向他们的引用。当闭包本身也被垃圾回收之后,这些闭包中的私有状态随后也会被垃圾回收。通常我们可以通过切断闭包的引用来达到这一目的。
将闭包函数置为 null

总结

闭包是一个可以访问外部作用域中变量的内部函数。

这些被引用的变量直到闭包被销毁时才会被销毁。

闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易。

可以通过闭包来达到封装性。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!