玩转JavaScript OOP[3]——彻底理解继承和原型链

概述

首先,我们简单描述一下继承的概念:当一个类和另一个类构成"is a kind of"关系时,这两个类就构成了继承关系。继承关系的双方分别是子类和基类,子类可以重用基类中的属性和方法。

上一篇我们介绍了通过构造函数和原型可以实现JavaScript中的“类”,由于构造函数和原型是对象,所以JavaScript的“类”本质上也是对象。这一篇我们将介绍JavaScript中的一个重要概念原型链,以及如何经原型链实现JavaScript中的继承。

C#的继承

C#可以显式地定义class,也可以让一个class直接继承另外一个class,下面这段代码就是一个简单的继承。

public class Person
{

    public string Name { get { return "keepfool"; } }

    public string SayHello()
    {
        return "Hello, I am " + this.Name;
    }
}

public class Employee : Person
{

}

我们在Person类中定义了Name属性和SayHello()方法,Employee类中什么都没定义。
由于Employee类是继承Person类的,所以Employee类的实例能够使用Person类的属性和方法。

Employee emp = new Employee();
Console.WriteLine(emp.Name);
Console.WriteLine(emp.SayHello());

Console.WriteLine("emp{0}是Person类的实例", emp is Person ? "" : "不");

使用Employee类的构造函数创建emp对象后,emp对象可以使用Name属性和SayHello()方法。
emp对象既是Employee类的实例,也是Person类的实例。

这是C#的继承语法,JavaScript则没有提供这样的语法,现在我们来探讨如何在JavaScript中实现继承。

JavaScript原型继承

类似的,我们先在JavaScript中定义两个类Person和Employee。

function Person() {
	this.name = ‘keefool‘;
	this.sayHello = function() {
		return ‘Hello, I am ‘ + this.name;
	}
}

function Employee(email) {
	this.email = email;
}

var person = new Person();
var emp = new Employee(‘[email protected]‘);

由于Employee和Person还不是继承关系,emp目前只是Employee类的实例,不是Person类的实例,所以它还不能访问Person类的name属性和sayHello()方法。

使用instanceof操作符同样可以确定emp是Employee类的实例,而不是Person类的实例。

实现继承

我们先自问一下,实现继承的目的是什么?当然是子类重用基类的属性和方法。
在这个示例里面,确切的说是让Employee类的实例能够使用Person类的name属性和sayHello()方法。

那么JavaScript是如何实现继承的呢?
这个答案有很多种,这里我先只介绍比较常见的一种——通过原型实现继承。

回顾上一篇的内容,我们知道Person()构造函数是有一个prototype属性的,该属性是一个对象。
在Person.prototype定义的属性和方法是可以被Person的实例使用的。

既然如此,我们当然也可以在Employee.prototype上定义一些属性和方法。
看到这里,知道怎么做了吗?直接让Employee.prototype指向一个Person类的实例。

function Person() {
	this.name = ‘keefool‘;
	this.sayHello = function() {
		return ‘Hello, I am ‘ + this.name;
	}
}

function Employee(email) {
	this.email = email;
}

var person = new Person();

Employee.prototype = person;

var emp = new Employee(‘[email protected]‘);

现在我们就可以访问emp.name和emp.sayHello()方法了。

在Chrome控制台,使用instanceof操作符,可以看到emp对象现在已经是Person类的实例了。

这是如何实现的?

因为Employee.prototype是一个引用类型,它指向Person类的一个实例person,而person持有name属性和sayHello()方法。
这使得我们使用Employee.prototype就像使用Person的实例一样,访问emp.name和emp.sayHello()方法实际上访问的是person.name和person.sayHello()方法。

如果你对这段代码还是有所疑惑,你还可以这样理解:

var person = new Person();
Employee.prototype.name = person.name;
Employee.prototype.sayHello = person.sayHello;

当然了,由于person对象在后面完全没有用到,以上这两行代码可以合并为一行。

