前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]
准备
[[Call]] 和函数
- Function也是Object, 协议内部是怎么区分的呢? 很简单, 如果Object内部方法
[[Call]]
, 那么就是函数。 协议内部对应有个方法叫做 IsCallable,通常用来识别是不是函数。

ordinary object 和 exotic object
Exotic Object 查阅了一下,有翻译成各种中文的,比如:特殊对象,异质对象,外来对象,特异对象,奇异对象的。
本章节就统一翻译为 特异对象了,明白这类对象比与协议内部的 ordinary object (普通对象) 有些不同就行。
一个( ordinary object)普通对象满足以下标准:
对应有特定的内部方法,比如 [[GetPrototypeOf]]``[[GetOwnProperty]]
等
详情: 对于 Table 4: Essential Internal Methods 中列出的内部方法,该对象使用在10.1 Ordinary Object Internal Methods and Internal Slots节中定义的方法。
- 如果该对象具有
[[Call]]
内部方法,则它使用在10.2.1节或10.3.1节中定义的方法。 - 如果该对象具有
[[Construct]]
内部方法,则它使用在10.2.2节或10.3.2节中定义的方法。
如下的都是普通对象:
javascript
var a = {};
function fun(){}
协议里面有大把的特异对象,也不要大惊小怪的,比如 Array, Proxy, Set等等都是。
Bound Function Exotic Objects 绑定函数特异对象
绑定函数特异对象是包装另一个函数对象的特异对象。
绑定函数特异对象是可调用的(它有一个[[ Call ]]内部方法,并且可能有一个[[ Construction ]]内部方法)。
调用绑定函数 特异对象通常会导致调用其包装函数。
对应协议,程序上有什么体现呢? 直接看代码, 下面的warnLog
就是 Bound Function Exotic Object。
javascript
function log(type,message){
console.log(`${type}:`, message);
}
const warnLog = log.bind("warn");
其主要就是, 或者目前来说,就是为了描述 Function.prototyp.bind
的行为。 如果客官您还不知道bind
的作用,暂时 10 分钟,去 goolgle
一下,再回来 (^_^)。
Function.prototyp.bind
就是预设函数在执行时的 this 和 参数。
看个题
来个题试试水
javascript
function Animal(sex, age) {
this.sex = sex;
this.age = age;
return this;
}
const Person = Animal.bind("人", "男");
const person = Person(18);
const person2 = new Person(22);
console.log("person: ", person);
console.log("person2: ", person2);
Object.prototype.toString.call(person);
Object.prototype.toString.call(person2);
运行结果如下:
javascript
console.log("person: ", person); // person: String {'人', sex: '男', age: 18}
console.log("person2: ", person2); // person2: Animal {sex: '男', age: 22}
Object.prototype.toString.call(person); // '[object String]'
Object.prototype.toString.call(person2); // '[object Object]'
Person(18)
Person(18)
走的是函数调用(非new), 示例中 Person 是 Bound Function Exotic Object, 其也有自身的 [[Call]] 方法,是可被调用的,

但最终的本质是是执行被包裹函数的 [Call] 方法。
除此以外, Bound Function Exotic Object 还有其他属性

-
\[BoundTargetFunction\]\]: 原函数
-
\[BoundArguments\]\]:预置的其他参数
-
取this
-
取预置参数
-
拼接参数
-
底层调用Function 自身的
[[Call]]

Bound Function Exotic Object被调用的代码路径如下
[[Call]] ( thisArgument, argumentsList ) Bound Function Exotic Object的call
=> [[Call]] ( thisArgument, argumentsList ) Function本身的call
=> OrdinaryCallBindThis ( F, calleeContext, thisArgument ) 绑定this
=> OrdinaryCallEvaluateBody ( F, argumentsList ) 执行脚本
重点看一下 绑定this的逻辑的OrdinaryCallBindThis方法:

