- A+
原型链
原型链的概念
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾以下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,加入我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
实现原型链有一种基本模式,代码如下:
function Person() {} Person.prototype.type = '人类' function Student() {} Student.prototype = new Person() Student.prototype.job = '学生' var student = new Student() console.log(student.type) // 人类 console.log(student.job) // 学生 console.log(student)
以上代码定义了两个类型:Person 和 Student。它们的主要区别是 Student 继承了 Person,而继承是通过 创建 Person 的实例,并将该实例替换为 Student.prototype。实现的本质是重写了 Student 的原型对象,取而代之的是一个新类型的实例。换句话说,原来存在于 Person 的实例中的所有属性或方法,现在也同样存在于 Student.prototype 中了。在确立了继承关系之后,我们给 Student.prototype 添加了一个新的属性: job。这样就在继承了 Person 的属性和方法的基础上又添加了一个新的属性。打开控制台可以看到以下信息:
我们知道,在创建一个函数时,系统会自动帮我们创建属于它的原型对象,在这个原型对象有一个默认属性 constructor 的指针,它指向了新创建的函数。但是在这里,由于 Student 中系统帮我们创建的原始原型对象被我们替换成了 Person 的实例,所以它的 constructor 也就被我们覆盖掉了。这时访问 Student.constructor 会出现这样的结果:
console.log(student.constructor) // ƒ Person() {}
在执行这句代码的时候,其内部经过 3 次搜索。
- 搜索 student 实例本身的属性 constructor,找不到
- 搜索 student 的原型对象(Person实例)的属性 constructor,找不到
- 搜索 student 的原型对象的(Person实例)的原型对象(本身是 Object 的实例)的属性 constructor,找到了!
最终返回的结果指向了 Person 函数,这明显是不符合逻辑的,需要我们手动为它矫正:
function Person() {} Person.prototype.type = '人类' function Student() {} Student.prototype = new Person() Student.prototype.constructor = Student // 为它的原型对象添加 constructor 属性 Student.prototype.job = '学生' var student = new Student() console.log(student)
在上面的代码的第 6 行中,为 Student 的原型对象添加了属性 constructor,并让其指向了 Student 函数。这样,在访问实例 student 的 constructor 属性时,它会进行以下操作:
- 搜索 student 实例本身的属性 constructor,找不到
- 搜索 student 的原型对象的属性 constructor,找到了!
再找不到属性或方法的情况下,搜索过程总是要一环一环地向前行到原型链末端才会停下来,如果以上第二步没有找到属性 construcotr (下图画红线的地方),它会再沿着原型链继续搜索,找到 student 的原型对象(Person的实例)的原型对象的属性 constructor(下图画蓝线的地方)。
打开控制台:
默认的原型——Object
我们知道,所有引用类型默认都继承了 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有i自定义类型都会继承 to.String()、valueOf() 等默认方法的根本原因。所以,上面例子展示的原型链中,student 实例原型对象是 Person 的实例,而这个 Person 实例的原型指针还指向了 Object 的实例。
一句话, Student 继承了 Person,而 Person 又继承了 Object。当调用 student.toString() 的时候,实际上就是在调用 Object.prototype 中的那个方法。
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。以下几行代码就说明了这一点。
console.log(student instanceof Object) // true console.log(student instanceof Person) // true console.log(student instanceof Student) // true
由于原型链的关系,可以说 instanceof 是 Object、Person、Student 中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了 true。
第二种方式是使用 isPrototypeOf() 方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf() 方法也会返回 true ,如下所示:
console.log(Object.prototype.isPrototypeOf(student)) //true console.log(Person.prototype.isPrototypeOf(student)) //true console.log(Student.prototype.isPrototypeOf(student)) //true
谨慎地定义方法
子类型有时候有时候需要重写超类(父类型)地某个方法,或者需要添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在一环原型的语句之后。
function Person() {} Person.prototype.type = '人类' function Student() {} Student.prototype.job = '学生' // 一定要放在替换原型的代码之后 Student.prototype = new Person() // 会覆盖掉上面添加的 job 属性 var student = new Student() console.log(student.job) // undefined
还有一点,不能通过对象字面量创建原型方法来添加新的属性。因为字面量赋值实际上就是创建了一个全新的对象,然后再进行赋值。
function Person() {} Person.prototype.type = '人类' function Student() {} Student.prototype = new Person() Student.prototype = { job: '学生' } var student = new Student() console.log(student.type) // undefined
上面代码中,在使用字面量的方式为 Student 的 prototype 赋值时,实际上经过了下面的过程
-
创建新对象 { job: '学生' }
-
为 Student.prototype 赋值为 { job: '学生' }
然而,这实际上是让 Student.prototype 的指针指向了全新的对象,所以原型链就在这儿被切断了,自然也就访问不到 Person的属性 type。
原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类性值得原型。原型对象中的引用型类型值会被所有实例共享,而这也正是为什么要在构造函数中,而不是再原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会编程另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。看下面代码:
function Person() { this.family = ['father', 'mother'] } Person.type = '人类' function Student() {} Student.prototype = new Person() const student1 = new Student() student1.family.push('brother') console.log(student1.family) // ["father", "mother", "brother"] const student2 = new Student() console.log(student2.family) // ["father", "mother", "brother"]
这个例子中的 Person 构造函数定义了一个 family 属性,该属性包含了一个(引用类型值)。Person 的每个实例都会有各自包含自己数组的 family 属性。当 Student 通过原型链继承了 Person 之后,Student.prototype 就变成了 Person 的一个实例,因此它也拥有了自己的 family 属性——就跟专门创建了一个 Student.prototype.family 属性一样。但结果是什么呢?结果是 Student 的所有实例都会共享这一个 family 属性。而我们对 student1.family 的修改能通过 student2.family 反映出来,就已经充分证实了这一点。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没办法在不影响所有对象实例下的情况下,给超类型的构造函数传递参数。有鉴于此,再加上之前说的由于原型中包含引用类型所带来的问题,实践中很少会单独使用原型链。