Libx

我不知道的JavaScript:原型

字数统计: 2,679阅读时长: 10 min
2018/07/16 Share

当我们在谈论原型时,我们在谈论什么?

从对象说起


有一句话是这样说的 JS 中万物皆对象,这句话虽然不完全正确但是有些道理,毕竟 JS 中还是有很多特殊的复杂子类型比如函数,虽然函数也是可调用的对象,不过毕竟typeOf null结果都是 object [滑稽](因为底层对象都表示为二进制,在 JS 中二进制前三位都为 0 的话会被判定为 object 类型,null 的二进制表示为全零自然会是 object)

但是还是要好好梳理下对象相关的一些东西

扯些关系不大的

数组和对象的关系源远流长,以至于要区分数组和对象都要专门使用Object.prototype.toString.call([])或者Array.isArray()来判断。对象可以像数组一样使用[]来访问值,数组也可以使用.来添加属性,只是不管使用点操作符还是[]操作符来为数组添加命名属性,数组的 length 属性都不会发生变化

let a =[]
a.test = "hello"
console.log(a)
//[test: "hello"] test: "hello" length: 0

对象属性描述符

在 ES5 之前并没有提供可以直接检测属性特性的方法,但是从 ES5 开始,属性具备了标识符:可以通过Object.getOwnPropertyDescriptor({})获取:
比如:

let test = {}
Object.defineProperty(test,"a",{
value: 1, writable: true, enumerable: true, configurable: true
})
Object.getOwnPropertyDescriptor(test,"a")
{value: 1, writable: true, enumerable: true, configurable: true}

顾名思义,writeble,enumrable,configurable。0

getter 和 setter

在 ES5 中可以部分改写默认操作,虽然只能应用到单个属性上,当给一个属性定义 getter 和 setter 或者两者都有的时候,这个属性会被定义成“访问描述符(与数据描述符相对)”对于访问描述符来说,JS 会忽略他们的 value 和 writeable 属性,而只关心 set 和 get、configurable、enumerable 特性

let obj = {
get a(){
return 1
}
}
Object.defineProperty(obj,"b",{
get:function(){return this.a*2}
enumerable:true
})
obj.a //1
obj.b //2

遍历

当一个属性值为 undefined 的时候,要如何来判断是否存在?方法很多也很简单,xx in obj,Object.getOwnProperty("xx")区别在于 in 操作符会到原型链上检查,而后者不会。之后我们会详细区分。
Object.getOwnPropertyNames({})或者 Object.keys()返回对象的所有属性名组成的数组,这可以用来便利对象,或者使用 for in

数组中包含着内置的@@iterator因此我们 for of 可以直接应用到数组上,其实这样工作的:

var myArray = [1,2,3]
var it = myArray[Symbol.iterator]() //使用Symbol.iterator获取对象内部的@@iterator属性。
it.next() //{value: 1, done: false}
it.next() //{value: 2, done: false}
it.next() //{value: 3, done: false}
it.next() //{value: undefined, done: true}

而普通的对象没有内置的@@iterator,但我们可以手写:

var obj ={a:1,b:2}
Object.defineProperty(obj,Symbol.iterator,{
enumerable:false,
writeable:false,
configurable:true,
value:function(){
var o = this
var idx = 0
var ks = Object.keys(o)
return{
next:function (){
return {
value:o[ks[idx++]],
done:(idx>ks.length)
}
}
}
}
})
//手动遍历
var it = obj[Symbol.iterator]();
it.next()
//for of 遍历:
for (let el of obj){
console.log(el)
}

由对象到原型


Javascript 中的对象都会有一个特殊的[[Prototype]]内置属性,就是对于其他属性的引用,几乎所有的对象在创建[[Prototype]]属性的时候都会赋一个非空的值,但很快我们就会看到对象的[[Prototype]]属性可以为空,虽然少见。

[[Prototype]]属性有什么用呢?我们之前提到过,当试图引用对象的属性的时候会触发[[get]]操作,第一步是检查对象本身是否有这个属性,如果没有就继续往原型链上寻找,整个过程会持续找到匹配的属性名,或者查找完整条原型链,如果是后者,最终会返回 undefined。

那么哪里是原型链的尽头呢?所有普通的原型链最终都会指向内置的 Object.prototype /由于所有的内置对象都源于这个 Object.prototype,所以这个对象会包含很多通用的功能,比如最常用的.toString,.valueOf

属性设置与屏蔽

给一个对象设置一个属性并非只是添加一个新属性或者修改一个属性而已,接下来我们来解释下这个过程:

obj.foo = "bar"

如果 obj 包含名为 foo 的普通数据访问属性,这条语句可以修改已有的属性值

如果 foo 并不包含在 obj 中那么就向原型链上寻找,如果找不到,foo 将添加到 obj

然而如果 foo 存在于原型链上层,那么赋值语句的行为会有些怪异:

  • 如果在原型链上存在 foo 且writeable:true 那么将会在 obj 添加 foo,并且是屏蔽属性。
  • 如果在原型链上存在 foo 且writeable:false 那么不能在 obj 添加 foo,且原型链上的 foo 不会被改写,语句会被忽略,严格模式下会报错。
  • 如果在原型链上存在 foo 且是一个 setter 那么将会调用这个 setter
    我们可能认为,只要向原型链上已存在的属性赋值就一定会触发屏蔽,但是在这以上三种情况中只有一种会发生屏蔽,2,3 想要触发屏蔽的话可以使用 Object.defineProperty()
    如果原型链上也存在,obj 也存在,那么会发生屏蔽,底层的 foo 将屏蔽链上的 foo