-
示例代码不属于严格模式,不会走标记1的代码,严格模式下this是什么就是什么,不会进行包装。
-
Person(22)
调用时, 因为此时的 this 是字符串"人"
, 被转为了对象ToObject
。
所以函数执行时,this 是 String对象,返回的 person 自然也是 String Object 对象, 所以 Object.prototype.toString.call(person)
的返回值是 '[object String]'
。
如果换成严格模式,答案又如何呢?
new Person(22)
new Person(22) 走的 new operater 的逻辑。 new 的执行逻辑,其实也很简单, 本质就是调用被包裹函数 内部方法[[Construct]] 。
Bound Function Exotic Object, 其也有自身的 [[Construct]], 执行逻辑也基本就是取参数,调用被包裹 Function 的 [[Construct]]。 细心的同学会发现 newTarget, 这玩意与Proxy 有关。 暂且不表。

重点转义到Function自身的 [[Construct]]
, 关注的步骤已添加备注。

[[ConstructorKind]]
Internal Slots of ECMAScript Function Objects ECMAScript 函数对象的内部插槽 可以看到这个。 base 表示是普通函数, derived 表示是 派生类的构造函数函数。 本示例当然是base。

- OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%" ) 会创建一个对象然后赋值给 thisArgument。 第二个参数其实是默认值,本例子其实用不到。 其底层逻辑是:
- GetPrototypeFromConstructor 获取构造函数的原型
- OrdinaryObjectCreate 创建一个对象,并把原型属性设置为上一步的原型
所以this的原型是啥, 是 原函数(即被包裹函数) 的原型。 注意红色部分,是原函数的原型。
例最后显式返回了 this, 所以如下代码结果是 true。
javascript
Object.getPrototypeOf(person2) === Animal.prototype // true。
其实也好理解,Bound Function Exotic Object 不管是被 调用([[Call]])
和被new([[Construct]]
) , 其实本质都是被其包裹的函数进项调用的。
所以(暂不讨论Class构造函数)
-
person2
最终是 Animal的一个实例。 -
Bound Function Exotic Object 没有
prototype
。
或者说 Bound Function Exotic Object 仅仅是一个特殊的包裹对象,表现的像函数而已。javascriptAnimal.prototype // {constructor: ƒ} Person.prototype // undefined
-
Bound Function Exotic Object 其预置的参数,在new的时候,依旧是生效的。
本例 预设的sex ,实例化后,完美生效。javascript// 省略代码.... const Person = Animal.bind("人", "男"); // 省略代码.... console.log("person2: ", person2); // person2: Animal {sex: '男', age: 22} // 省略代码....
-
new 操作的如果你返回的是一个有效的对象,将不会返回 this。( 这不是本文的重点)
bind.bind是怎么运行的
如果Function.prototype.bind
多次,又是一个什么执行逻辑呢?
javascript
var obj1 = {
name: "obj1"
};
var obj2 = {
name: "obj2"
};
function log(num1, num2) {
console.log(`${this.name}: ${num1 + num2}`);
}
var log1 = log.bind(obj1, 10);
var log2 = log1.bind(obj2, 20);
log2();
两个特异对象 log1 和 log2的结构大致如下:

