聊聊原型与继承

439 阅读4分钟

原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

传统的OOP中,对象的继承一般是通过类来完成。但在javascript中不同,js本身没有类的概念,即使是class也只是语法糖而已。在js中,继承通过特殊的方式实现——原型链。

构造函数

构造函数,是一种特殊的函数。它主要用来在创建对象时初始化对象,即为对象赋予初始值。
你可以将构造函数理解为“对象生成器”,js中所有的对象都有自己的构造函数,就像所有人都有自己的父母。
构造函数总是与new运算符一起使用在创建对象的语句中。

let arr = new Array(1, 2, 3);
console.log(a); // [1, 2, 3]

let str = new String('string~~~~');
console.log(str); // 'string~~~~'

js内置许多构造函数,如String、Number、Boolean、Data等。当然,你也可以自己构建构造函数。实际上,js中所有的函数都可以成为构造函数。

// es5
let Person = function (name) {
  this.name = name;
}
Person.prototype.sayName = function () {
  console.log(this.name)
}

let thovino = new Person('thovino');
thovino.sayName() // 'thovino'
// es6
class Person {
  constructor (name) {
    this.name = name;
  }

  sayName () {
    console.log(this.name)
  }
}

let thovino = new Person('thovino');
thovino.sayName() // 'thovino' 

在我们使用new关键字来生成对象时,继承的属性被定义在实例本身身上,继承的方法则被定义在构造函数的prototype属性上。而这个prototype属性,也就是实例对象的原型对象
当我们访问对象的属性或方法时,浏览器首先会查找对象本身是否拥有这些属性和方法。如果没有,浏览器会去查找对象的原型对象上有没有这些属性和方法。如果还是没有,就查找原型对象的原型对象。这就是所谓的原型链。

__proto__

在js中,万事万物皆对象。所有对象都有一个内部属性[[prototype]],在部分浏览器中该属性名为__proto__。(为了区别两个不同的属性名,我们在下文中直接写作__proto__
该属性的值是一个指针,它和对象的构造函数的prototype属性指向同一个对象,既该对象的原型对象
换句话说,对象的__proto__属性和该对象的构造函数的prototype属性是同一个对象,既该实例对象的原型对象

实际上[[prototype]]属性在web标准中并没有,也就是说原本作为开发者我们是无法访问对象的原型对象的。但它仍被各浏览器厂商所支持(如今你可以使用Object.getPrototypeOf(obj)方法访问它)。它在chrome浏览器中用obj.__proto__访问。

let A = function () {};

let a = new A();
let b = new String('b');
let c = 1;
let d = [];
let e = {};

console.log(a.__proto__ === A.prototype); // true
console.log(b.__proto__ === String.prototype); // true
console.log(c.__proto__ === Number.prototype); // true
console.log(d.__proto__ === Array.prototype); // true
console.log(e.__proto__ === Object.prototype); // true

prototype

前面说到,对象的__proto__属性指向该对象的构造函数的prototype属性,而继承通过__proto__属性实现。之所以如此别扭,是因为js没有类的概念。
那么,我们如何通过构造函数去定义那些我们需要继承给实例对象的属性和方法呢?prototype属性就是负责完成此事的。
我们给构造函数定义一个prototype属性,让它指向实例对象的原型对象。这样一来,我们在定义构造函数的时候,只要给构造函数的prototype属性添加属性或方法,它所生成的实例对象也就可以通过原型对象访问到了。


下方是一张原型链图,他能帮助你更好的理解什么是原型链。