共计 6264 个字符,预计需要花费 16 分钟才能阅读完成。
作用域、执行上下文(作用域链、变量对象、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(); // 2
f.sub(); // 1
f = null; // 词法环境从内存中被删除
利用场景
-
解决 for 循环绑定问题
实质就是通过更改作用域链解决
const helpTexts = [{ msg: 'Your e-mail address'},
{msg: 'Your full name'},
{msg: 'Your age (you must be over 16)' }
];
const data = [];
// 计划 1
for (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 address
data[1](); // Your full name
data[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()); // 0
Counter.increment(); // 1
Counter.decrement(); // 0
// 相似于
function Counter() {
let counter = 0;
this.increment = function() {return ++counter;}
this.decrement = function() {return --counter;}
}
let counter = new Counter();
counter.increment(); // 1
counter.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(); // 7
bindFunc.bind({a: 2, b: 4}, 1, 2)(); // 7
// 箭头函数中,无奈批改 this 指向
a = {b:1}
const foo = () => this.a
foo.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; // 37
o = 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