今天我们来谈谈 JavaScript 中的继承。与其说是继承,不如说委托更准确点。因为 JavaScript 中没有所谓的父亲和孩子的概念,更谈不上继承。在 JavaScript 中,一切皆是对象,对象之间通过委托建立关联,使一个对象可以引用其他对象的属性和方法,达到“继承”的目的。
Javascript 中的每个对象都有一个隐藏的 __proto__ 属性,也叫 [[prototype]] 。这个属性指向一个叫做原型的对象,那么,什么是原型对象呢?
JavaScript 中的每个函数都有一个 prototype 属性,该属性指向一个对象,这个对象就叫做原型对象。
let Animals = function (name) {
this.name = name
}
console.log(Animals.prototype) // 打印结果看下图
由上图可知,原型对象有两个不可迭代属性,constructor 和 __proto__ 。constructor 属性指向构造函数 Animals,__proto__ 则指向 Animals.prototype 的原型,即 Objcet.prototype。
console.log(Animals.prototype.constructor === Animals); // true
console.log(Animals.prototype.__proto__ === Object.prototype) // true
为了更好的理解原型链,我们需要先了解一下构造函数,原型对象和实例对象之间的关系。
// 往原型对象中添加属性和方法
Animals.prototype.weight = 120
Animals.prototype.colors = ['pink']
Animals.prototype.getName = function () {
return this.name
}
// 实例对象可以同时访问构造函数和原型对象中的属性和方法
let Dog = new Animals('Sugary')
console.log(Dog.weight) // 120
console.log(Dog.getName()) // 'Sugary'
由上面的代码可以看出,实例对象可以同时访问构造函数和原型对象中的属性和方法,那实例属性和原型属性有什么区别呢?
// 创建另一个实例对象
let Cat = new Animals('Momo')
console.log(Cat.name) // 'Momo'
console.log(Cat.weight) // 120
Cat.colors.push('white')
console.log(Dog.colors) // ['pink', 'white']
由上面的代码可以看出,Cat 实例对属性 colors 的操作影响了 Dog 实例的 colors 属性,这当然不是我们想要的结果。
通俗来讲,原型对象存放可共享的属性和方法,而构造函数中定义的属性和方法则应是实例对象独有的。
原型对象具有如下特征:
1.由构造函数的 prototype 属性生成;
2.构造函数的实例对象通过 __proto__ 属性可以访问它的原型对象;
看到这里,你是否会有点晕眩?不要被 prototype 属性和 __proto__ 属性搞晕了,它们长得很像,但其实是不一样的东西。通俗点来讲,prototype 是函数才有的属性,而 __proto__ 是对象才有的属性。
讲到这里,我们已经嗅到原型链的味道了,那么,到底什么是原型链呢?
在 JavaScript 中,一切皆对像。JavaScript 中的对象都有一个隐藏的属性 __proto__ 指向它的原型对象(prototype),而该原型对象也有自己的 __proto__ 指向它的原型对象,像一条链条一样,层层向上,直到到达一个对象 Object.prototype 的原型是 null,( null 是没有原型对象的 ),原型链结束。所以,Object.prototype 是原型链的终点。let myArray = [1, 2, 3];
console.log(myArray.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
// 此时的原型链是:myArray --> Array.prototype --> Object.prototype --> null
let myFunction = function () {};
console.log(myFunction.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
// 原型链: myFunction --> Function.prototype --> Object.prototype --> null
let myObj = {};
console.log(myObj.__proto__ === Object.prototype); // true
// 原型链: myObj --> Object.prototype --> null
由上面的代码可以发现,所有的原型链最终都会经过 Object.prototype 到达 null,也就是说,原型链的终点就是 Object.prototype。
注意:构造函数的实例对象可以访问原型对象中的属性,但是构造函数不能访问原型对象中的属性哦,举个栗子:
let Animals = function (name, age) {
this.name = name
}
Animals.prototype.weight = 120
let Dog = new Animals('Sugary', 12)
console.log(Dog.weight) // 100
console.log(Animals.weight) // undefined
原型链的强大不只如此。假设一个原型对象(A)指向另一个实例对象(B),而这个实例对象(B)本身就继承了其原型对象和构造函数的属性和方法,也就是说此时A可以访问到B原型对象和构造函数的所有属性和方法。
let Animals = function (name) {
this.name = name
this.colors = ['white']
}
Animals.prototype.getName = function () {
return this.name
}
let Dog = function (color) {
console.log('doSomething!')
}
Dog.prototype = new Animals('Sugary')
let dog1 = new Dog()
dog1.colors.push('black') // 实例dog1往Animals的实例属性colors添加值 [ 'white', 'black' ]
console.log(dog1.colors)
let dog2 = new Dog()
console.log(dog2.colors) // 但是影响了实例dog2的colors值 [ 'white', 'black' ]
且慢,似乎有点不对劲。前面我们说过,构造函数的属性是实例独有的,不会互相影响的,而原型属性则是可以共用和相互影响。Dog 通过原型链继承了 Animals 的所有属性和方法,但是原先属于 Animals 的实例属性如今变成了 Dog 的原型属性(比如 name 和 colors),而 colors 属性是一个引用类型值,一个实例对其改变会影响另一个实例的该属性的值,这当然不是我们想要的啦。
let Animals = function (name) {
this.name = name
this.colors = ['white']
}
let Dog = function (name) {
Animals.call(this, name) // 改成这样就可以啦 -- Animals 函数第二次调用
// doSometing
}
Dog.prototype = new Animals('Sugary') // Animals 函数第一次调用
看上去似乎没啥问题了,但是上面的代码中,函数 Animals 调用了两次,造成了一些不必要的浪费,我们完全不需要这么做。
let Animals = function (name, age) {
this.name = name
this.colors = []
}
Animals.prototype.getName = function () {
return this.name
}
let Dog = function (color, name, age) {
Animals.call(this, name, age) // 实例属性继承
this.color = color
}
// 原型属性继承
Dog.prototype = Object.create(Animals.prototype) // 改成这样就可以啦
下面说说几种创建对象和生成原型链的方式
1.使用语法结构创建的对象
let o = {a: 1};
// 原型链如下:
// o ---> Object.prototype ---> null
let a = ["yo", "whadup", "?"];
// 原型链如下(数组都继承于 Array.prototype):
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 原型链如下(函数都继承于 Function.prototype):
// f ---> Function.prototype ---> Object.prototype ---> null
2.使用构造器创建的对象(即通过 new 操作符)
function Graph() {
this.vertices = [];
}
Graph.prototype = {};
let g = new Graph();
// 在 g 被实例化时,g.__proto__ 指向了 Graph.prototype。
3.使用 Object.create 创建的对象
let s = Object.create(t) // 表示 s 的 __proto__ 指向 t
let a = {a: 1};
// a ---> Object.prototype ---> null
let b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
let d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
hasOwnProperty 和 Object.keys() 是 JavaScript 中唯一两个处理属性并且不会遍历原型链的方法。
以上,就是我对 JavaScript 原型链继承的理解。JavaScript 中的对象依靠 __proto__ 这个隐藏的属性,可以访问它的原型对象中的属性和方法,而它的原型对象 __proto__ 属性,仿佛一条链条一样,将 JavaScript 中的对象联系起来。