JavaScript 原型链
还记得在上一篇文章8. 原型中讲过,当我们从一个对象中获取某一个属性时, 它会触发 [[get]] 操作
- 在当前对象中去查找对应的属性, 如果找到就直接使用
- 如果没有找到, 那么会沿着它的原型链去查找 [[prototype]]对象
原型怎么就成链了呢?因为对象原型也是个对象啊,是个对象就有[[prototype]]
。这样原型指向原型一路上去就形成了一个链条。
let obj = {}
// 将属性添加到 obj 的原型对象中
// 采用重写原型对象的方式,constructor 暂时不管
obj.__proto__ = {
name: 'zs'
}
console.log(obj.name); // zs
// 将属性添加到 obj 的原型对象的原型对象中
obj.__proto__.__proto__ = {
age: 18
}
console.log(obj.age); // 18 ,都是可以正常访问到
原型链的顶层对象
原型链的顶层是**Object()**
函数的原型对象**Object.prototype**
函数原型对象没有对象原型,也就是**Object.prototype.__proto__**
属性为**null**
顶层对象里面除了 constructor 属性还有很多其他的方法,属性可以供所有函数使用
console.log(Object.getOwnPropertyDescriptors(Object.prototype));
字面量对象的原型
首先要说明一点,js 创建对象的方式严格来说其实只有一种,就是 new 构造函数。字面量创建对象其实是**new Object()**
的语法糖。所以字面量创建的对象都没有具体类型,都是 Object。
let obj = {}
其实就是let obj = new Object()
看如下代码,node 环境直接打印字面量对象的原型
let obj = {}
console.log(obj.__proto__) // [Object: null prototype] {}
打印的是[Object: null prototype] {}
,这其实就是原型链的顶层,Object 函数的原型对象。因为我们知道 new 一个函数,就会把函数原型对象赋值给 new 创造的空对象的原型。相当于执行了这样的操作obj.__proto__ = Object.prototype
。
所以字面量对象的原型对象就是原型链的顶层对象,**let obj = {}; obj.__proto__; 顶层对象**
这也解释了很多问题,比如前文中为了表示 constructor 属性和函数对象相互指向的测试代码,字面量对象比较就为 false。
let obj = {}
// obj.__proto__ 实际是 Object.prototype
console.log(obj.__proto__.constructor === obj) // false
console.log(obj.__proto__.constructor === Object) // true
还有上面表示原型链的代码,为什么要选择重写对象原型的方式,而不是直接obj.__proto__.name
这样赋值。因为obj.__proto__
就已经是原型链的顶层对象了,再来一个__proto__
就是 null ,无法赋值。
let obj = {}
// 顶层原型对象的隐式原型为 null
console.log(obj.__proto__.__proto__); // null
函数的顶层原型
和字面量对象差不了多少,无非就是中间多了一步,不像字面量对象原型直接就指向顶层对象了
function Person() { }
console.log(Person.prototype.__proto__) // [Object: null prototype] {}
总结
- 原型链顶层对象
Object.prototype
- 顶层对象没有隐式原型
Object.prototype.__proto__ === null
- 字面量对象访问顶层对象
obj.__proto__
- 函数访问顶层对象
Fun.prototype.__proto__
继承
JavaScript 中构造函数就相当于 Java 中的类。继承最大的好处就是复用了代码。
原型链实现继承
继承是为了复用父类代码,那只要子类能访问到父类的实例对象,那不就是相当于拥有了父类的一切属性,复用了父类的代码;又想到对象查找属性会先从本身查完再去原型对象上查,那只要把父类的实例对象赋值给子类构造函数的原型就行了。因为 new 子类构造函数的时候,子类的实例对象会指向子类构造函数原型,所以子类实例对象就能访问到父类实例对象了。这就是实现了继承的核心关键目标:子类可以访问父类的属性和方法。
// 父类
function Person(name) {
this.name = name
Person.prototype.eat = function() {
console.log(this.name + ' eating');
}
}
// 子类
function Student(sno) {
this.sno = sno
}
// 原型链继承,将子类的显示原型指向父类的一个实例
let p = new Person()
Student.prototype = p
// 给子类的显示原型上添加方法,注意:一定要在子类连接父类后修改,因为子类原型已经变了
Student.prototype.sing = function() {
console.log(this.name + this.sno + ' sining');
}
// 实例化对象
let student1 = new Student(111)
student1.name = 'zs'
student1.eat() // zs eating
student1.sing() // zs111 sining
let student2 = new Student(222)
student2.name = 'ls'
student2.eat() // ls eating
student2.sing() // ls222 sining
虽然这种方式虽然实现了继承,但是也有三个很大的缺点
- 继承来的属性是枚举不到的
- 枚举类似浅拷贝,不会去枚举引用类型中的内容,隐式原型是个对象,就是个引用类型
- 父类属性中的引用类型,会被多个对象实例共享
- 子类实例化的时候,不能在子类构造函数中给继承来的父类属性传递参数
- 因为父类对象只在子类实现继承的时候实例化一次给子类的原型对象(没办法定制化) ```javascript // 父类 function Person() { this.name, this.friends = [] }
// 子类 function Student(sno) { this.sno = sno }
// 原型链继承,将子类的显示原型指向父类的一个实例 let p = new Person() Student.prototype = p
// 给子类的显示原型上添加方法,注意:一定要在子类连接父类后修改,因为子类原型已经变了 Student.prototype.sing = function() { console.log(this.name + this.sno + ‘ sining’); }
// 实例化学生1 let student1 = new Student(111)
// 弊端一:无法枚举父类的属性 console.log(student1); // Person { sno: 111 } // 弊端二:需要动态给父类属性赋值 student1.name = ‘zs’ // 注意:给父类属性赋值,其实是给student1对象本身添加了一个 name 属性等于’zs’ // 因为name是添加在子类对象上,所以可以被遍历到了 console.log(student1); // Person { sno: 111, name: ‘zs’ }
student1.friends.push(‘w5’)
// 实例化学生2 let student2 = new Student(222) student2.name = ‘ls’
// 弊端三:父类的引用属性被共享了 console.log(student2.friends); // [ ‘w5’ ],ls 居然访问到了 zs 的朋友
<a name="dBtLC"></a>
## 借用父类构造函数优化原型链继承
为了解决原型链继承中存在的问题,开发人员提供了一种思路来优化原型链继承。就是**在子类型构造函数的内部调用父类型构造函数,并且修改父类构造函数执行时的 this ,使他它指向子类。这就相当于将父类和子类合并成一个函数**,因为父类构造函数执行时 new 出来的空对象,其实是子类的空对象。所以也可以说是借用了父类的构造函数的代码。
```javascript
// 父类
function Person(name, friends) {
this.name = name,
this.friends = friends
}
// 子类
function Student(sno, name, friends) { // 添加上父类参数好给父类执行时初始化
Person.call(this, name, friends) // 执行父类并修改 this 指向子类 new 出来的空对象
this.sno = sno
}
// 原型链继承,将子类的显示原型指向父类的一个实例
let p = new Person()
Student.prototype = p
// 给子类的显示原型上添加方法,注意:一定要在子类连接父类后修改,因为子类原型已经变了
Student.prototype.sing = function() {
console.log(this.name + this.sno + ' sining');
}
// 子类上初始化父类参数
let student1 = new Student(111, 'zs', ['w5'])
// 可枚举父类的属性
console.log(student1); // Person { name: 'zs', friends: [ 'w5' ], sno: 111 }
let student2 = new Student(222, 'ls', ['hh'])
// 父类引用属性互相隔离
console.log(student2.friends); // [ 'hh' ]
虽然解决了三个弊端,但又产生了两个新问题:
- 实例一个对象的时候,父类构造函数要执行两次
- 一次在子类构造函数中,一次将父类实例对象赋值给子类显式原型
- 所有的子类实例事实上会拥有两份父类的属性
- 一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.proto里面),父类和子类连接而实例化的一套父类属性是多余的。
- 因为父类构造函数实例化了两次,自然会出现两套父类属性,但访问不受影响,因为查找对象先从对象自身查起
继承的终极方案
看这两个问题,会发现都是new Person()
产生的。
// 原型链继承,将子类的显示原型指向父类的一个实例
let p = new Person()
Student.prototype = p
我们为什么需要new Person()
?因为想让子类对象可以访问到父类的对象,这样就可以访问父类的属性和方法。
但是现在对于父类的属性,我们已经通过借用父类构造函数的方式,在实例化子类对象的时候,将父类的属性全部定义到子类中了,所以我们只需要让子类可以访问到父类的方法就行。
父类的方法定义在父类构造函数原型里面,所以只要子类实例对象可以访问到父类构造函数的原型对象就行。
总结一下阶段成就:
- 继承的本质目标是复用代码,子类实例对象可以访问到父类的属性和方法
- 继承的最佳实现
- 属性:借用父类构造函数
- 方法:具体方案未知;目标:子类实例对象可以访问到父类构造函数的原型对象
怎么实现子类实例对象访问到父类构造函数原型?
最直接的方式将父类构造函数原型赋值给子类构造函数原型
Student.prototype = Person.prototype
这显然不行,因为给子类 Student 定义方法的时候是定义在子类构造函数的原型上,那现在一赋值,不就定义到父类的原型上了吗。如果Person 有多个子类,那么这些子类上的方法将会全部堆积到父类原型对象上。
天降猛男 道格拉斯·克罗克福德
道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写了一篇文章: 《Prototypal Inheritance in JavaScript(在JS中使用原型式继承)》,提出了原型式继承。文章中实现了一个函数,将父对象赋值给子对象的原型,完成了对象间的继承。
// 实现 info 对象继承 obj 对象
let obj = {
name: "zs",
say: function() {
console.log(this.name + ' saying');
}
}
let info = {}
// 实现对象间的继承,但是很明显这样子对象是写死的,所以封装了一个函数
// info.__proto__ = obj
// 封装成函数
// 封装成函数
function createObj(fatherObj,) {
let temp = {}
// 实际开发不能使用__proto__,因为不是官方的API,存在兼容性问题
// temp.__proto__ = fatherObj
// 存在获取对象原型的方法 get,自然有个设置对象原型的方法
Object.setPrototypeOf(temp, fatherObj)
return temp
}
console.log(info.name); // zs
info.say() // zs saying
console.log(info.__proto__) // { name: 'zs', say: [Function: say] }
上面我们用Object.setPrototypeOf(对象, 新原型)
方法将父对象赋值给了子对象的原型,然后道格拉斯那个时候并没有这个方法,所以这个对象继承函数他是这样实现的。
// 利用了new 关键字会将构造函数的显示原型赋值给实例对象的隐式原型的特点
function createObject(fatherObj) {
function temp() {}
temp.prototype = fatherObj
return new temp()
}
然后现在 ECMA 官方推出了这么一个方法,封装了上面的这些具体实现,一个方法就实现了对象继承。
Object.creat(obj)
方法会将参数对象obj,赋值给一个新对象的原型,并返回该新对象。let person = {}
let info = Object.create(obj) // info 对象继承了 person 对象
终极方案
现在回到开始那个问题:怎么实现子类实例对象访问到父类构造函数原型?
我们开始是直接原型赋值原型,发现有问题。但是可别忘记了原型也是对象呀,前面大佬不是提出了对象间继承的方法吗?那么将子类构造函数原型对象继承父类构造函数原型对象,这不就实现了子类实例对象访问父类构造函数原型的目标。
Student.prototype = Object.create(Person.prototype) // 对象间的继承
我们知道Object.create()
方法的具体实现,方法内部会创建一个空的新对象作转接,所以定义在子类上的方法都是定义到那个中间空对象去了,并不会全部堆积到父类构造函数原型对象中。这就解决了直接赋值的问题。
函数中的空对象不是临时的吗,不是会销毁吗?答:不会销毁,形成闭包了,我的老伙计
注意一个问题:记得添加constructor
属性指回子类构造函数
空对象赋值给子类构造函数的原型对象,对原型对象整体赋值,里面的 constructor 属性没了。这会导致子类实例对象打印父类的类型。
因为类型名来自构造函数原型对象中的 constructor 中的 name 属性,现在 constructor 没了,就会沿着原型链继续往上找,子类原型继承自父类原型,父类原型中 constructor。所以子类实例对象就会打印父类的类型。
function Person(name) {
this.name = name
}
function Student(sno, name) {
Person.call(this, name) // 借用构造函数继承属性
this.sno = sno
}
Student.prototype = Object.create(Person.prototype) // 对象继承实现继承方法
console.log(new Student(1, 'zs')); // Person { name: 'zs', sno: 1 }
总结一下继承:
- 继承的本质目标是复用代码,子类实例对象可以访问到父类的属性和方法
- 继承的最佳实现
- 属性:借用父类构造函数,将父类属性注入子类
- 方法:通过对象间的继承,实现子类实例对象访问父类构造函数的原型对象
function Person(name, friends) {
this.name = name,
this.friends = friends
}
Person.prototype.eat = function() {
console.log(this.name + ' eating');
}
function Student(sno, name, friends) {
Person.call(this, name, friends) // 借用构造函数继承属性
this.sno = sno
}
Student.prototype = Object.create(Person.prototype) // 对象继承实现继承方法
Object.defineProperty(Student.prototype, 'constructor', { // 添加 constructor 属性
configurable: true,
enumerable: false,
writable: true,
value: Student
})
Student.prototype.study = function() {
console.log(this.name + this.sno + ' studying');
}
let stu1 = new Student(001, 'zs', ['w5'])
let stu2 = new Student(002, 'ls', ['hh'])
console.log(stu1.__proto__.constructor.name) // Student
console.log(stu1); // Student { name: 'zs', friends: [ 'w5' ], sno: 1 }
console.log(stu2); // Student { name: 'ls', friends: [ 'hh' ], sno: 2 }
stu1.eat() // zs eating
stu2.study() // ls2 studying
这样继承基本上可以了,没有明显问题,但是假如有多个子类继承父类的时候,中间的继承过程需要重复写,这很麻烦,所以可以再优化一下,将对象继承的过程封装成一个工具函数。
// 实现继承方法的工具函数
function createObject(SuperType) { // 考虑兼容性,手动实现 Object.create() 方法
function Fn() {}
Fn.prototype = SuperType
return new Fn()
}
function inheritPrototype(SubType, SuperType) {
SubType.prototype = createObject(SuperType.prototype) // // 对象继承
Object.defineProperty(SubType.prototype, "constructor", { // 添加 constructor 属性
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
// 父类
function Person(name, friends) {
this.name = name,
this.friends = friends
}
Person.prototype.eat = function() {
console.log(this.name + ' eating');
}
// Student 子类
function Student(sno, name, friends) {
Person.call(this, name, friends) // 借用构造函数继承属性
this.sno = sno
}
// Teacher 子类
function Teacher(tno, name, friends) {
Person.call(this, name, friends),
this.tno = tno
}
// 实现继承
inheritPrototype(Student, Person)
inheritPrototype(Teacher, Person)
Student.prototype.study = function() {
console.log(this.name + this.sno + ' studying');
}
Teacher.prototype.teach = function() {
console.log(this.name + ' teaching');
}
let stu1 = new Student(001, 'zs', ['w5'])
let stu2 = new Student(002, 'ls', ['hh'])
let teacher = new Teacher(999, '付老师', ['dd'])
console.log(stu1); // Student { name: 'zs', friends: [ 'w5' ], sno: 1 }
console.log(stu2); // Student { name: 'ls', friends: [ 'hh' ], sno: 2 }
console.log(teacher); // Teacher { name: '付老师', friends: [ 'dd' ], tno: 999 }
stu1.eat() // zs eating
stu2.study() // ls2 studying
teacher.teach() // 付老师 teaching
原型方法的补充
hasOwnProperty
对象.hasOwnProperty(属性)
判断属性是否是属于对象自己的属性(不是在原型上的属性)
let obj = {
name: 'zs'
}
// Object.create() 方法第二个参数和 Object.defineProperties() 一样
// 可以批量给子类对象本身添加属性
let info = Object.create(obj, {
age: {
value: 18,
enumerable: true
}
})
console.log(info.hasOwnProperty('name')); // false,name 是父类的属性
console.log(info.hasOwnProperty('age')); // true,age 是info 对象自身的属性
console.log(info.hasOwnProperty('hhhh')); // 完全不属于该对象的属性也是 false
in / for in
in/for in 操作符
判断某个属性是否在对象上,无论是在对象自身上还是在原型链上,都是 true。in
操作符和hasOwnProperty()
一个不区分原型链,一个区分原型链。所以两个一结合,就可以判断出一个属性是否是该对象父类上的属性。
先找出对象上的所有属性,再区分哪些是自身的,其他的就是原型链上的属性,也就是父类的属性。
let obj = {
name: 'zs'
}
let info = Object.create(obj, {
age: {value: 18, enumerable: true}
})
console.log('name' in info); // true,name 是父类的属性,
console.log('age' in info); // true,age 是info 对象自身的属性
// for in 操作会遍历出对象自身和原型链上的属性
for (const key in info) {
console.log(key); // age, name
}
instanceof
对象 instanceof 构造函数
用于检测构造函数的 pototype,是否出现在某个实例对象的原型链上。注意:“原型链只能往上”。换句话说判断该构造函数是否是该对象的父类,或者就是该对象的构造函数。
另外所有引用类型数据都是 Object 的子类实例,所以value instanceof Object
可以用来判断 value 是否为引用类型数据。
// 实现继承方法的工具函数
function createObject(SuperType) { // 考虑兼容性,手动实现 Object.create() 方法
function Fn() {}
Fn.prototype = SuperType
return new Fn()
}
function inheritPrototype(SubType, SuperType) {
SubType.prototype = createObject(SuperType.prototype) // // 对象继承
Object.defineProperty(SubType.prototype, "constructor", { // 添加 constructor 属性
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
// 父类
function Person() {
}
// Student 子类
function Student() {
Person.call(this)
}
// 继承
inheritPrototype(Student, Person)
let stu = new Student()
let per = new Person()
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
console.log(per instanceof Student); // false, Student类不是 per 对象的父类
isPrototypeOf
对象.isPrototypeOf(对象)
用于检测某个对象,是否出现在某个实例对象的原型链上。和instanceof
很类似,不同点就是换成了比较对象。一般用原型对象去调用这个方法,判断这个原型对象是否是该参数对象的爸爸辈。
// 实现继承方法的工具函数
function createObject(SuperType) { // 考虑兼容性,手动实现 Object.create() 方法
function Fn() {}
Fn.prototype = SuperType
return new Fn()
}
function inheritPrototype(SubType, SuperType) {
SubType.prototype = createObject(SuperType.prototype) // // 对象继承
Object.defineProperty(SubType.prototype, "constructor", { // 添加 constructor 属性
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
// 父类
function Person() {
}
// Student 子类
function Student() {
Person.call(this)
}
// 继承
inheritPrototype(Student, Person)
let stu = new Student()
let per = new Person()
console.log(stu.__proto__.isPrototypeOf(stu)); // true
console.log(per.__proto__.isPrototypeOf(stu.__proto__)); // true
console.log(stu.__proto__.isPrototypeOf(per)); // true