在我们学习认识原型(链)之前,有一个重要的前提是,一切引用类型都是对象,而对象时属性的集合。
对象的原型
当我们通过typeof来对于类型判断时我们会发现,引用类型的判断结果是function和object。其中,函数也是对象的一种。
通常,我们都是通过函数来创建对象的。
var obj = { foo: 1 }
// 等价于
var obj = new Object()
obj.foo = 1
console.log(typeof Object) // function
而同时函数又是对象的一种。这里可以看出函数与对象的关系有些复杂。
首先,我们创建的每个函数都有一个prototype的属性。这个属性是一个指针,指向一个对象,这个对象中包含了创建实例所共享的属性与方法。而我们通过该函数创建的实例中,包含了一个指针叫做[[prototype]],在浏览器中一般会暴露出一个私有属性_proto_,这实际上就是[[prototype]]的浏览器实现。这个属性指向了函数的prototype,通常我们把它叫做隐式原型。
此时我们再观察Object.prototype
而Object.prototype也是一个对象,那么它的_proto_是什么呢?这里有一个特例,Object.prototype的_proto_指向为null,这一点很特殊。
那么我们回过头来看函数,函数是否也有_proto_属性呢?
function foo() {}
var foo = new Function()
上面的代码中,第一种是我们常见的函数创建方式,而第二种是使用Function 构造函数来创建。当然第二种方法是不推荐使用的,这里我们是为了便于理解"函数是对象"的概念。
到这里,我们就可以看出,实际上foo._proto_指向的是Funtion.prototype,同样的,我们可以看到Object._proto_指向Funtion.prototype。Function也是一个函数,函数是一种对象,也有__proto__属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的。所以它的__proto__指向了自身的Prototype。
我们来梳理一下以上提到的关系:
- 对象都有一个
_proto_属性,它指向创建该实例的函数的prototype,比如obj._proto_ === Object.prototype - 函数由于是通过使用Function构造函数创建的,那么它也是一个对象,也含有_proto_指针指向
Function.prototype - 同样地,prototype属性本身也是一个对象。上文没提到的
Function.prototype._proto_指向的是Object.prototype。其中的一个特例是Object.prototype._proto_指向为null _proto_属性作为一个隐藏属性,是不应该被开发者直接去使用到的。
到这里,上述所讲的概念可以汇集成一张我们在提到原型关系经常会看到的一张图。
对象类型判断
那么对于引用类型的值的类型判断,我们可以使用instanceof。来看一个例子:
function Foo() {}
var fn = new Foo()
console.log(fn instanceof Foo) // true
console.log(fn instanceof Ojbect) // true
这里为了fn instanceof Object也是为true呢?首先我们要知道instanceof的判断规则:在A instanceof B中,分别沿着A的_proto_与B的prototype往上寻找,如果两条线有相同的一个引用,则返回true,否则为false。
那么我们结合上图来看,fn._proto_指向Foo.prototype,而Foo.prototype是一个对象,其_proto_指向Object.prototype。因此,该结果返回为true。
通过以上instanceof的判断规则,这实际上就是原型链得实现概念。结合上文我们提到的每个对象都有一个[[prototype]](proto)内置属性,用于指定对象的原型对象。
var arr = [1, 2, 3]
arr.toString() // 1,2,3
那么arr所用到的toString()方法是从何而来的呢?我们可以先观察arr的原型链:
arr._proto_ => Array.prototype
Array.prototype._proto_ => Object.prototype
Object.prototype._proto_ => null
我们可以看出,每个对象拥有一个原型对象,通过_proto_指针指向上一个原型,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。这种关系被称为原型链。
arr中的toString()方法调用时,js会沿着原型链向上查找,我们会发现在Array.prototype中有toString方法,于是便调用了该方法,这就是原型链的基本作用。
而我们沿着Array.prototype继续往上,其_proto_指向了Object.prototype。我们会发现toString实际上是在Object原型上的方法,而Array.prototype继承了来自Object.prototype的方法。这就是利用原型链的一种继承方式。
理解原型
通过上文我们可以知道,创建的每个函数都有一个prototype属性,这个属性指向一个对象,而这个对象包含了所有该函数创建实例所共享的属性与方法,这也正是使用原型对象的好处。
function Person() {}
Person.prototype.name = 'Mike'
Person.prototype.age = 18
Person.prototype.getName = function () {
console.log(this.name)
}
var person1 = new Person()
person1.getName() // Mike
var person2 = new Person()
person2.getName() // Mike
person1.getName === person2.getName // true
上述代码中,我们将getName()方法添加到了Person的prototype属性中,这些属性与方法会被我们所创建的实例所共享。我们同时也可以看到,实例访问的getName()方法实际上是同一个函数。
当我们调用person1.getName()时,实际上是通过person1.prototype.getName()来访问。也就是说,第一次,解析器先查找person1有没有sayName属性,如果没有,继续沿着原型链搜索,找到了person1的原型(Person),当找到时,读取这个函数。
我们能够访问原型中的属性方法,但是却不能通过实例去重写原型的值。
// 接上
person1.age = 20
person1.age // 20
当我们给实例添加age属性并赋值时,实际上是屏蔽了原型上的age。因为在第一步查找的时候,已经找到了person1实例中含有age,因此不必再去搜索其原型。同样地,此时我们通过delete操作删去我们刚刚添加的age属性,则又会重新访问到原型中的属性。
使用hasOw0nProperty()可以检测一个属性是否存在于实例中。同样的hasPrototyperoperty()方法可以检查属性是否存在于原型中。in操作符用来判断属性是否存在,无论是在实例还是原型中。
但是,如果对于原型中的引用类型属性,会存在问题。
function Foo() {}
Foo.prototype.colors = ['red', 'yellow']
var bar1 = new Foo()
person1.colors.push('black')
var bar2 = new Foo()
console.log(person2.colors) // ['red', 'yellow', 'black']
上述代码中,修改了bar1.colors引用的数组,这一修改会影响到bar2.colors,而我们如果希望实例拥有各自的属性而不是共享同一个数组,这便是原型模式的问题所在。
为了简化prototype的写法,我们可以通过使用对象字面量的来重写整个原型对象。
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Mick',
age: 18,
getName() {
console.log(this.name)
}
}
其中有一个特例是constructor属性,它指向Person。如果我们重写原型对象时遗漏了该属性,则它不会像预期那样指向Person,而是指向Object。如果我们需要用到该属性,可以将它设置指回合理的值。这样的重写会导致该属性变为可枚举的(原生不可枚举),可通过Object.defineProperty方法将其置为false。
这样的重写带来的问题是,如果我们创建实例的语句在重写之前,那么等于是切断了构造函数与原型之间的联系。不再是原来再调用构造函数时添加的一个指向原始原型的指针。
function Person() {}
var person1 = new Person()
Person.prototype = {
constructor: Person,
name: 'Mick',
age: 18,
getName() {
console.log(this.name)
}
}
person1.getName() // Uncaught TypeError: person1.getName is not a function
person1此时指向的原型不包含有getName命名的属性,它引用的仍然是最初的原型,因此抛出了错误。
相关练习
例1
function Foo() {}
Foo.prototype.a = 1
var bar = new Foo()
foo.prototype = {
a: 2,
b: 3
}
console.log(bar.a) // 1
console.log(bar.b) // undefined
上述代码中由于重写了prototype,使得bar实例仍然指向了原来的prototype。
例2
var Foo = function () {}
Object.prototype.a = function() {
console.log('a')
}
Function.prototype.b = function() {
console.log('b')
}
var bar = new Foo()
console.log(bar.a()) // a
console.log(bar.b()) // Uncaught TypeError: bar.b is not a function
console.log(Foo.a()) // a
console.log(Foo.b()) // b
首先我们来看实例bar,它的_proto_指向了Foo.prototype,而Foo.prototype._proto_指向了Object.prototype。因此可以调用a方法,而通过原型链找到b方法。
再来,Foo._proto_指向了Function.prototype,Function.prototype._proto_指向了Object.prototype,因此,其可以通过原型链找到a和b两种方法来调用。