作用域、执行上下文(作用域链、变量对象、this)
作用域
动态, 负责收集并保护由所有申明的标识符(变量)组成的一系列查问,确定以后执行的代码对这些标识符的拜访权限
词法作用域和动静作用域:js采纳词法作用域(动态作用域),词法作用域次要在代码的编译阶段,一个变量和函数的词法作用域取决于该变量和函数申明的中央
执行上下文
动静的,能够了解为代码执行前的筹备工作。应用执行上下文栈(Execution context stack, ECStack)来治理执行上下文, 栈底为全局执行上下文globalContext
全局执行上下文是在全局作用域确定之后,JS代码执行之前创立;函数执行上下文是在调用函数时,函数体代码执行之前创立
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. ECStack.push(<checkscope> functionContext);ECStack.push(<f> functionContext);ECStack.pop();ECStack.pop();2.ECStack.push(<checkscope> functionContext);ECStack.pop();ECStack.push(<f> functionContext);ECStack.pop();
一个执行上下文的生命周期分为:
- 创立阶段
在这个阶段中,执行上下文会别离创立变量对象VO,建设作用域链,以及确定this的指向。 - 代码执行阶段
创立实现之后,就会开始执行代码,这个时候,会实现变量赋值,函数援用,以及执行其余代码。
执行上下文蕴含三个属性
(1)变量对象(Variable object,VO)
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包含 Arguments 对象
- 在进入执行上下文时会给变量对象增加形参、函数申明、变量申明等初始的属性值且函数申明优先于变量申明
- 在代码执行阶段,会再次批改变量对象的属性值
(2)作用域链(Scope chain)
由多个执行上下文的变量对象形成的链表
变量查找程序:以后EC的变量对象 -> 父级EC的变量对象 -> 父级的父级EC的变量对象 -> ... -> 全局EC的变量对象
函数有一个外部属性 [[scope]],当函数创立的时候,就会保留所有父变量对象到其中
(3)this
从 ECMASciript 标准解说 this 的指向,参考文末链接
以一个具体的案例详述函数执行上下文中作用域链的建设和变量对象的创立过程以及this指定:
var scope = "global scope";// 函数执行上下文中,用AO(Active Object)示意变量对象function foo(a) { var b = 2; // g(); // 函数申明晋升,失常运行 // d(); // 'd is not a function' 变量申明晋升 // var d = function() {}; // 未声明,不存在于AO中 console.log(e); // Uncaught ReferenceError: e is not defined e = 4; console.log(f); // undefined // 非严格模式下,变量申明提前, 创立阶段AO中存在值为undefined属性f, 因而此处读取为undefined // 严格模式下,Uncaught ReferenceError: Cannot access 'e' before initialization var f = 5; // 函数申明优先 console.log(g); // function g(){ console.log("g"); } var g = 6; function g(){ console.log("g"); } console.log(g); // 6 g();}foo(1);执行过程如下:1. 函数foo申明,保留作用域链到外部属性[[scope]], 此时所有父级变量对象已创立能够拿到,但未蕴含该函数执行上下文的变量对象,因而不是残缺的作用域链foo.[[scope]] = [globalContext.VO];2. 函数foo执行,先创立foo执行上下文并压入执行上下文栈ECStack = [fooContext, globalContext];3. foo函数体代码执行之前,进行筹备工作的第一步建设作用域链,复制函数[[scope]]属性创立作用域链fooContext = { Scope: foo.[[scope]],}4. 进行筹备工作的第二步,创建活动对象,以arguments对象、形参、函数申明、变量申明来初始化AO对象fooContext = { AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, f: undefined, g: reference to function g(){}, ~g: undefined } Scope: foo.[[scope]],}5. 将流动对象压入foo作用域链前端, 造成foo函数的残缺作用于链fooContext = { AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, f: undefined, g: reference to function g(){}, ~g: undefined } Scope: [AO, foo.[[scope]]],}6. 函数体代码执行,批改流动对象属性值fooContext = { AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 2, f: 5, g: 6 } Scope: [AO, foo.[[scope]]],}7. 函数执行结束,函数执行上下文从执行上下文栈中弹出ECStack = [globalContext];
留神点
以上案例中,有一个留神点,函数申明能够被晋升,然而函数表达式不能被晋升(能够了解为变量申明晋升,此时变量为undefined,,无奈作为一个函数被调用)
闭包
由函数以及申明该函数的词法环境组合而成, 函数关闭了定义时的环境
词法环境:
由环境记录(一个存储局部变量作为其属性的对象)以及对外部的词法环境(Lexical Environment)的援用组成。
所有函数都有名为 [[Environment]] 的暗藏属性,该属性保留了对创立该函数的词法环境的援用
实践上的闭包
指一个函数能够记住其内部变量并能够拜访这些变量, 因为所有函数都能拜访全局变量window,因而能够了解为所有函数都是闭包
实际上的闭包
援用了内部变量,且该内部变量和该函数一起存在,在创立该函数的上下文销毁时,该函数仍然存在
闭包的特点:
在函数内部能拜访函数外部变量,援用的自在变量不能被革除, 因而内存占用较高
let foo = () => { let a = 1; return { // 相当于返回了一个通道,这个通道能够拜访这个函数词法环境中的变量,即函数所须要的数据结构保留了下来 add: () => { return ++a; }, sub: () => { return --a; } }}let f = foo();f.add(); // 2f.sub(); // 1f = null; // 词法环境从内存中被删除
利用场景
解决for循环绑定问题
实质就是通过更改作用域链解决
const helpTexts = [ { msg: 'Your e-mail address' }, { msg: 'Your full name' }, { msg: 'Your age (you must be over 16)' }];const data = [];// 计划1for (let i = 0; i < helpTexts.length; i++) { // let/const生成块级作用域, 每次迭代创立独立的词法环境 const item = helpTexts[i]; data[i]= function () { console.log(item.msg); }}// 计划2, 应用闭包for (var i = 0; i < helpTexts.length; i++) { var item = helpTexts[i]; data[i]= (function (msg) { return function () { console.log(msg); } }(item.msg))}// 批改前后的data[i]函数的作用域链比照// 批改前:data[0]Context = { AO: {}, Scope: [AO, globalContext.VO],}// 批改后:匿名函数Context = { AO: { arguments: { 0: helpTexts[0].msg, length: 1 }, msg: helpTexts[0].msg }}data[0]Context = { AO: {}, Scope: [AO, 匿名函数Context.AO, globalContext.VO],}------------------------或for (var i = 0; i < helpTexts.length; i++) { (function () { var item = helpTexts[i]; data[i] = function () { console.log(item.msg); } })()}data[0](); // Your e-mail addressdata[1](); // Your full namedata[2](); // Your age (you must be over 16)
- 模仿公有办法
// 多个闭包共用一个词法环境const Counter = (function() { let privateCounter = 1; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } }})();console.log(Counter.value()); // 0Counter.increment(); // 1Counter.decrement(); // 0// 相似于function Counter() { let counter = 0; this.increment = function() { return ++counter; } this.decrement = function() { return --counter; }}let counter = new Counter();counter.increment(); // 1counter.decrement(); // 0
- 结构辅助(性能)函数
// 结构过滤器function isBetween(a, b) { return function(x) { return x >= a && x <= b; }}arr = [2, 3, 4, 5, 6, 7, 8]arr.filter(isBetween(3, 5)); // [3, 4, 5]// 排序const users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" }];function byField(key) { return function(a, b) { return a[key] > b[key]; }}users.sort(byField('name')); // (Ann, John, Pete)
最初思考,其实闭包的实质起因是因为“作用域链”
var scope = "global scope";function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f;}var foo = checkscope();foo(); // "local scope"通过前文可得函数f的EC为:fContext = { AO: { ... }, Scope: [AO, checkscopeContext.AO, globalContext.VO]}即便函数checkscope的执行上下文销毁,但f函数的作用域链中援用了checkscope上下文的变量对象,因而该变量对象依然存在于内存中,函数f能通过作用域链找到
this指向
以后执行上下文(global、function 或 eval)的一个属性
- 全局上下文
严格/非严格模式: window
- 函数上下文
严格模式:undefined
非严格模式:window(浏览器) / globalThis(node)
扭转this指向, 应用call、apply、bind
function add(c, d) { console.log(Object.prototype.toString.call(this)); return this.a + this.b + c + d;}var o = {a: 1, b: 3};add.call(o, 5, 7); // 16, [object Object]add.apply(o, [10, 20]); // 34, [object Object]add.call(null, 5, 7);// 非严格模式下,null, undefined转换为window;严格模式下不做转换// 16, [object Window]// bind一次绑定,永恒失效const bindFunc = add.bind(o, 1, 2);bindFunc(); // 7bindFunc.bind({a: 2, b: 4}, 1, 2)(); // 7// 箭头函数中,无奈批改this指向a = {b:1}const foo = () => this.afoo.call({a : 2}); // {b: 1}
- 构造函数的this
function C(){ // 默认返回this this.a = 37;}function C2(){ this.a = 37; return {a:38};}var o = new C();o.a; // 37o = new C2();o.a; // 38
- 类中的this
class Car { constructor() { // 更改sayBye中的this为Car类的实例 this.sayBye = this.sayBye.bind(this); } sayHi() { console.log(`Hello from ${this.name}`); } sayBye() { console.log(`Bye from ${this.name}`); } get name() { return 'Ferrari'; }}class Bird { get name() { return 'Tweety'; }}const car = new Car();const bird = new Bird();bird.sayBye = car.sayBye;bird.sayBye(); // Bye from Ferrari
this绑定优先级:new绑定 > bind绑定 > 绑定(apply/call) > 隐式绑定(obj.foo()) > 默认绑定(独立函数应用))
参考链接:
闭包
JavaScript深刻之从ECMAScript标准解读this