犀牛书第七版学习笔记:执行上下文与作用域

发布于:2023-01-04 ⋅ 阅读:(387) ⋅ 点赞:(0)

目录

1.执行上下文

2.作用域链

3.作用域链增强Scope Chain Augmentation

4.变量声明 Variable Declaration

4.1使用 var 的函数作用域声明 Function Scope Declaration Using var

4.2 使用 let 的块级作用域声明 Block Scope Declaration Using let

4.3 使用 const 的常量声明Constant Declaration Using const

4.4 标识符查找Identifier Lookup


1.执行上下文

执行上下文(以下简称“上下文”)execution context

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。The execution context of a variable or function defines what other data it has access to, as well as how it should behave

每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

虽然无法通过代码访问变量对象,但后台处理数据会用到它。

全局上下文global execution context是最外层的上下文。

根据 ECMAScript 实现的宿主环境host environment for an ECMAScript implementation,表示全局上下文的对象可能不一样。

在浏览器中,全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数 When an execution context has executed all of its code, it is destroyed, taking with it all of the variables and functions defined within it

全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器

每个函数调用function call都有自己的上下文。当代码执行流进入函数时Whenever code execution flows into a function,函数的上下文被推到一个上下文栈上context stack。

在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文

ECMAScript程序的执行流就是通过这个上下文栈进行控制的。

2.作用域链

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。When code is executed in a context, a scope chain of variable objects is created

这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。The purpose of the scope chain is to provide ordered access to all variables and functions that an execution context has access to.

代码正在执行的上下文的变量对象始终位于作用域链的最前端。The front of the scope chain is always the variable object of the context whose code is executing

如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。

If the context is a function, then the activation object is used as the variable object

作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;The next variable object in the chain is from the containing context, and the next after that is from the next containing context. This pattern continues until the global context is reached

全局上下文的变量对象始终是作用域链的最后一个变量对象 the global context’s variable object is always the last of the scope chain

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。Identifiers are resolved by navigating the scope chain in search of the identifier name

搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)

The search always begins at the front of the chain and proceeds to the back until the identifier is found. (If the identifier isn’t found, typically an error occurs.)

var color = "blue"; 
function changeColor() { 
 if (color === "blue") { 
 color = "red"; 
 } else { 
 color = "blue"; 
 } 
} 
changeColor();
//函数 changeColor()的作用域链包含两个对象:一个是它自己的变量对象(就是定义 arguments 对象的那个)the function changeColor() has a scope chain with two objects in it: its own variable object (upon which the arguments object is defined)
//另一个是全局上下文的变量对象。
//这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它。The variable color is therefore accessible inside the function, because it can be found in the scope chain

局部作用域中定义的变量可用于在局部上下文中替换全局变量

locally defined variables can be used interchangeably with global variables in a local context.

var color = "blue"; 
function changeColor() { 
 let anotherColor = "red"; 
 function swapColors() { 
 let tempColor = anotherColor; 
 anotherColor = color; 
 color = tempColor; 
 // 这里可以访问 color、anotherColor 和 tempColor 
 } 
 // 这里可以访问 color 和 anotherColor,但访问不到 tempColor 
 swapColors(); 
} 
// 这里只能访问 color 
changeColor();

以上代码涉及 3 个上下文:全局上下文、changeColor()的局部上下文和 swapColors()的局部上下文。全局上下文中有一个变量 color 和一个函数 changeColor()。changeColor()的局部上下文中有一个变量 anotherColor 和一个函数 swapColors(),但在这里可以访问全局上下文中的变量 color。 swapColors()的局部上下文中有一个变量 tempColor,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到 tempColor。而在 swapColors()中则可以访问另外两个上下文中的变量,因为它们都是父上下文。

 

**内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。**上下文之间的连接是线性的、有序的。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。

swapColors()局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。

changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问 swapColors()的上下文。

函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则

3.作用域链增强Scope Chain Augmentation

执行上下文主要有全局上下文和函数上下文两种 two primary types of execution contexts, global and function

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。

Certain statements cause a temporary addition to the front of the scope chain that is later removed after code execution

  • try/catch 语句的 catch 块
  • with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。add a variable object to the front of the scope chain

对 with 语句来说,会向作用域链前端添加指定的对象the specified object;

对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。a new variable object is created and contains a declaration for the thrown error object

function buildUrl() {
 let qs = "?debug=true";
 
 with(location){
 let url = href + qs; 
 }
 
 return url;
}

with 语句将 location 对象作为上下文the with statement is acting on the location object,因此 location 会被添加到作用域链前端。

buildUrl()函数中定义了一个变量 qs

当 with 语句中的代码引用变量 href 时,实际上引用的是location.href,也就是自己变量对象的属性。

When the variable href is referenced, it’s actually referring to location.href, which is in its own variable object.

在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,它定义在函数上下文的变量对象上。

When the variable qs is referenced, it’s referring to the variable defined in buildUrl(), which is in the function context’s variable object.

在 with 语句中 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回

Inside the with statement is a variable declaration for url, which becomes part of the function’s context and can, therefore, be returned as the function value

4.变量声明 Variable Declaration

4.1使用 var 的函数作用域声明 Function Scope Declaration Using var

在使用 var 声明变量时,变量会被自动添加到最接近的上下文。the most immediate context available

在函数中,最接近的上下文就是函数的局部上下文。function’s local context

在 with 语句中,最接近的上下文也是函数上下文。

如果变量未经声明就被初始化了If a variable is initialized without first being declared,那么它就会自动被添加到全局上下文

function add(num1, num2) { 
 var sum = num1 + num2; //函数 add()定义了一个局部变量 sum,保存加法操作的结果。
 return sum; 
} 
let result = add(10, 20); // 30 
console.log(sum); // 报错:sum 在这里不是有效变量
//这个值作为函数的值被返回,但变量 sum 在函数外部是访问不到的。

