JavaScript 进阶
this 绑定规则
全局作用域 this 指向
-
浏览器环境:指向
window -
NodeJS 环境
- 指向一个空对象
{} - 执行 js 文件,会当作一个模块进行加载 -> 编译 -> 放入一个函数
compiledWrapper.call({}),使用call调用该函数同时绑定了一个空对象
- 指向一个空对象
-
this 绑定特点
- 函数在调用时,JavaScript 引擎会默认给 this 绑定一个值
- this 的绑定和定义的位置没有关系
- this 的绑定和调用方式以及调用的位置有关
- this 是在运行时被动态绑定的
绑定规则一:默认绑定
-
独立函数调用 —— 函数没有被绑定到某个对象上进行调用,函数调用时没有调用主体
-
this 指向
- 默认绑定时严格模式(
"use strict"):指向undefined - 非严格模式
- 浏览器环境:指向
window对象 - NodeJS 环境:指向
{}
- 浏览器环境:指向
- 默认绑定时严格模式(
-
案例(浏览器环境下)
// 直接调用
function foo() {
console.log(this);
}
foo(); // window
// 创建一个对象,对象中的属性指向 foo
const obj = {
foo: function() {
console.log(this);
}
};
const bar = obj.foo;
bar(); // window
// 闭包函数
function fun() {
return function() {
console.log(this);
}
}
const fn = fun();
fn(); // window
// 严格模式
const o = {
name: 'why',
foo() {
"use strict";
console.log(this)
}
}
const boo = o.foo;
boo(); // undefined
绑定规则二:隐式绑定
-
通过某个对象进行调用——将函数作为对象的属性方法,此时 this 绑定为这个发起调用的对象
-
案例
function foo() {
console.log(this);
}
const obj = {
name: 'obj',
foo
}
obj.foo(); // { name: 'obj', foo: [Function: foo] }
const obj1 = {
name: 'obj1',
foo() {
console.log(this);
}
}
const obj2 = {
name: 'obj2',
bar: obj1.foo
}
obj2.bar(); // { name: 'obj2', bar: [Function: foo] }
绑定规则三:显示绑定
-
隐式绑定有一个前提:必须在调用对象内部有一个对函数的引用(比如一个属性)
-
如果不希望在对象内部包含这个函数的引用,又想在这个对象上进行强制调用(隐式绑定),可以使用显示绑定
call(thisArg: any, ...argArray: any[])apply(thisArg: any, argArray?: any[])bind(thisArg: any, ...argArray: any[])
function foo(...args) {
console.log(this, ...args)
}
foo.call(true, 1, 2); // Boolean {true} 1 2
foo.apply(true, [1, 2]); // Boolean {true} 1 2
const bar = foo.bind("aaa", 1, 2);
bar(); // 显示绑定优先级 > 隐式绑定 String {'aaa'} 1 2
bar(3, 4); // String {'aaa'} 1 2 3 4 -
需要注意的是,一个函数通过
bind绑定this只能绑定一次,多次调用bind不能覆盖前面的绑定function foo() {
console.log(this);
}
const newFoo = foo.bind([1, 2]).bind({});
newFoo(); // [1, 2]
绑定规则四:new 绑定
使用 new 关键字调用函数时,会执行以下操作
- 创建一个新 的对象,this 指向这个创建的对象
- 这个新对象会被执行
prototype连接 - 这个新对象会绑定到函数调用的的
this上 - 如果函数没有
return非空对象,表达式会返回这个生成的新对象
function Person(name) {
this.name = name;
this.say = function () {
console.log(this.name);
}
}
const p = new Person('why');
console.log(p); // Person {name: 'why', say: ƒ}
p.say(); // why
内置函数的 this 绑定
-
setTimeout/setInterval-
浏览器环境:指向
windowsetTimeout(function() {
console.log(this); // window
}, 1000); -
NodeJS 环境:指向创建出来的定时器对象
const timer = setTimeout(function() {
console.log(this); // Timeout {}
console.log(timer === this); // true
}, 1000);
-
-
浏览器事件监听函数,如
onClick、addEventListener— 指向绑定事件的元素 -
数组的遍历方法
forEach/map/find/filter-
默认情况(不传递 this 指向)
-
浏览器:指向
window[1].forEach(function () {
console.log(this); // window
}); -
NodeJS 环境:指向全局对象
global[1].forEach(function () {
console.log(this); // Object [global] {}
});
-
-
可以传递第二个参数指定 this:指向传递的 this 参数
[1].forEach(function () {
console.log(this) // String {'aaa'}
}, 'aaa');
-
绑定规则优先级
-
默认绑定的优先级最低
function foo() {
console.log(this);
}
const obj = { name: 'obj', foo };
obj.foo(); // { name: 'obj', foo: [Function: foo] } -
显示绑定的优先级高于隐式绑定
bind优先级高于apply/call
const o = {
name: 'o',
foo() {
console.log(this);
}
}
o.foo(); // { name: 'o', foo: [Function: foo] }
o.foo.call('aaa'); // String {'aaa'}
function foo() {
console.log(this);
}
const o2 = {
name: 'o2',
foo: foo.bind('aaa')
}
o2.foo(); // String {'aaa'}
// bind 优先级高于 call/apply
foo.bind('ooo').call('ccc'); // String {'ooo'} -
new 绑定的优先级高于显示绑定
function foo() {
console.log(this);
}
const bar = foo.bind('aaa');
const obj = new bar(); // foo {} -
优先级:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
this 特殊绑定
-
忽略显示绑定:使用
call/apply/bind显示绑定时传入null或undefined时,会忽略显示绑定,指向全局对象- 浏览器环境下指向
window - NodeJS 环境指向
global
// 忽略显示绑定
function foo() {
console.log(this);
}
// 浏览器环境:window
// Node环境:Global
foo.call(null);
foo.apply(undefined);
foo.bind(null)(); - 浏览器环境下指向
-
间接函数引用:会解析成独立函数调用
const obj1 = {
name: 'obj1',
foo() {
console.log(this);
}
};
const obj2 = { name: 'obj2' };
// 赋值语句返回 foo 函数
(obj2.foo = obj1.foo)(); // 指向全局对象
箭头函数 this 访问
-
箭头函数没有自己的 this 和 arguments ,上述四种绑定规则不适用于箭头函数
-
在箭头函数中使用
this,会向上层作用域中进行查找const message = 'app';
const obj = {
message: 'obj',
foo1: () => console.log(this.message),
foo2() {
return () => {
console.log(this.message);
};
}
};
obj.foo1(); // undefined,全局对象上不存在 message 属性
obj.foo2()(); // obj
obj.foo2.call({ message: 'obj2' })(); // 上层作用域 foo2 函数内部绑定 obj2
call/apply/bind 模拟
-
模拟
call函数// 剩余参数定义的数组,如果没有传递参数,会是一个空数组
// Object(variable) 函数可以将变量转化为对应的对象类型
Function.prototype.customCall = function(thisArg, ...argArray) {
// 获取要调用的函数
const fn = this;
// 判断 thisArg 的类型
const newThisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : globalThis;
// 将传递的 thisArg 绑定在调用的函数上
const key = Symbol();
// 使用 Symbol 值作为 key,防止覆盖 newThisArg 上的属性
newThisArg[key] = fn;
// 调用函数
const result = newThisArg[key](...argArray);
// 删除 fn 属性
delete newThisArg[key];
// 返回函数执行的结果
return result;
}
function foo() {
console.log(this);
}
function sum(n1, n2) {
console.log(this);
return n1 + n2;
}
foo.customCall(null); // window
foo.customCall(11); // Number {11, [Symbol()]: f}
const res = sum.customCall('aaa', 10, 20); // String {'aaa', [Symbol()]: f}
console.log(res); // 30 -
模拟
apply函数Function.prototype.customApply = function(thisArg, argArray = []) {
// 获取要调用的函数
const fn = this;
// 判断 thisArg 的类型
const newThisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : globalThis;
// 将传递的 thisArg 绑定在调用的函数上
const key = Symbol();
// 使用 Symbol 值作为 key,防止覆盖 newThisArg 上的属性
newThisArg[key] = fn;
// 调用函数
const result = newThisArg[key](...argArray);
// 删除 fn 属性
delete newThisArg[key];
// 将函数调用结果返回
return result;
}
function foo() {
console.log(this);
}
function sum(...numbers) {
console.log(this, Math.max(...numbers));
}
foo.customApply(11); // Number {11, [Symbol()]: f}
sum.customApply(null, [1, 2, 3]); // window 3 -
模拟
bind函数Function.prototype.customBind = function(thisArg, ...argArray) {
// 获取要调用的函数
const fn = this;
// 判断 thisArg 的类型
const newThisArg = (thisArg !== undefined && thisArg!== null) ? Object(thisArg) : globalThis;
return function(...args) {
// 将传递的 thisArg 绑定在生成的函数上
const key = Symbol();
// 使用 Symbol 值作为 key,防止覆盖 newThisArg 上的属性
newThisArg[key] = fn;
// 调用函数
const result = newThisArg[key](...argArray, ...args);
// 删除 fn 属性
delete newThisArg[key];
// 将函数调用结果返回
return result;
}
}
function foo() {
console.log(this);
}
function sum(...args) {
console.log(this, args);
}
foo.customBind('111')(); // String {'111', [Symbol()]: ƒ}
sum.customBind({}, 10, 20)(30, 40, 50); // {[Symbol()]: ƒ} [10, 20, 30, 40, 50]
深入浏览器渲染原理
网页解析过程
浏览器内核
-
浏览器最核心的部分是渲染引擎(Rendering Engine),一般也称为浏览器内核。负责解析网页语法,并渲染网页
-
不同浏览器的内核
- Trident:IE、360安全浏览器、搜狗高速浏览器、百度浏览器、UC浏览器
- Gecko:Mozilla Firefox
- Presto -> Blink:Opera
- Webkit:Safari、360极速浏览器、搜狗高速浏览器、移动端浏览器(Android、iOS)
- Webkit -> Blink(Webkit 的一个分支):Google Chrome
渲染页面的流程
渲染页面更详细的 流程

HTML 解析过程
- 因为默认情况下服务器会给浏览器返回
index.html文件,所以解析 HTML 是所有步骤的开始 - 解析 HTML ,会构建 DOM Tree
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div>
<img src="./demo.jpg">
</div>
</body>
</html>
生成 CSS 规则
- 在解析的过程中,如果遇到 CSS 的
link元素,那么会由浏览器负责下载对应的 CSS 文件- 下载 CSS 文件是不会影响 DOM 的继续解析
- 浏览器下载完 CSS 文件后,就会对 CSS 文件进行解析,解析出对应的规则树(CSSOM:CSS 对象模型)
body { font-size: 16px; }
p { font-weight: bold; }
span { color: red; }
p span { display: none; }
img { float: right; }
构建 Render Tree
- 当有了 DOM Tree 和 CSSOM 后,就可以两个结合来构建 Render Tree
link元素不会阻塞 DOM Tree 的构建过程,但是会阻塞 Render Tree 的构建过程- Render Tree 在构建时,需要对应的 CSSOM
- Render Tree 和 DOM Tree 并不是一一对应的关系
- 比如对于
display: none的元素,不会出现在 Render Tree 中
- 比如对于
布局和绘制
- 布局(Layout):在渲染树上运行布局以计算每个节点的几何体
- 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
- 布局是确定呈现树中所有节点的大小和位置信息
- 绘制(Paint):将每个节点绘制到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个 frame 转为屏幕上实际的像素点
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如 img)
回流和重 绘
回流
- 回流(reflow,也可以称为重排)
- 第一次确定节点的大小和位置,称之为布局
- 之后对节点布局(大小、位置)修改重新计算称之为回流
- 引起回流的情况
- DOM 结构发生改变(添加新的节点或者移除节点)
- 改变了布局(修改了 width、height、padding、font-size 等值)
- 窗口 resize(修改了窗口的尺寸等)
- 回流是一项非常消耗性能的操作,要尽量避免发生回流
- 修改样式时尽量一次性修改,通过动态添加 class 修改
- 尽量避免频繁地操作 DOM
- 对某些元素使用绝对定位,降低回流的开销
重绘
- 重绘(repaint)
- 第一次渲染内容称为绘制
- 之后重新渲染称为重绘
- 引起重绘的情况
- 修改背景色、文字颜色、边框颜色、样式等
- 回流一定会引起重绘
Composite 合成
-
绘制的过程,可以将布局后的元素绘制到多个合成图层中,这是浏览器的一种优化手段
-
默认情况 下,标准流中的内容都会被绘制在同一个图层(Layer)中
-
一些特殊的属性,会创建一个新的合成层,并且新的图层可以利用 GPU 来加速绘制
- 每个合成层都是单独渲染的,一个合成层发生回流时,不会影响其他的合成层
-
可以形成新的合成层的 CSS 属性
- 3D transform
- video、canvas、iframe
- opacity 动画转换时
- position: fixed
- will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化
- animation 或 transition 设置了 opacity、transform
<style>
body {
background-color: skyblue;
}
.container {
width: 200px;
height: 200px;
background-color: orange;
position: fixed;
}
</style>
<body>
<div class="container"></div>
</body>
-
分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用
script 元素和页面解析
-
浏览器在解析 HTML 的过程中,遇到了 script 元素是不能继续构建 DOM 树的
- 它会停止继续构建,首先下载 JavaScript 代码,并且执行 JavaScript 的脚本
- 只有等到 JavaScript 脚本执行结束后,才会继续解析 HTML,构建 DOM 树
- 这是因为 JavaScript 可以操作并修改 DOM,如果等到 DOM 树构建完成并且渲染再执行 JavaScript,会造成严重的回流和重绘,影响页面的性能
- 所以会在遇到 script 元素时,优先下载和执行 JavaScript 代码,再继续构建 DOM 树
-
但是这样往往会带来新的问题,特别是在 SPA 页面中
- 下载处理 js 脚本的时间需要更长,所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
- 为了解决这个问题,script 元素提供了两个属性(attribute)— defer 和 async
-
defer属性告诉浏览器不要等待脚本下载,继续解析 HTML,构建 DOM Tree- 脚本会由浏览器来进行下载,但是不会阻塞 DOM Tree 的构建过程
- 如果脚本提前下载好了,它会等待 DOM Tree 构建完成,在
DOMContentLoaded事件之前先执行 defer 脚本中的代码 - 多个带 defer 的脚本是可以保证按正确的顺序执行
- 从某种角度来说,defer 可以提高页面的性能,并且推荐放到 head 元素中
- defer 仅适用于外部脚本,对于 script 标签中的默认内容会被忽略
<head>
<script src="./defer_demo.js"></script>
</head>
<body>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
});
</script>
</body> -
async属性能让一个脚本完全独立- 浏览器不会因 async 脚本而阻塞解析(与 defer 类似)
- async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
- async 脚本不能保证在
DOMContentLoaded之前或之后执行
-
区别
- defer 通常用于需要在文档解析后操作 DOM 的 JavaScript 代码,并且对多个 script 文件有顺序要求
- async 通常用于独立的脚本,对其他脚本,甚至 DOM 没有依赖
深入 JavaScript 运行原理
JavaScript 引擎
- JavaScript 引擎作用
- 高级编程语言都是需要转义成最终的机器指令来执行
- 编写的 JavaScript 无论交给浏览器或 Node.js 执行,最后都是需要被 CPU 执行的
- CPU只认识自己的指令集(机器语言),才能被CPU所执行
- 所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行;
- 常见 JavaScript 引擎
- SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(JavaScript 作者)
- Chakra:微软开发,用于 IE 浏览器
- JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发
- V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出
- 浏览器内核和 JavaScript 引擎
- 浏览器内核负责 HTML 解析、布局、渲染等
- JavaScript 引擎负责执行 JavaScript 代码
V8 引擎工作原理
V8 引擎执行 JavaScript
-
官方对于 V8 的定义
- V8 是用 C ++ 编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等
- V8 可以独立运行,也可以嵌入到任何 C ++ 应用程序中
- V8 引擎本身的源码非常复杂,大概有超过100w行 C++ 代码
-
V8 引擎执行 JavaScript 原理图
-
Parse 模块:将源代码经过词法分析、语法分析转化成 AST 树
- 词法分析:将源代码中的每一个词分析成一个 tokens 数组,数组中每个对象包括该词的具体类型和值
- 语法分析:将每个分析出来的词进行语法分析,生成 AST 树
- Parse 的 V8 官方文档: https://v8.dev/blog/scanner
-
Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码)
- 由于同一份源代码需要在不同的环境执行,对于不同架构的 CPU,所对应的机器指令不同,所以需要转换为跨平台的字节码
- 字节码在对应的平台上转换为对应的机器码,然后才能执行
- Ignition 的 V8 官方文档: https://v8.dev/blog/ignition-interpreter
-
TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码
- 如果一个函数被多次调用,会被标记为热点函数,那么就会经过 TurboFan 直接转换成优化的机器码,提高代码的执行性能
- 如果后续执行函数的过程中,函数执行操作发生了变化(比如 sum 函数原来执行的是 number 类型计算,后来执行变成了 string 类型拼接),之前优化的机器码并不能正确的处理运算,就会逆向还原为字节码
- 某种意义上来说,使用 TypeScript 编写编译而成的源代码 ,由于有类型限制,所以比直接使用 JavaScript 编写的源代码运行效率要高
- TurboFan 的 V8 官方文档: https://v8.dev/blog/turbofan-jit
function sum(n1, n2) {
return n1 + n2;
}
sum(1, 2);
sum('1', '2');
V8 引擎的解析 (Parse)
-
Blink 将源码通过 Stream流 将获取到源码传递给 V8引擎
-
Scanner 会进行词法分析(lexical analysis),词法分析会将代码转换成 tokens
-
接下来 tokens 会被转换成 AST 树,经过 Parser 和 PreParser
-
Parser 就是直接将tokens转成AST树架构
-
PreParser 称之为预解析,为什么需要预解析呢
- 并不是所有的 JavaScript 代码,在一开始时就会被执行。如果一开始就对所有的 JavaScript 代码进行解析,必然会 影响网页的运行效率
- V8 引擎实现了 Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行
- 比如在一个函数 outer 内部定义了一个函数 inner,那么 inner 函数就只会进行预解析,只有运行时才会开始真正解析
function outer() {
function inner() {}
}
outer();
-
-
生成 AST 树后,会被 Ignition 转成字节码(bytecode),之后开始执行代码
全局代码执行过程
console.log(message); // undefined
var message = 'code';
var n1 = 10;
var n2 = 20;
var res = n1 + n2;
console.log(res); // 30
初始化全局对象