function Person() {
	this.name = ‘keefool‘;
	this.sayHello = function() {
		return ‘Hello, I am ‘ + this.name;
	}
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();

var emp = new Employee(‘[email protected]‘);

下面这幅图概括了这个过程:

请注意,name和sayHello()方法不是Employee类的自有属性和方法,它来源于Employee.prototype。

原型继承的本质

JavaScript的原型继承是基于对象的,即一个类的原型对象指向另外一个类的实例。

结合上面的描述,因为Employee.prototype指向了Person类的一个实例,所以我们可以说Employee类继承了Person类。

另外,即使我们没有显式地设置Employee.prototype,JavaScript也会默认地帮我们做一件事情:

Employee.prototype = {};

{}表示是一个普通的Object类的实例,这意味着Employee也是Object类的实例。

再说constructor

对象的constructor属性

前面讲过,每个对象都有constructor属性,constructor属性应该指向对象的构造函数,例如Person实例的constructor属性是指向Person()构造函数的。

var person = new Person();

在未设置Employee.prototype时,emp对象的构造函数原本也是指向Employee()构造函数的。

当设置了Employee.prototype = new Person();时,emp对象的构造函数却指向了Person()构造函数。

无形之中,emp.constructor被改写了。
emp对象看起来不像是Employee()构造函数创建的,而是Person()构造函数创建的。
你是否也有这样的困惑?

这不是我们期望的,我们希望emp对象看起来也是由Employee()构造函数创建的,即emp.constructor应该是指向Employee()构造函数的。
要解决这个问题,我们先弄清楚对象的constructor属性是从哪儿来的,知道它是从哪儿来的就知道为什么emp.constructor被改写了。

constructor属性的来源

当我们没有改写构造函数的原型对象时,constructor属性是构造函数原型对象的自有属性。
例如:Person()构造函数的原型没有改写,constructor是Person.prototype的自有属性。

当我们改写了构造函数后,constructor属性就不是构造函数原型对象的自有属性了。
例如:Employee()构造函数的原型被改写后,constructor就不是Person.prototype的自有属性了。

Employee.prototype的constructor属性是指向Person()构造函数的。

这说明:当对象被创建时,对象本身没有constructor属性,而是来源于创建对象的构造函数的原型对象。

即当我们访问emp.constructor时,实际访问的是Employee.prototype.constructor,Employee.prototype.constructor实际引用的是Person()构造函数,person.constructor引用是Person()构造函数,Person()构造函数实际上是Person.prototype.constructor。

这个关系有点乱,我们可以用以下式子来表示这个关系:

emp.constructor = person.constructor = Employee.prototype.constructor = Person = Person.prototype.constructor

它们最终都指向Person.prototype.constructor!

改写原型对象的constructor

弄清楚了对象的constructor属性的来弄去脉,上述问题就好解决了。
解决办法就是让Employee.prototype.constructor指向Employee()构造函数。

function Person() {
	this.name = ‘keefool‘;
	this.sayHello = function() {
		return ‘Hello, I am ‘ + this.name;
	}
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee(‘[email protected]‘);

如果你还是不能理解关键的这行代码:

Employee.prototype.constructor = Employee;

你可以尝试从C#的角度去理解,在C#中Employee类的实例肯定是由Employee类的构造函数创建出来的。

原型链

__proto__属性

我们已经知道,定义函数时,函数就有了prototype属性,该属性指向一个对象。
prototype属性指向的对象是共享的,这有点像C#中的静态属性。
站在C#的角度讲,实例是不能够访问类的静态属性的。
那么在JavaScript中,为什么对象能够访问到prototype中的属性和方法的呢?

当对象由new构造函数创建时,对象会自带一个__proto__属性,这个属性是由JavaScript分配的。
这个属性指向的正是构造函数的原型。

例如:当emp对象被创建时,JavaScript自动地为emp对象分配了一个__proto__属性,这个属性是指向Employee.prototype的。

在Chrome中看一看emp.__proto__包含什么内容

首先,▼Person {name: "keepfool"}表示emp.__proto__是一个Person对象,因为Employee.prototype确实指向一个Person对象。
其次,我们把emp.__proto__的属性分为3个部分来看。

1. 第1部分:name属性和sayHello()方法,它们两个来源于Person对象。我们称这一部分为继承的属性和方法。

2. 第2部分:constructor属性,因为我们重写过Employee()构造函数的原型对象的constructor属性,即Employee.prototype.constructor = Employee,所以constructor是指向Employee()构造函数的。

3. 第3部分:__proto__它指向一个Object,Person类是Employee类的父类,那么谁是Person类的父类呢?——Object类。

对象的__proto__属性就像一个秘密链接,它指向了构造函数的原型对象。

什么是原型链

我们注意到第3部分的内容仍然是一个__proto__属性,我们展开它看个究竟吧。

再往下看,还有两层__proto__。

emp.__proto__.__proto__:从?constructor:function Person()可以看出它是Person()构造函数的原型。

这也说明了Person()构造函数在创建时,JavaScript自动地为Person()构造函数分配了prototype属性。

Person.prototype包含两部分内容:Person()构造函数,以及一个__proto__属性,这个属性指向内置的Object对象,即emp__proto__.__proto__.__proto__指向的是Object对象。

我们将这一系列的__proto__称之为原型链。

原型链也表示了JavaScript的继承关系,Employee类继承了Person类,Person类继承了Object类。

结合以上这两幅图我们也不难看出,最底端的原型链的是null,因为最底端的Object对象没有__proto__属性了。

理解原型链

这就可以解释为什么emp对象能够访问Employee.prototype的name属性和sayHello()方法了。
以访问sayHello()方法为例,我们用几个慢镜头来阐述:

  1. 通过emp对象访问sayHello()方法,即emp.sayHello()

  2. JavaScript引擎发现sayHello()方法不是emp对象的自有属性,即sayHello()方法不是定义在Employee类中的
  3. 于是JavaScript引擎去找emp的上一级原型链,即emp.__proto__
  4. 由于emp.__proto__和Employee.prototype持有相同的引用,它们都指向的是一个Person对象
  5. 于是JavaScript去Person对象中寻找sayHello()方法,果然被JavaScript找到了,于是就使用了这个方法。

下图阐述了这几个镜头,注意我们在代码中执行①③④这3步时,JavaScript在背后都帮我们做了哪些事情。

值得注意的是,上面在访问到sayHello()方法时,实际访问的是emp.__proto__.sayHello()

将方法提升到原型对象

上一篇有提到过,Person类的sayHello()方法放到它的原型对象中更合适,这样所有的Person实例共享一个sayHelo()方法副本,如果我们把这个方法提到原型对象会发生什么?

function Person() {
	this.name = ‘keefool‘;
}

Person.prototype.sayHello = function(){
	return ‘Hello, I am ‘ + this.name;
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee(‘[email protected]‘);

可以看到sayHello()方法的路径是:emp.__proto__.__proto__.sayHello(),比直接定义在Person()构造函数中多了一层。

这样看来将方法定义在原型对象中并不是绝对的好,会使得JavaScript遍历较多层数的原型链,这也会有一些性能上的损失。

原型链示例

为了加强对原型链的理解,我们来做个简单的示例吧。

从以上的图示中,可以知道toString()方法是属于Object,我们以toString()方法来讲解这个示例。

在Chrome控制台输入emp.toString(),我们得到的结果是"[object Object]"
通过原型链和hasOwnProperty()方法,可以看出找到toString()方法经过了3层原型链,即emp.__proto__.__proto__.__proto__,上面的图已经说明了它就是Object对象。

emp.toString()输出"[object Object]"没有什么意义,现在我们在Person()构造函数的原型上定义一个toString()方法。

function Person() {
	this.name = ‘keefool‘;
}

Person.prototype.sayHello = function(){
	return ‘Hello, I am ‘ + this.name;
}

Person.prototype.toString = function(){
	return ‘[‘ + this.name + ‘]‘;
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee(‘[email protected]‘);

通过原型链和hasOwnProperty()方法,可以看出找到toString()方法经过了2层原型链,即emp.__proto__.__proto__。
emp.__proto__.__proto__就是Person()构造函数的原型对象,即Person.prototype。

这个例子也是一个简单的重写示例,尽管Object提供了toString()方法,但它的子类Person重写了toString()方法,所emp最终调用的是Person的toString()方法。

时间: 2024-10-12 04:46:31

玩转JavaScript OOP[3]——彻底理解继承和原型链的相关文章

JavaScript难点系列(六):原型链与继承

类和构造函数 JS中使用构造函数来定义类: function Range(from, to) { this.from = from this.to = to } Range.prototype.includes = function(x) { return this,from <= x && x <= this.to } Range.prototype.toString = function() { return this.from + '...' + this.to } va

关于JavaScript的原型继承与原型链

在讨论原型继承之前,先回顾一下关于创建自定义类型的方式,这里推荐将构造函数和原型模式组合使用,通过构造函数来定义实例自己的属性,再通过原型来定义公共的方法和属性. 这样一来,每个实例都有自己的实例属性副本,又能共享同一个方法,这样的好处就是可以极大的节省内存空间.同时还可以向构造函数传递参数,十分的方便. 这里还要再讲一下两种特色的构造函数模式: 1.寄生构造函数从形式上来看,这种模式和工厂模式并无区别: function Person(name, age, job){var o = new O

继承与原型链

对于那些熟悉基于类的面向对象语言(Java 或者 C++)的开发者来说,JavaScript 的语法是比较怪异的,这是由于 JavaScript 是一门动态语言,而且它没有类的概念(虽然 class 是个保留字,不能作为变量名来使用). 继承方面,JavaScript 中的每个对象都有一个内部私有的链接指向另一个对象,这个对象就是原对象的原型.这个原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型).这种一级一级的链结构就称为原型链. 虽然这通常会被称作 JavaScrip

js继承与原型链

对于那些熟悉基于类的面向对象语言(Java 或者 C++)的开发者来说,JavaScript 的语法是比较怪异的,这是由于 JavaScript 是一门动态语言,而且它没有类的概念(虽然 class 是个保留字,不能作为变量名来使用). 继承方面,JavaScript 中的每个对象都有一个内部私有的链接指向另一个对象,这个对象就是原对象的原型.这个原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型).这种一级一级的链结构就称为原型链. 虽然这通常会被称作 JavaScrip

继承和原型链

继承方面,JavaScript 中的每个对象都有一个内部私有的链接指向另一个对象,这个对象就是原对象的原型.这个原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型).这种一级一级的链结构就称为原型链. 虽然这通常会被称作 JavaScript 的弱点之一,实际上这种原型继承的模型要比经典的继承模型还要强大.虽然在原型模型上构建一个经典模型是相当琐碎的,但如果采取其他方式实现则会更加困难. 基于原型链的继承 继承属性 JavaScript 对象有两种不同的属性,一种是对象自身

玩转JavaScript OOP[4]&mdash;&mdash;实现继承的12种套路

概述 在之前的文章中,我们借助构造函数实现了"类",然后结合原型对象实现了"继承",并了解了JavaScript中原型链的概念. 理解这些内容,有助于我们更深入地进行JavaScript面向对象编程. 由于JavaScript是一门基于对象和原型的弱语言,灵活度非常高,这使得JavaScript有各种套路去实现继承.本篇文章将逐一介绍实现继承的12种套路,它们可以适用于不同的场景,总一种套路适合你. (亲:文章有点长,请点击右侧的「显示文章目录」按钮,以便导航和阅读

理解JavaScript:继承与原型链

本文翻译自https://wildlyinaccurate.com/understanding-javascript-inheritance-and-the-prototype-chain Javascript里的一切几乎都是对象,每一个对象都有一个链接到其他对象的内部属性,我们称之为prototype (原型).原型对象自己也有自己的原型对象,以此类推,这时候原型链就出来了.如果你追踪原型链,你最终会到达原型为 null的内核 Object,这是原型链的终点. 原型链的作用是什么呢?当我们访问

玩转JavaScript OOP[1]&mdash;&mdash;复杂类型

概述 在JavaScript中,我们可以使用函数.数组.对象,以及日期.正则等一些内置类型的实例,它们都是复杂类型的表现.从本质上讲,这些复杂类型都是Object类型.本篇主要的内容有3点:函数.数组和对象. 函数 函数是JavaScript的一大重点,它非常的灵活.不像C#这种强类型语言,可以显式地声明"class",JavaScript没有"class"的概念,但借助函数我们可以实现"class"的概念.类和对象是面向对象编程的基础,所以掌握

JavaScript中的继承与原型链

先看一个例子 function User(){} var u1 = new User(); console.log(u1.prototype);// undefined 使用对象实例无法访问到prototype console.log(User.prototype);//{},使用构造函数名访问prototype console.log(u1.__proto__);//{},使用对象实例访问prototype的指针 这个是 __proto__ 和prototype最基本的区别:说明构造的对象无p