//如果省略上面例子中的关键字 var,那么 sum 在 add()被调用call之后就变成可以访问的了
function add(num1, num2) { 
 sum = num1 + num2; //变量 sum 被用加法操作的结果初始化时并没有使用 var 声明
 return sum; 
} 
let result = add(10, 20); // 30 
console.log(sum); // 30 在调用 add()之后,sum被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到

//**未经声明而初始化变量是 JavaScript 编程中一个非常常见的错误,会导致很多问题**。

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”(hoisting)。A var declaration will be brought to the top of the function or global scope and before any existing code inside it

提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。use a hoisted variable anywhere in the same scope without consideration for whether or not it was declared yet

可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。

//在全局作用域中两段等价的代码
var name = "Jake"; 
// 等价于:
name = 'Jake'; 
var name;

//两个等价的函数
function fn1() { 
 var name = 'Jake'; 
} 
// 等价于:
function fn2() { 
 var name; 
 name = 'Jake'; 
}

通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是Reference Error

console.log(name); // undefined 
var name = 'Jake'; 
function() { 
 console.log(name); // undefined 
 var name = 'Jake'; 
}

4.2 使用 let 的块级作用域声明 Block Scope Declaration Using let

块级作用域由最近的一对包含花括号{}界定。Block scope is defined as the nearest set of enclosing curly braces {}

换句话说,if 块、while 块、function 块,甚至连单独的块standalone blocks也是 let 声明变量的作用域 the extent of the scope of any variable declared with 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 声明也会导致报错
// 这不是对象字面量object literal,而是一个独立的块standalone block
// JavaScript 解释器会根据其中内容识别出它来
{ 
 let d; 
} 
console.log(d); // ReferenceError: d 没有定义

let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError

var a; 
var a; 
// 不会报错
{ 
 let b; 
 let b; 
} 
// SyntaxError: 标识符 b 已经声明过了

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。

for (var i = 0; i < 10; ++i) {} 
console.log(i); // 10 
for (let j = 0; j < 10; ++j) {} 
console.log(j); // ReferenceError: j 没有定义

4.3 使用 const 的常量声明Constant Declaration Using const

使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值

const a; // SyntaxError: 常量声明时没有初始化
const b = 3; 
console.log(b); // 3 
b = 4; // TypeError: 给常量赋值

//const除了要遵循以上规则,其他方面与 let 声明是一样的
if (true) { 
 const a = 0; 
} 
console.log(a); // ReferenceError: a 没有定义

****while (true) { 
 const b = 1; 
} 
console.log(b); // ReferenceError: b 没有定义
function foo() { 
 const c = 2; 
} 
console.log(c); // ReferenceError: c 没有定义
{ 
 const d = 3; 
} 
console.log(d); // ReferenceError: d 没有定义

const 声明只应用到顶级原语或者对象 The const declaration only applies to the top-level primitive or object

赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制

const o1 = {}; 
o1 = {}; // TypeError: 给常量赋值
const o2 = {}; 
o2.name = 'Jake'; 
console.log(o2.name); // 'Jake'

如果想让整个对象都不可变,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败

If you wish to make the entire object immutable, you can use Object.freeze(), although attempted property assignment will not raise errors; it will just silently fail

const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。Because const declarations imply that the value is of a single type and immutable, the JavaScript runtime compiler can replace all instances of it with its actual value instead of performing a variable lookup through a lookup table. The Google Chrome V8 engine performs such an optimization.

4.4 标识符查找Identifier Lookup

当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。When an identifier is referenced for either reading or writing within a particular context, a search must take place to determine what identifier it represents.

搜索开始于作用域链前端,以给定的名称搜索对应的标识符。The search starts at the front of the scope chain, looking for an identifier with the given name.

如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。If it finds that identifier name in the local context, then the search stops and the variable is set; if the search doesn’t find the variable name, it continues along the scope chain.

(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)objects in the scope chain also have a prototype chain, so searching may include each object’s prototype chain

这个过程一直持续到搜索至全局上下文的变量对象。This process continues until the search reaches the global context’s variable object.

如果仍然没有找到标识符,则说明其未声明。

var color = 'blue'; 
function getColor() { 
 return color; 
} 
console.log(getColor()); // 'blue'
//调用函数 getColor()时会引用变量 color。为确定 color 的值会进行两步搜索。
//第一步,搜索 getColor()的变量对象,查找名为 color 的标识符。
//结果没找到,于是继续搜索下一个变量对象(来自全局上下文),然后就找到了名为 color 的标识符。因为全局变量对象上有 color的定义
//所以搜索结束

引用局部变量会让搜索自动停止,而不继续搜索下一级变量对象 referencing local variables automatically stops the search from going into another variable object

如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符 identifiers in a parent context cannot be referenced if an identifier in the local context has the same name

var color = 'blue';
 
function getColor() {
 let color = 'red';
 return color;
}
 
console.log(getColor()); // 'red'

使用块级作用域声明并不会改变搜索流程,但可以给词法层级(lexical hierarchy)添加额外的层次

var color = 'blue'; 
function getColor() { 
 let color = 'red'; 
 { 
 let color = 'green'; 
 return color; //在执行到函数返回语句时,代码知道必须使用变量 color
 } 
} 
console.log(getColor()); // 'green'
//于是开始在局部上下文中搜索这个标识符,结果找到了值为'green'的变量 color。因为变量已找到,搜索随即停止,所以就使用这个局部变量。

getColor()内部声明了一个名为 color 的局部变量 a local variable named color is declared inside the getColor() function. 在调用这个函数时,变量会被声明 When the function is called, the variable is declared