重学JavaScript(1) - 原型、原型链

137 阅读4分钟

1994年,网景公司推出了第一款比较成熟的网络浏览器Navigator,但是当时还没有一款网络脚本语言和浏览器进行交互,时任公司工程师的Brendan Eich被委以重任,设计一种新语言。Brendan Eich认为新语言只是为了做一些简单的交互,同时受到当时风靡的面向对象编程的影响,所以JavaScript最终被设计成一款简洁的“面向对象编程语言”。

构造函数

JavaScript中所有的数据类型都是对象,为了能够将所有对象串在一起,必须要有一种“继承”机制,但是又不想像Java的类那么复杂,所以直接用了类里的构造函数来实现原型到实例的继承。

Java中的实例化

Foo foo = new Foo()

JavaScript中的实例化

// 首先定义一个构造函数Dog,表示狗对象的原型
function Dog(name){
    this.name = name
}
​
// 实例化一个狗对象
var dog1 = new Dog('旺财')
console.log('狗的名字:', dog1.name)
// 狗的名字:旺财

prototype

使用构造函数,有一个缺点,就是无法共享属性和方法。

// 定义一个构造函数Dog,表示狗对象的原型
function Dog(name){
    this.name = name;
    this.species = '犬科';
}
​
//实例化两个狗对象
var dog1 = new Dog('旺旺')
var dog2 = new Dog('旺财')
​
//修改其中一个对象的属性
console.log('旺旺的物种',dog1.species)  // 犬科
dog1.species = '猫科'
console.log('旺旺的物种',dog1.species)  // 猫科
console.log('旺财的物种',dog2.species)  // 犬科

为了解决该问题,Brendan Eich决定为构造函数设置一个prototype属性。

这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

// 定义一个构造函数Dog,表示狗对象的原型
function Dog(name){
    this.name = name;
}
​
Dog.prototype.species = '犬科'//实例化两个狗对象
var dog1 = new Dog('旺旺')
var dog2 = new Dog('旺财')
​
//修改其中一个对象的属性
console.log('旺旺的物种',dog1.species)  // 犬科
Dog1.species = '猫科'
console.log('旺旺的物种',dog1.species)  // 猫科
console.log('旺财的物种',dog2.species)  // 猫科

注意:不要随意修改prototype对象属性和方法,其所有实例对象都会受到影响!!!

继承

备注:只说明构造函数的继承最常用方法,关于非构造函数的继承不在这里说明)

首先定义两个构造函数,表示动物对象和狗对象

function Animal(){
    this.species = "动物";
}
​
function Dog(name){
    this.name = name;
}

prototype 方式

将“狗”的prototype对象指向Animal实例,这样Dog的实例就可以继承Animal了。

Dog.prototype = new Animal();

这样会把Dog的prototype对象重新赋值。但是这样有一个问题:prototype对象里面有一个constructor属性,它指向构造函数 ,也会被修改

没有执行上面语句之前,Dog.prototype指向Dog;执行之后,Dog.prototype指向Animal。

console.log(Dog.prototype.constructor == Animal); //true

更加严重的是,每一个实例对象也有一个constructor属性,默认调用prototype对象的constructor属性。

var dog1 = new Dog();
console.log(dog1.constructor == Dog.prototype.constructor); //true
console.log(dog1.constructor == Animal); //true

这样对导致原型链的混乱,因此我们手动纠正过来。

Dog.prototype.constructor == Dog;

完整步骤如下:

Dog.prototype = new Animal();
Dog.prototype.constructor == Dog;
var dog1 = new Dog('旺财');
console.log(dog1.species) // 动物

注意!!!

如果要替换prototype对象,一定要将prototype对象的constructor属性指向原来的构造函数

 o.prototype = {};
 o.prototype.constructor = o;

原型链

当要在一个实例对象中找属性时,如果找不到就会去该实例对应构造函数的原型对象中去找,如果还找不多,就会去原型对象的原型对象中找,一直到最顶层为止,最顶层就是Object对象。

// 首先定义一个构造函数Dog,表示狗对象的原型
function Dog(name){
	this.name = name
}

//实例化狗对象
var dog1 = new Dog('旺财')
dog1.toString  // f toString(){[native code]}

dog1实例对象先找自己有没有toString函数,发现没有,就找到Dog构造函数对应的原型对象,发现也没有。因为原型对象也是一个对象,是通过构造函数Object生成的,Object的原型对象中有toString方法。

console.log(Dog.prototype)  // {constructor: ƒ}

console.log(Dog.prototype._proto == Object.prototype)  // true

注:对象都有一个隐形的原型_proto_属性,该属性指向对应构造函数的prototype属性

原型链.drawio.png

参考

  1. Javascript继承机制的设计思想
  1. Javascript面向对象编程(二):构造函数的继承