Javascript 笔记:原型链继承

这篇文章记录 Javascript 中原型链继承的相关概念。

概念

普通对象与函数对象

JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。下面举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function
console.log(typeof Function); //function

console.log(typeof f1); //function
console.log(typeof f2); //function
console.log(typeof f3); //function

console.log(typeof o1); //object
console.log(typeof o2); //object
console.log(typeof o3); //object

在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

构造函数

构造函数是一种用于创建和初始化类对象实例的特殊方法。代码如下所示:

1
2
3
4
5
6
7
8
9
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() { alert(this.name) }
}

var person1 = new Person('Nicholas', 28, 'Software Engineer');
var person2 = new Person('Greg', 23, 'Doctor');

要创建 Person 的新实例,需要经过四个步骤:

  1. 创建 new 一个新对象;
  2. 将构造函数的 this 作用域赋值给新对象;
  3. 执行构造函数中的代码;
  4. 返回新对象。

person1 和 person2 分别保存着 Person 的不同实例,这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person:

1
2
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

原型对象

在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype 属性,这个属性指向函数的原型对象。

使用原型对象的好处就是可以让所有对象共享他所包含的属性或方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 28;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
alert(this.name);
}

var person1 = new Person();
person1.sayName(); // 'Nicholas'

var person2 = new Person();
person2.sayName(); // 'Nicholas'

console.log(person1.sayName == person2.sayName); //true

默认情况下,所有原型对象都会自动获得一个 constructor(构造函数) 属性,通过这个构造函数,我们还可以继续为原型对象添加属性。同时,由于实例的 constructor 指向构造函数,可以得出以下两个结论:

1
2
console.log(Person.prototype.constructor == Person); //true
console.log(person.constructor == Person); //true

__proto__属性

当通过构造函数创建对象时,所创建的对象中都会有一个隐藏的属性,该属性指向构造函数的的原型对象,我们可以通过 __proto__ 来访问该属性。

1
2
var person1 = new Person();
console.log(person1.__proto__ == Person.prototype); //true

原型链

原型链的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。原型链的核心是继承。

它是由 __proto__ 串联起来的链状结构。每一个函数对象都有一个 prototype 属性,也有一个 __proto__,每一普通对象都只有一个 __proto__ 属性;一般我们把属性放在构造函数中,把方法放在prototype 下面,对象的 __proto__ 指向构造函数的 prototype,原型链的终点为 Object.prototype,再往上 Object.prototype.__proto__=null

说明: 构造函数是一个函数对象,它也会包含 __proto__ 属性,该属性指向 Function 函数的原型对象。

想象一下,既然每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,那么,我们让原型对象等于另一个类型的实例会怎样?

显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Father(){ 
this.property = true;
}
Father.prototype.getFaValue = function(){
return this.property;
};
function Child(){
this.chProperty = false;
}

//继承了 Father
Child.prototype = new Father();
Child.prototype.getChValue = function (){
return this.chProperty;
};

let instance = new Child();
console.log(instance.getFaValue()); //true

内置构造函数

可以使用如下创建一个对象:

1
var obj = {}

它等同于下面这样:

1
var obj = new Object()

obj 是构造函数 Object 的一个实例。所以:

1
2
obj.constructor === Object
obj.__proto__ === Object.prototype

新对象 obj 是使用 new 操作符后跟一个构造函数来创建的。构造函数 Object 本身就是一个函数(也是一个函数对象),它和上面的构造函数 Person 差不多。

同理,可以创建对象的构造函数不仅仅有 Object,也可以是 ArrayDateFunction等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let b = new Array();
b.constructor === Array;
b.__proto__ === Array.prototype;

let c = new Date();
c.constructor === Date;
c.__proto__ === Date.prototype;

let d = new Function();
d.constructor === Function;
d.__proto__ === Function.prototype;


console.log(typeOf Object) // function
console.log(typeOf Function) // function
console.log(typeOf Array) // function
console.log(typeOf Date) // function
console.log(typeOf Number) // function
console.log(typeOf String) // function
console.log(typeOf Boolean) // function

所有的构造函数的实例的 __proto__ 都指向构造函数的 prototype,所以又可得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype // true
Object.constructor == Function // true
Object.__proto__.__proto__ === Object.prototype // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype // true
Date.constructor == Function //true

那么 Function.prototype__proto__是谁呢?

1
console.log(Function.prototype.__proto__ === Object.prototype) // true

函数本身也是对象,作为构造函数或者实例同时也继承了 Object.prototype 上的所有方法:toStringvalueOfhasOwnProperty等等。

那么 Object.prototype.__proto__ 指向谁?

1
Object.prototype.__proto__ === null // true

原型链的尽头,为 null

最终的关系如下图所示: js-object-layout

原型链继承

原型链是实现继承的重要方式。通过将子类的原型设置为父类的实例,可以实现子类继承父类的属性和方法。

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Animal(name) {
this.name = name;
}

Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};

function Dog(name) {
Animal.call(this, name); // 继承属性
}

// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
console.log(`${this.name} says Woof!`);
};

const dog = new Dog('Buddy');
dog.eat(); // 输出: Buddy is eating.
dog.bark(); // 输出: Buddy says Woof!

Object.create(Animal.prototype) 创建了一个以 Animal.prototype 为原型对象的对象,并作为 Dog 对象的原型对象,从而 Dog 对象可以继承 Animal原型对象中的方法,并设置原型对象的构造函数为 Dog 函数。


参考:


1. 最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)

2. 详解JS原型与原型链(长文详解

3. 解释JavaScript中的原型链

4. js中的原型链,6种继承方式及其案例