有时候会发生隐式屏蔽:

var obj1 = {a:1}
var obj2 = Object.create(obj1)
obj1.a // 1
obj2.a // 1
obj1.hasOwnProperty("a") //true
obj2.hasOwnProperty("a") //false
obj2.a++ //发生隐式屏蔽
obj2.a //2
obj1.a //1
obj2.hasOwnProperty("a") //true

这段代码其实还蛮有意思的。

”类“

众所周知 JS 和传统的面向对象的语言不同,他并没有类来作为对象的抽象,JS 只有对象,他是少有的可以不通过类直接创建对象的语言。

虽然没有类,但是开发者们其实一直都在模仿类的行为,其实就是利用了函数的一种特性:所有的函数都会有一个名为 prototype 的公有且不可枚举的属性,他会指向一个对象。

Function Foo(){}
Foo.prototype //{}

这个对象称为 Foo 的原型,因为我们通过 Foo.prototype 来访问他,但是以学习 JS 的经验来看(滑稽),顾名思义会出大问题滴(再次滑稽)。先抛开名字不谈,这个东西到底是什么?
之前我们讲过 new 对象时大概会发生的四个步骤,这里就要提到第二步:执行原型连接,关联到 Foo.prototype 上。

function Foo(){
// ...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype // true

在面向对象的语言中,类可以被实例化多次,实例化一个类就意味着把类的行为复制到一个对象中,但是在 JS 中并没有类似的复制机制,你不能创建一个类的多个实例,只能创建多个对象,他们的[[Prototype]]关联到同一个对象上,因此在默认情况下,并不会发生复制,因此这些对象不会完全失去联系,他们是互相关联的。
但是事实上,我们可以认为 new Foo()所创建的这个链接其实是一种副作用,他并非直接创建关联。更直接的方法时:Object.create()

JS 中我们并不会把一个对象(类)复制到另一个对象(实例)上,只是将他们关联起来,这个机制通常被称为原型继承,这个名称主要是为了应对面向类的语言中继承的意义,但是其实并没有表示出动态语言中对应的含义。也就是说,这里说到继承,其实算不上继承。这可能会让习惯于传统语言的开发者非常不习惯。

继承意味着复制操作,但 JS 默认并不会复制对象属性,只是会创建一个关联,这样一个对象可以通过委托来访问另一个对象的属性和函数,委托这个词更能准确描述 JS 中对象的关联机制。

回到之前的代码中,为神马会认为 Foo 是一个类呢?因为 new。但是事实,JS 中,对于构造函数最好的解释我认为应该是:所有带 new 的函数调用。除了这个迷惑人的东西之外,还有一个 Prototype.constructor

Foo.prototype.constructor===Foo //true
var a = new Foo()
a.constructor === Foo //true

Foo.prototype 默认会有一个不可枚举的 constructor 属性,这个属性引用的是与对象关联的函数(Foo),而 a 竟然也有一个 constructor 属性,指向”创建这个对象的函数“,可能你已经想到,实际上,这个.constructor 是 Foo.prototype 的。

原型继承

先来看一段典型的原型继承的代码:

function Foo (name){
this.name = name
}
Foo.prototype.myName=function(){
return this.name
}
function Bar (name,label){
Foo.call(this,name)
this.label=label
}
Bar.prototype=Object.create(Foo.prototype)
// 注意这里相当于Bar.prototype 指向了一个新的对象,constructor丢失
Bar.prototype.myLabel=funciton(){
return this.label
}
var a = new Bar("a","obj a")
a.myName() //a
a.myLabel() //obj a

这段代码的核心就是使用 Object.create()创建了一个新对象,并把新的对象内部的[[Prototype]]关联到指定的对象,也就是说,创建一个新的 Bar.prototype 对象并且把他关联到 Foo.prototype.
有两种可能会用的方法:
Bar.prototype=Fo.prototype 这种是直接让 Bar.prototype 指向了 Foo.prototype,这样会在修改 Bar 的原型方法时其也修改了 Foo 的,这其实就没有 Bar 存在的意义了。

Bar.prototype=new Foo()这样的确会创建一个新对象,但是可能有一些 Foo 内部的副作用。

因此,要创建一个合适的关联对象,使用 Object.create()是一个比较好的方法,但是也有缺点,就是我们创建了一个新对象,然后把旧的抛弃掉了。在 ES6 之前可以通过修改._proto_来修改,但这个方法并不是标准而且浏览器兼容会有问题,在 ES6 中,出现了Object.setPrototypeOf(Bar.prototype,Foo.prototype)
也就是说我们现在有两种方法:

//ES6之前需要抛弃默认的Bar.prototype
Bar.prototype=Object.create(Foo.prototype)
//ES6之后可以直接修改
Object.setPrototypeOf(Bar.prototype,Foo.prototype)

那么如何找到委托关联对象之间的关系(内省)呢?a.isPrototypeOf(c),我们也可以直接获取一个对象的[[Prototype]]链:Object.getPrototypeOf(c),还可以通过c.__proto__ ===Foo.prototype来判断 , __proto__的实现大概类似这样:

Object.defineProperty(Object.prototype,"__proto__",{
get:function (){
return Object.getPrototypeOf(this)
},
set:function(o){
Object.setPrototypeOf(this,o)
return o
}
})
CATALOG
  1. 1. 从对象说起
    1. 1.1. 扯些关系不大的
    2. 1.2. 对象属性描述符
    3. 1.3. getter 和 setter
    4. 1.4. 遍历
  2. 2. 由对象到原型
    1. 2.1. 属性设置与屏蔽
  3. 3. ”类“
  4. 4. 原型继承