ECMAScript 中的特异对象

前言

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节中定义的方法。

  1. 如果该对象具有[[Call]]内部方法,则它使用在10.2.1节或10.3.1节中定义的方法。
  2. 如果该对象具有[[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. 示例代码不属于严格模式,不会走标记1的代码,严格模式下this是什么就是什么,不会进行包装。

  2. 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]], 关注的步骤已添加备注。

  1. [[ConstructorKind]]Internal Slots of ECMAScript Function Objects ECMAScript 函数对象的内部插槽 可以看到这个。 base 表示是普通函数, derived 表示是 派生类的构造函数函数。 本示例当然是base。
  1. OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%" ) 会创建一个对象然后赋值给 thisArgument。 第二个参数其实是默认值,本例子其实用不到。 其底层逻辑是:

所以this的原型是啥, 是 原函数(即被包裹函数) 的原型。 注意红色部分,是原函数的原型。

例最后显式返回了 this, 所以如下代码结果是 true。

javascript 复制代码
Object.getPrototypeOf(person2) === Animal.prototype   // true。

其实也好理解,Bound Function Exotic Object 不管是被 调用([[Call]])和被new([[Construct]]) , 其实本质都是被其包裹的函数进项调用的。

所以(暂不讨论Class构造函数)

  1. person2最终是 Animal的一个实例。

  2. Bound Function Exotic Object 没有prototype
    或者说 Bound Function Exotic Object 仅仅是一个特殊的包裹对象,表现的像函数而已。

    javascript 复制代码
    Animal.prototype  // {constructor: ƒ}
    Person.prototype  // undefined
  3. Bound Function Exotic Object 其预置的参数,在new的时候,依旧是生效的。
    本例 预设的sex ,实例化后,完美生效。

    javascript 复制代码
    // 省略代码....
    const Person = Animal.bind("人", "男");
    // 省略代码....
    console.log("person2: ", person2);  // person2:  Animal {sex: '男', age: 22}
    // 省略代码....
  4. 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

小结

  1. bind多次之后调用, 预设参数都会一层一层拼接回去, 先bind的参数在前面。 因为拼接逻辑是 boundArgs and argumentsList
  2. 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 引入的剩余参数,默认参数,解构赋值也带来一些新的变化。

非严格模式

  • aguments的值是会和具名参数进行联动变化的,
  • 剩余参数默认参数解构赋值参数的存在是会改变 arguments的行为的,即不会进行联动变化

下面的例子,有默认参数的时候 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

小结

思考完毕了,小结

  1. 严格模式下, arguments不会跟踪参数的值
  2. 非严格模式,
    1. 如果没有剩余参数、默认参数和解构赋值的, arguments不会跟踪参数的值
    2. 否则, arguments会跟踪参数的值

所以呢? 非严格模式 + 没有剩余参数、默认参数和解构赋值, 函数参数和arguments 会进行联动。

Leaking arguments

先看一段代码,都是对arguments的复制,想必很多同学都用过第一种。 如果是第一种,你就 Leaking arguments 了。不是说你泄漏了arguments 参数,而是说你这么处理,v8引擎没法优化你的代码。

著名 Promise 库 bluebirdOptimization 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

Object.setPrototypeOf()

Arguments Exotic Objects

The arguments object

Managing arguments

Does node.js really not optimize calls to [].slice.call(arguments)?

Changing JavaScript function's parameter value using arguments array not working

JavaScript: arguments leak var array.slice.call?

相关推荐
layman0528几秒前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝1 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML2 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia3112 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生18 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇32 分钟前
一文搞定CSS Grid布局
前端
0xHashlet38 分钟前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝39 分钟前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大41 分钟前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂41 分钟前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端