执行逻辑代码:
javascript
// log2是绑定函数特异对象,等同于获取取this(obj2),
// 获取参数+合并参数 {0:20, length:1},调用log1
log2()
// log1是绑定函数特异对象,继续等同于 获取this(obj1),
// 取参数+合并参数 {0:10, 1:20, length:2},调用log
log1.[[Call]](obj2, 10)
// log为普通函数,log.[[Call]] 可以等价为 log.call
log.[[Call]](obj1, 10, 20) /
log.call(obj, 10, 20) // obj1: 30
小结
- bind多次之后调用, 预设参数都会一层一层拼接回去, 先
bind
的参数在前面。 因为拼接逻辑是boundArgs and argumentsList
- bind多次之后调用,this是第一次绑定的值。
Immutable Prototype Exotic Objects 不可变原型特异对象
不可变的原型特异对象是一个具有[[ Prototype ]]
内部槽的异常对象,该槽在初始化后不会更改。
简单说,原型初始化后不可更改。
见过这种的对象吗, 应该说每时每刻都在接触。比如经典的 Object.prototype
javascriptjavascript
Object.setPrototypeOf(Object.prototype, {});
// Uncaught TypeError: Immutable prototype object '#<Object>' cannot have their prototype set
当然,如果你设置的值和原有的值一样,是不会抛出异常。
javascript
Object.setPrototypeOf(Object.prototype, null);
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, ...}
浏览器环境也存在这样的对象, 比如Window.protype
, Location.prototype
javascript
Object.setPrototypeOf(Window.prototype, {});
// Uncaught TypeError: Immutable prototype object '#<Window>' cannot have their prototype set
Object.setPrototypeOf(Location.prototype, {});
// Uncaught TypeError: Immutable prototype object '#<Location>' cannot have their prototype set
这个时候有些人可能会想,是不是frozen,sealed等
呢? 自行体会。
javascript
const log = console.log;
log('sealed:',Object.isSealed(Object.prototype)); // sealed: false
log('frozen:',Object.isFrozen(Object.prototype)); // frozen: false
log('extensible:',Object.isExtensible(Object.prototype)); // extensible: true
实现类似的功能
如果程序上想实现类似的功能,应该怎么做呢? 抛砖引玉,来段代码,大家一起思考。
javascript
function Animal(sex, age) {
this.sex = sex;
this.age = age;
}
// 冻结原型
const AnimalFun = frozenPrototype(Animal);
function frozenPrototype(funOrClass) {
const prototype = Object.getPrototypeOf(funOrClass);
if (prototype === undefined) {
return;
}
// Proxy 拦截 setPrototypeOf 方法
return new Proxy(funOrClass, {
setPrototypeOf(_target, val) {
// 如果一致,
if (val === prototype) {
return prototype;
}
// 报错
throw new TypeError(`Immutable prototype object '#<${funOrClass.name}>' cannot have their prototype set`)
}
})
}
Object.setPrototypeOf(AnimalFun, Object.getPrototypeOf(Animal)); // 不会报错
Object.setPrototypeOf(AnimalFun, {}); // TypeError: Immutable prototype object '#<Animal>' cannot have their prototype set
Arguments Exotic Objects
这玩意就是函数里面常用 arguments
对象,也是常说的类数组对象。 其除了能索引访问和length
属性看起来 像个数组外,并没有数组相关的属性。
主要就说两个东西
- 跟踪参数变化
- Leaking arguments
跟踪参数变化
arguments 这个特异对象和函数的参数是存在一定的关联的,严格模式和非严格模式下,又存在一些区别? ES6 引入的剩余参数,默认参数,解构赋值也带来一些新的变化。
非严格模式
下面的例子,有默认参数的时候 arguments[0]
更新了,但是 a
并没有更新。
javascript
// 有默认参数,a不更新
function func(a = 55) {
a = 99;
console.log(arguments[0], a);
}
func(10); // 10 99
// 无默认参数,a同步更新
function func2(a) {
a = 99;
console.log(arguments[0], a);
}
func2(10); // 99 99
反之如果 a 更新, arguments[0] 会更新吗? 穷举
javascript
// 无默认值, 更新 arguments[0]
function func(a) {
arguments[0] = 99; // 更新 arguments[0], a也更新
console.log(a, arguments[0]);
}
func(10); // 99 99
// 默认值, 更新 arguments[0]
function func2(a = 55) {
arguments[0] = 99; // 更新 arguments[0], a不更新
console.log(a, arguments[0]);
}
func2(10); // 10 99
// 无默认值, 更新a
function func3(a) {
a = 99;
console.log(a, arguments[0]); // a更新, arguments[0]更新
}
func3(10); // 99 99
// 默认值, 更新a
function func4(a = 55) {
a = 99;
console.log(a, arguments[0]); // a更新, arguments[0] 不更新
}
func4(10); // 99 10
严格模式
在严格模式下,剩余参数、默认参数和解构赋值参数的存在不会改变 arguments 对象的行为。 这话怎么理解,因为严格模式,压根 arguments 和 具名参数根本不会进行联动, arguments 的更改不会同步参数的值的更改,反义亦然。
javascript
"use strict";
// 无默认值, 更新 arguments[0]
function func(a) {
arguments[0] = 99; // 更新 arguments[0], a不更新
console.log(a, arguments[0]);
}
func(10); // 10 99
// 默认值, 更新 arguments[0]
function func2(a = 55) {
arguments[0] = 99; // 更新 arguments[0], a不更新
console.log(a, arguments[0]);
}
func2(10); // 10 99
// 无默认值, 更新a
function func3(a) {
a = 99;
console.log(a, arguments[0]); // a更新, arguments[0] 不更新
}
func3(10); // 99 10
// 默认值, 更新a
function func4(a = 55) {
a = 99;
console.log(a, arguments[0]); // a更新, arguments[0] 不更新
}
func4(10); // 99 10
小结
思考完毕了,小结
- 严格模式下,
arguments
不会跟踪参数的值 - 非严格模式,
- 如果没有剩余参数、默认参数和解构赋值的,
arguments
不会跟踪参数的值 - 否则,
arguments
会跟踪参数的值
- 如果没有剩余参数、默认参数和解构赋值的,
所以呢? 非严格模式 + 没有剩余参数、默认参数和解构赋值, 函数参数和arguments 会进行联动。
Leaking arguments
先看一段代码,都是对arguments的复制,想必很多同学都用过第一种。 如果是第一种,你就 Leaking arguments 了。不是说你泄漏了arguments 参数,而是说你这么处理,v8引擎没法优化你的代码。
著名 Promise 库 bluebird 在 Optimization killers(最后更新日期2018.5.31) 文章的 3. Managing arguments 提到了这个概念。
如下两个版本复制 转 arguments 为 数组, 第一种方式就是文章所谓的 Leaking arguments。
javascript
function leaksArguments() {
var args = [].slice.call(arguments);
return args;
}
function notLeakingArguments() {
var i = arguments.length;
var args = [];
while (i--) args[i] = arguments[i];
return args
}
细节不说, 简单测试一下(本人台式机电脑测试):
javascript
function costTest(method, times = 100 * 10000) {
const startTime = Date.now();
for (let i = 0; i < times; i++) {
method(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
const endTime = Date.now();
console.log('cost:', endTime - startTime);
}
node 7.7.0
leaksArguments 方法 基本是大于200 毫秒, 浮动相对较大
javascript
console.log(`process:version`, process.version);
costTest(leaksArguments)
// process:version v7.7.0
// cost: 204
notLeakingArguments 方法大致是 80 毫秒多点,很稳定
javascript
console.log(`process:version`, process.version);
costTest(notLeakingArguments)
// process:version v7.7.0
// cost: 82
还是表格比较直观
node版本 | 方法 | 时间消耗 |
---|---|---|
7.7.0 | leaksArguments | 200 |
7.7.0 | notLeakingArguments | 80 |
12.14.1 32位 | leaksArguments | 54 ~ 56 |
12.14.1 32位 | notLeakingArguments | 65 ~ 70 |
14.18.0 | leaksArguments | 70 |
14.18.0 | notLeakingArguments | 72 ~ 80 |
16.10.0 | leaksArguments | 70 ~ 75 |
16.10.0 | notLeakingArguments | 80+ 浮动较大 |
18.15.0 | leaksArguments | 70 ~ 75 |
18.15.0 | notLeakingArguments | 85+ |
这种测试不具备什么权威,但是也能从一定程度反映指向效率。 可以简单的看出,node的高级版本,slice
的执行效率是高于最原始的复制模式的。 但是,差距非常的小于。说明引擎也在进步。
所以呢?
- 如果你面临的是低版本的chrome浏览器或者node, 那么你可以注意一下,毕竟效率还是低了倍数关系,
- 如果是高版本,一笑而过就好。
其他特异对象
数组特异对象,这玩意就是常见的Array。
字符串特异对象。就是常用的字符串。
整数索引的特异对象与普通对象具有相同的内部插槽,另外还有[[ ViewedArrayBuffer ] ,[ ArrayLlength ] ,[ ByteOffset ] ,[ ContentType ]和[ TypedArrayName ]内部插槽。
TypedArray Objects 系列就是这类对象, 比如 Int8Array,Uint8Array,Float32Array等等
模块命名空间特异对象是一个公开了从ECMAScript Module 导出的绑定的特质对象。export
熟悉吧,唉, 对味了。
其他还有其他的特异对象,比如代理(Proxy)等。
引用
Built-in Exotic Object Internal Methods and Slots
Immutable Prototype Exotic Objects
Immutable Prototype Exotic Objects
Arguments Exotic Objects
Does node.js really not optimize calls to [].slice.call(arguments)?
Changing JavaScript function's parameter value using arguments array not working