- js 引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象可以被所有的作用 域访问,同时该对象会包含 Date、Array、setTimeout 等全局属性和方法
- 其中还有一个属性
this/globalThis(比如浏览器环境下的 window)指向自己
创建执行上下文
- js 引擎内部有一个执行上下文栈(Execution Context Stack),它是用于执行代码的调用栈,栈顶的执行上下文即正在执行的上下文
- 为了执行全局的代码块,会构建一个全局执行上下文(Global Execution Context),放入到 ECS 中执行
- 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 Global Object 中,但是并不会赋值
- 这个过程也称之为变量的作用域提升
- 在代码执行中,对变量赋值,或者执行其他的函数
- 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 Global Object 中,但是并不会赋值
创建 VO 对象
-
每一个执行上下文会关联一个 VO(Variable Object),变量和函数声明会被添加到这个 VO 对象中

-
当全局代码被 执行的时候,VO 即 GO 对象,同时全局中还有一个
this指向全局对象
执行全局代码
-
执行代码前所有的准备工作已做好,开始一行行执行代码
// 打印 message 变量,查找当前 VO 中的变量 message,取值为 undefined
console.log(message); // undefined
// 将 VO 中的 message 变量赋值为 'code'
var message = 'code';
// 将 VO 中的 n1 变量赋值为 10
var n1 = 10;
// 将 VO 中的 n2 变量赋值为 20
var n2 = 20;
// 将 VO 中的 res 变量赋值为 10 + 20 = 30
var res = n1 + n2;
// 打印 res,为 30
console.log(res); // 30 -
全局代码执行完后,对应的内存结构
函数执行过程
function foo(num) {
var count = 10;
var res = num + count;
console.log(res);
}
foo(20);
console.log('11');
-
初始化全局对象
-
创建全局执行上下文
-
创建 VO 对象(指向 GO),解析到
foo函数声明,创建函数对象并将内存地址(指针)赋值给 foo 变量
-
函数解析完成,执行
foo(),根据函数体创建一个函数执行上下文(Functional Execution Context)并且压入到执行栈中- 每个执行上下文都会关联一个 VO,当进入一个函数执行上下文时,会创建一个 AO 对象(Activation Object)
- 这个 AO 对象会使用 arguments 作为初始化,并且初始值是传入的参数
- 这个 AO 对象会作为执行上下文的 VO 来存放变量的初始化

