深入理解JavaScript系列(二): 原型、原型链与继承

1.原型

1.什么是原型

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 

如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person(){
    }
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    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"

    alert(person1.sayName == person2.sayName);  //true

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person。 



alert(Object.getPrototypeOf(person1) == Person.prototype); //true 

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

使用  delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性 :

var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name);  <span style="font-family: Arial, Helvetica, sans-serif;">//"Greg"——来自实例</span>
alert(person2.name);<span style="font-family: Arial, Helvetica, sans-serif;">//"Nicholas"——来自原型</span>
delete person1.name;
alert(person1.name); //"Nicholas"——来自原型 


//in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。 
alert("name" in person1);  //true


2.用对象字面量来定义原型

除了上文的方式,我们还可以用对象字面量来定义原型:

function Person(){
}
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
}; 
但此时 constructor 属性不再指向 Person了。 前面曾经介绍过,每创建一个函数,就会同时创建它的  prototype  对象,这个对象也会自动获得  constructor  属性。而我们在这里使用的语法,本质上完全重写了默认的  prototype  对象,因此  constructor  属性也就变成了新对象的  constructor  属性(指向  Object  构造函数),不再指向  Person  函数。此时,尽管  instanceof 操作符还能返回正确的结果,但通过  constructor 已经无法确定对象的类型了,如下所示。

var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

2.原型链

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。 

一个简单的有漏洞的例子:(怎么修补会在下文详细讲述)

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());//true 

如图:



调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。 


3.继承

1.ES5实现方式

举例子,一步一步推出最佳实践:
// Parent Class
function Developer(name) {
    this.name = name || 'Frank';
}
Developer.prototype.code = function() {
    console.log('I am ' + this.name + ', and I am coding!');
};
 
// Child Class
function JSDeveloper(name) {}

期望效果:
var dev = new JSDeveloper('Calvin');
dev.code();
dev.name; // Calvin
dev.constructor.name === 'JSDeveloper'; // true


1.直接使用原型链

<pre name="code" class="javascript">JSDeveloper.prototype = new Developer();
var dev = new JSDeveloper();
dev.code(); // I am Frank, and I am coding!
 
// But
var dev = new JSDeveloper('Calvin');
dev.code(); // I am Frank, and I am coding!
dev.hasOwnProperty('name'); // false
dev.constructor.name; // Developer
 
 

有两个问题: 
1. 在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性
2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数

2.使用构造函数

function JSDeveloper(name) {
 <strong>Developer.apply(this, arguments);</strong>
}
var dev = new JSDeveloper();
dev.hasOwnProperty('name'); // true
dev.constructor.name; // JSDeveloper
 
// But
dev.code(); // Error, does not get methods from parent class

代码中加粗的那一行代码“借调”了超类型的构造函数。通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的JSDeveloper实例的环境下调用了Developer 构造函数。这样一来,就会在新 JSDeveloper 对象上执行Developer()函数中定义的所有对象初始化代码。结果,JSDeveloper的每个实例就都会具有自己的name属性的副本了。 

但是问题也很明显,JSDeveloper的实例没有Developer中的方法。

3.原型+构造函数

function JSDeveloper(name) {
    Developer.apply(this, arguments);
}
JSDeveloper.prototype = new Developer();
 
 
var dev = new JSDeveloper('Calvin');
dev.code(); // I am Calvin, and I am coding!
 
// But
console.log(dev.constructor.name); // Developer
console.log(dev instanceof JSDeveloper); //true
console.log(dev instanceof Developer); //true
console.log(JSDeveloper.prototype.isPrototypeOf(dev)); //true
console.log(Developer.prototype.isPrototypeOf(dev));// true
delete dev.name;
dev.code(); // I am Frank, and I am coding! (Get attributes from prototype)

4.更好的方式

function JSDeveloper(name) {
    Developer.apply(this, arguments);
}
<strong>JSDeveloper.prototype = Developer.prototype;
JSDeveloper.prototype.constructor = JSDeveloper;</strong>
 
var dev = new JSDeveloper('Calvin');
dev.name; // Calvin
delete dev.name;
dev.name; // undefined
dev.constructor.name; // JSDeveloper
 
// But
JSDeveloper.prototype.jsFormat = function(){
    console.log('js format');
};
var nonJSDev = new Developer('LuLu');
nonJSDev.jsFormat();

新的问题: 修改子类的原型时,父类的也跟着被修改了!

5.最佳实践

function JSDeveloper(name) {
    Developer.apply(this, arguments);
}
 
var F = function(){};
F.prototype = Developer.prototype;
JSDeveloper.prototype = new F();
 
JSDeveloper.prototype.constructor = JSDeveloper;

这种方式也叫寄生组合式继承,我们可以把继承封装成一个函数(但是,在代码中不推荐自己封装,可以直接用第三方类库!):
function object(o){
        function F(){}
	F.prototype = o;
        return new F(); 
} 
function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); 
    prototype.constructor = subType;		 
    subType.prototype = prototype;		
}

2.ES6实现方式

class Pony{
    constructor(color) {
        this.color = color;
    }
    tellColor() {
        console.log(`I am ${this.color}`);
    }
}
 
class SplunkPony extends Pony{
    constructor(color, age) {
        super(color);
        this.age = age;
    }
    tellAge() {
        console.log('I am ' + this.age);
    }
}
 
var pony = new SplunkPony('black', 20);
pony instanceof Pony; //true
pony.tellColor();
pony.tellAge();

3.Object.create

var dev = Object.create(Developer.prototype, {
    name : {
        value: 'Leon'
    }
});
 
dev.code(); // I am Leon, and I am coding!
dev.name; // Leon
dev.hasOwnProperty('name'); // true
dev.hasOwnProperty('code'); // false

这从严格意义上来讲,不是“类”与“类”的继承。

关于Object.create 和 new SomeFunction()的区别,之后我会另写一文来阐述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值