为甚么说js中一切皆对象
本文是通过规范的角度去讲解语言内部的执行逻辑,有必要为大家介绍一些前置知识,同时也考虑到规范中有一套自己的描述"体系",限于篇幅所以我们只介绍必要的知识:在本节中我们需要了解的是内部槽(Internal Slot:内部用来存储数据属性)和内部方法(Internal Method:内部用来存储方法属性),它和我们的对象是极其相似的
type | mean |
---|---|
Internal Slot | 表示程序的内部状态,它不会被继承,如[[StringData]]、[[ParameterMap]] |
Internal Method | 内部方法是多态的,对于不同对象会调用不同的算法即使它们的拥有相同的内部方法名,如[[GetPrototypeOf]]、[[Get]]、[[Set]]… |
没错就是这么简单,你已经掌握了阅读ECMAScript标准的能力,下面让我们开始正式的内容吧
当我们刚刚接触javascript这门语言就听过这样一句话:javascript中一切皆对象。这就像"PHP是最好的语言一样",咳咳,请原谅,我是一个phper。这显然和javascript基本数据类型的定义是矛盾的。所以在这里我给出了一个更形象的说法:javascript中一切类型皆具有对象行为模式,当我们需要执行某种特定的行为,就会对基础类型进行临时的包装以具备这些能力(Capability),当然这都得益于那个饱受争议的"隐式转换"。
这里需要注意的是:undefined和null是不需要包装的所以没有对应的包装函数,undefined代表没有定义自然也就无法包装,而为null引入包装能力是绝对危险的,试想一下:如果作为对象原型链顶点的null还有原型,会发生什么?事情会就此结束吗,不会,绝对不会,将undefined和null排除在外还怎么会说一切皆对象呢,让我们从了解类型判定开始(这里并不打算从null值的底层表示去讨论)。
值类型的两种形态:装箱和未装箱
由于typeof的局限性这里就不做介绍了,下面来看一下类型判断的一般方式
- instanceof:
console.log([] instanceof Array); // true
console.log('123' instanceof String); // false
console.log(new String(123) instanceof String); // true
instanceof在判断’123’是否是String的实例时候会失败,究其原因是因为并没有进行包装,这里’123’只是作为一个操作数被调用,真正发生操作行为的是instanceof而不是’123’自身,为了让大家更容易理解,我将上面的写法转换为了下面的形式
//这里的'123'只是一个参数,规范中并没有要求对它进行包装,自然也就无法判定'123'是Sting构造函数的实例。
Array[Symbol.hasInstance]('123') //false
- Object.prototype.toString()
//注意,一般情况下tag是通过内部槽确定的,比如:[[Call]] -> Function,[[StringData]] -> String,而非通过Symbol.toStringTag
Object.prototype.toString.call() //'[object Undefined]'
Object.prototype.toString.call('123') //'[object String]'
Object.prototype.toString.call([1, 2, 3]) //'[object Array]'
let arr = [1, 2, 3]
let proxyArr=new Proxy(arr, {})
Object.prototype.toString.call(arr) //'[object Array]'
可以看到Object.prototype.toString方法符合了我们对一切皆对象的认知,底层究竟做了哪些让我们可以操作一个没有包装对象的原始值呢?来让我们看下规范中的处理方式吧:
//20.1.3.6 Object.prototype.toString() 1. If the this value is undefined, return "[object Undefined]". 2. If the this value is null, return "[object Null]". 3. Let O be ! ToObject(this value).
很简单粗暴是不是?如果没有1,2的操作,那么必定会在执行ToObject(undefined)抛出错误,也就是说js底层在最低限度的保证各种原始类型数据拥有对象的操作能力。看到这里可能会有疑惑,明明可以通过new Object([null | undefined])去创建一个对象,为什么在规范中的操作会强制返回类型,又为什么说会报错呢。现在就让我们解开new Object(null)的神秘面纱吧:
let nullObj = new Object(null)
nullObj.__proto__==Object.prototype //true
es6Obj = Object.create(null)
es6Obj.__ //undefined
es6Obj.__proto__==Object.prototype //false
观察得知通过new Object(null)创建的并不是一个纯净的对象,当Object的参数是null时,底层会帮我们将%Object.prototype%作为原型创建一个新的对象(注意:这仅在被操作值无法获取构造器的情况下出现),从语法层面早期javascript允许基本类型或多或少拥有对象的行为方式,但在ES6之后javascript有意区分类型,减少没有必要的类型转换以减少因类型转换导致的怪异行为,如Object.create修正了这个问题得以创建出一个以null为原型的对象,并且在处理上也不再“包容”undefined,而是抛出了错误:
Object.create(undefined) //Object prototype may only be an Object or null:
成员表达式的行为
下面我们以字符串为例细剖析属性的读取和设置来看一下为了同化这些差异底层究竟为我们做了哪些。字符串是一个基本类型,它将字符串值封装在[[StringData]]内部插槽,并公开字符串值的各个代码单元的虚拟整数索引属性,和一个值为代码单元个数的length属性。代码单元数据属性和“length”属性都是不可写和不可配置的。它的所有操作并不会修改字符串本身,那么它是如何被赋予这些操作的能力?不能修改值的原因又是什么?先让我们从简单的属性获取来看一下字符串是如何被赋予对象的能力。
let str = '123'
console.log(str[1]) //2
这是我们最常见的写法,我们把形如MemberExpression [ Expression ]称为属性访问器(Property Accessors),另一种形式是MemberExpression . IdentifierName,他们所表达的意图是一样的,唯一的区别是前者会对Expression求值为一个StringValue,下面是规范中对属性访问器求值(Evaluation)的步骤截取
//13.3.2.1 Runtime Semantics: Evaluation MemberExpression : MemberExpression [ Expression ] 1. Let baseReference be ? Evaluation of MemberExpression. 2. Let baseValue be ? GetValue(baseReference). 3. If the source text matched by this MemberExpression is strict mode code, let strict be true; else let strict be false. 4. Return ? EvaluatePropertyAccessWithExpressionKey(baseValue, Expression, strict).
例子中的str对应于MemberExpression,首先将baseReference设置为str,并对baseReference进行GetValue操作,下面我们追踪到GetValue抽象操作步骤截取:
//6.2.5.5 GetValue ( V ) 1. If V is not a Reference Record, return V. 2. If IsUnresolvableReference(V) is true, throw a ReferenceError exception. 3. If IsPropertyReference(V) is true, then a. Let baseObj be ? ToObject(V.[[Base]]). b. If IsPrivateReference(V) is true, then i. Return ? PrivateGet(baseObj, V.[[ReferencedName]]). c. Return ? baseObj.[[Get]](V.[[ReferencedName]], GetThisValue(V)). //略...
因为baseReference是一个属性引用,所以在步骤3.a中会将base转换为对象形式,就是我们常说的隐式转换(确切的说是包装转换),此时它就获得了Object类型的能力,最后通过普通对象(Ordinary Objects)的[[get]]内部方法获取值,让我们在看一下赋值操作吧:
//注意这里为了看到更明显的区别我们需要在严格模式下执行
"use strict";
let str = '123';
str[2] = 10;
LeftHandSideExpression = AssignmentExpression执行以下操作
//13.15.2 Runtime Semantics: Evaluation AssignmentExpression : LeftHandSideExpression = AssignmentExpression 1. If LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral, then a. Let lref be ? Evaluation of LeftHandSideExpression. b. If IsAnonymousFunctionDefinition(AssignmentExpression) and IsIdentifierRef of LeftHandSideExpression are both true, then i. Let rval be ? NamedEvaluation of AssignmentExpression with argument lref.[[ReferencedName]]. c. Else, i. Let rref be ? Evaluation of AssignmentExpression. ii. Let rval be ? GetValue(rref). d. Perform ? PutValue(lref, rval). e. Return rval. 省略...
由于我们不是匿名函数的赋值,所以执行1.c,也就是把右侧的值赋值给left-hand表达式,这里我们只要重点关注d和e两个执行步骤了,看到这里大家就能猜到了,非严格模式下的赋值会把右侧的值返回,所以上面的代码我们不得不在严格模式下执行,才能让PutValue真正发挥作用(1.d中产生一个错误):
//6.2.5.6 PutValue ( V, W ) //省略... 1. If IsPropertyReference(V) is true, then a. Let baseObj be ? ToObject(V.[[Base]]). b. If IsPrivateReference(V) is true, then i. Return ? PrivateSet(baseObj, V.[[ReferencedName]], W). c. Let succeeded be ? baseObj.[[Set]](V.[[ReferencedName]], W, GetThisValue(V)). d. If succeeded is false and V.[[Strict]] is true, throw a TypeError exception. e. Return unused. //省略...
[[set]]方法中涉及到的步骤如下:
//10.1.9 [[Set]] ( P, V, Receiver ) //省略... 2. If IsDataDescriptor(ownDesc) is true, then a. If ownDesc.[[Writable]] is false, return false. //省略...
在3.d中由于赋值失败在严格模式中所以抛出了一个错误,而在非严格模式下执行[[set]]方法只会静默失败而已。值得一提的是,对不可扩展对象在设置原型将会得到类似的行为,但是却有根本的不同,没有报错是由于同值检测优先于可扩展检测导致成功了,下面代码展示了这一现象
"use strict";
let man = {name:'zhangsan'},
people = {say:'something'}
man.__proto__ = people
Object.preventExtensions(man)
man.__proto__ = people
总结
众所周知javascript是一门弱类型语言,在设计之初更是弱化了类型间的行为差异并沿用至今,这种弱化在带来语法层面的方便的同时也埋下了很多隐患,让我们看到曙光的是ES6所带来的重大变革。在本节我们通过对基本类型包装的过程了解到在js中一切皆对象这一表象,在对字符串的操作中了解到了包装这一过程发生的时机。在下一节中,我们会深入探讨对象的行为模式。
最后在这里我将隐式转换归为两类:
-
底层创建临时对象以提供统一的上层接口随即销毁,合理的使用会成为开发的利器!
-
编写时的不规范使用导致的类型转换,这种转换往往是无传递性的且无规律的,这在javascript语言精粹一书中也多有吐槽
专栏全部内容请查看深入理解javascript