-
开始一行行执行函数体代码,对变量进行赋值
-
函数体代码执行完成,函数执行上下文从执行栈中弹出销毁,函数的 AO 也被销毁,此时栈顶的执行上下文又变成了全局执行上下文,执行全局的下一行代码,打印 11
作用域和作用域链
-
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
- 作用域链在代码解析时就已经确定,与执行的位置无关

-
变量查找过程:先在自己的 VO 对象中查找,无法找到则沿着作用域链进行查找
var message = 'Global Message';
function foo() {
var num = 10;
function bar() {
console.log(message);
console.log(num);
}
return bar;
}
var bar = foo();
bar();-
初始化全局对象
-
创建全局执行上下文
-
创建全局执行上下文的 VO 对象(指向 GO),解析到
foo函数声明和变量声明,创建函数对象并将内存地址(指针)赋值给 foo 变量,同时创建message变量-
函数对象中除了包含函数的一些属性
-
还包含作用域链
[[scopes]],foo 函数的作用域链中只有一个全局对象 GO
-
-
函数解析完成,先对
message变量赋值,然后执行foo(),根据函数体创建一个函数执行上下文 -
创建
foo的 AO 对象,遇到bar函数定义,创建函数对象并将内存地址赋值给 bar 变量- 此时 bar 函数的作用域链 为
foo函数的 AO 和 全局对象 GO
- 此时 bar 函数的作用域链 为
-
foo()执行完毕,foo 的函数执行上下文从栈顶弹出,将定义的bar函数返回,由全局的 bar 变量进行接收
-
执行
bar(),创建bar函数执行上下文,压入执行栈顶,创建 bar 函数的 AO 对象
-
开始执行
bar函数体代码
- 首先打印
message变量,先在自己的 AO 中查找,没有找到;在作用域链中查找,先查找foo函数的 AO 对象,也没有查找到;继续往上查找,在全局对象 GO 中查找到,打印 “Global Message” - 打印
num变量,先在自己的 AO 中查找,没有找到;在作用域链中查找,先查找foo函数的 AO 对象,查找到打印 10
console.log(message); // 'Global Message'
console.log(num); // 10 - 首先打印
-
函数参数的作用域
If the function's formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations.
如果函数参数不存在默认值,那么函数体内部的声明和函数参数的声明会存放到同一个环境记录(作用域)内。如果参数存在默认值,那么函数参数部分会创建第二个环境记录
const x = 10;
function foo(x, y = x++) { // 此次x++操作访问的x是函数参数中声明的x
console.log(x); // 2
}
foo(1);
console.log(x); // 10
词法环境和环境记录
- 前面提到的 VO、AO、GO 等都是基于早期( ES5 之前) ECMAScript 的版本规范
- 在新的 ECMA 代码执行描述中,对于代码的执行流程描述修改了一些词汇