先阅读一段晦涩难懂的文字
执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。 全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一 样。在浏览器中,全局上下文就是我们常说的 window 对象(第 12 章会详细介绍),因此所有通过 var 定 义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域 链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上 下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。
再看示例
var color = "blue";
function changeColor() { if (color === "blue") { color = "red"; } else { color = "blue"; }
}
changeColor();
对这个例子而言,函数 changeColor()的作用域链包含两个对象:一个是它自己的变量对象(就
是定义 arguments 对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量
color,就是因为可以在作用域链中找到它。
作用域链增强
function buildUrl() { let qs = "?debug=true"; with(location){ let url = href + qs; } return url;
}
这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。
buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是
location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那
个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数
上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作
用域(稍后介绍),所以在 with 块之外没有定义。
变量声明
- 使用 var 的函数作用域声明
在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函
数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。
提一个面试常问的问题
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”
(hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。
通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是
Reference Error:
console.log(name); // undefined
var name = 'Jake';
function temp() { console.log(name); // undefined var name = 'Jake';
}
- 使用 let 的块级作用域声明
if (true) { let a;
}
console.log(a); // ReferenceError: a 没有定义
while (true) { let b;
}
console.log(b); // ReferenceError: b 没有定义
function foo() { let c;
}
console.log(c); // ReferenceError: c 没有定义// 这没什么可奇怪的// var 声明也会导致报错
// 这不是对象字面量,而是一个独立的块
// JavaScript 解释器会根据其中内容识别出它来
{ let d;
}
console.log(d); // ReferenceError: d 没有定义
var a;
var a;
// 不会报错
{ let b; let b;
}
// SyntaxError: 标识符 b 已经声明过了
for (var i = 0; i < 10; ++i) {}
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义
- 使用 const 的常量声明
除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。
一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const a; // SyntaxError: 常量声明时没有初始化
const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值
const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值
为其他引用值,但对象的键则不受限制。
const o1 = {};
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,
但会静默失败:
const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined
- 标识符查找
var color = 'blue';
function getColor() { return color;
}
console.log(getColor()); // 'blue'
2023年最后一天晚上(12.31) 先抄写到这,过去一年因为很多事耽误了不少,中断抄经时常有,希望新的一年顺利!
从今诸事愿、胜如旧。人生强健,喜一年入手。 休辞最后、饮酴酥酒。