在JavaScript
中, 继承机制的基础是原型,(包括内部原型__proto__
和prototype
)。当访问一个对象的属性时,JavaScript
引擎是这么搜索的:如果在本对象中找不到一个属性时,就会去其原型对象中找,如果原型对象中还没找到的话,就去到原型对象中的原型中去找,直到顶级父类Object
对象为止。
纸上得来终觉浅,绝知此事要躬行。所以,无论是什么知识,如果不在实践中去进行学习,最终必然是无法深入理解。
接下来,就进入本集的重点部分,用上集学习到的知识实现原型继承,姑且将其称之为最佳实战。
一、需求说明
- 定义三个类
Shape
、TwoDShape
和、Triangle
- 实现 :
TwoDShape
继承Shape
、Triangle
继承TwoDShape
二、普通实现
下面代码部分展示了一种我们最常见的写法。通过new
父类方式类实现原型继承。
function Shape(){
this.name = "Shape";
this.toString = function () {
return this.name;
}
}
function TwoDShape(){
this.nam = "2D Shape";
}
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
function Triangle(side, height){
this.name = "Triangle";
this.side = side;
this.height = height;
this.getArea = function () {
return this.side * this.height / 2;
}
}
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
var my = new Triangle(5, 10);
console.log(my.toString()); // Triangle
console.log(my.__proto__); // TwoDShape {nam: '2D Shape', constructor: ƒ}
console.log(my.constructor);// Triangle(side,height)
2.1 代码分析
JavaScript
引擎在my.toString()
被调用究竟做了哪些事情?
在上面的示例中,过程大概是这样的:
- 他首先会遍历
my
对象中的所有属性,但是么有找到一个叫做toString()
的方法 - 接下来,再去查看
my.__proto__
所指向的对象,该对象是在继承关系构建的过程中由new TwoDShape()
所创建的实体,显然JavaScript
引擎在遍历TwoDShape
实体的过程中依然不会找到toString()
方法,然后它又会检查该实体__proto__
属性,这个时候该__proto__
属性所指向的实体是由new Shape()
创建的。 - 终于,在
new Shape()
所创建的实体中找到了toString()
方法 - 最后,该方法就会在
my
对象中被调用,并且this
指向了my
。
所以本次的总共进行了3
次搜索
- 第1次是在本对象中找,
- 第2次是在其原型对象即TwoDShape对象中找,
- 第3次是在TwoDShape对象的原型对象Shape实例中找,最终找到了。
所以可以推测new
关键字做的复制的工作,并不是真正的复制原型对象上的属性,而只是建立指针链接指向它。我们在查找的时候还是要一层一层往上找。
如果我们向my
对象询问:“您的构造器函数是哪一个?”,它应该是能够给出正确答案的,因为我们在构建继承关系的时候已经对相关的constructor
属性进行了重置。如果不重置的话就会指向了TwoDShape()
2.2 如何回溯原型
- 因为我们重置了
constructor
属性my.constructor.prototype
(是my
对象的原型)
这样我们就么有办法通过my.constructor.prototype.constructor.prototype…
回溯到原型。 - 如果没有进行constructor属性的修正,只能通过对象内部的原型链进行回溯:
my.__proto__.__proto__.__proto__....
三、将共享属性迁移到原型中
当我们用某一个构造器创建对象时,其属性就被添加到this
中去,这会使某些不能通过实体改变的属性出现一些效率低下的情况。Shape()
构造器是这样定义的
function Shape(){
this.name = “shape”;
}
这种实现意味着当我们用new Shape()
新建对象时,每个实例都会有一个全新的name
属性,并在内存中拥有自己独立的存储空间。而事实上,我们也可以将name
属性添加到所有实例所共享的的原型对象中去:
function Shape(){}
Shape.prototype.name = “shape”;
这样一来,每当我们在用new Shape()
新建对象时,新对象中就不在含有自己的name
属性了,而是被添加到了该对象的原型中。虽然这样做通常会更有效率,但这也是只针对对象实体中的不可变的属性而言,另外这种方式也同样适合对象中的共享性方法。
注意点:我们在对原型对象扩展之前,先要完成相关的继承关系构建。
3.1 代码分析
那么此时会使得
my.toString()
方法查找有什么不同么?
- 首先,
JavaScript
引擎查看my
对象中有么有toString()
方法,自然是找不到, - 于是它就去搜索该对象的原型属性。此时该原型属性是
TwoDShape
的一个实例对象,也找不到, - 在去这个实例对象的原型中去找,就在
TwoDShape.prototype
中找,而TwoDShape.prototype
这时是Shape
的一个实例,自然也找不到, - 于是就去
Shape
的原型中找,最终找到了。
总计要4
步的搜寻才能找到
- 在本对象中找
- 在TwoDShape实例对象中找
- 在Shape实例中找
- 在Shape原型中找,找到了
3.2 实现代码
function Shape(){}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {
return this.name;
}
function TwoDShape(){}
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = "2D Shape";
function Triangle(side, height){
this.side = side;
this.height = height;
}
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
var my = new Triangle(5, 10);
console.log(my.getArea()); //25
console.log(my.toString()); //Triangle
//用hasOwnProperty()方法来明确对象自身属性和原型链属性
console.log(my.hasOwnProperty('side')); //true
console.log(my.hasOwnProperty('name')); //false
//用isPrototypeOf()方法来判断一个原型对象是不是实例的原型
console.log(Shape.prototype.isPrototypeOf(my)); //true
console.log(TwoDShape.prototype.isPrototypeOf(my)); //true
console.log(Triangle.prototype.isPrototypeOf(my)); //true
//用instanceof()方法来,用来测试一个对象是不是由某个指定的构造器函数所创建的。
console.log(my instanceof Shape); //true
console.log(my instanceof TwoDShape); //true
console.log(my instanceof Triangle); //true
四、prototype方式
正如上面所说,出于效率考虑,我们应该尽可能将一些可重用的属性和方法添加到原型中去,如果形成了一个好习惯,我们仅仅依靠原型就能完成继承关系构建了。而不需要使用new Shape()
等实例对象。
由于原型中所有的代码都是可重用的,这意味着继承自Shape.prototype
比继承自new Shape()
所创建的实体要好的多。毕竟,new Shape()
方式会将Shape
属性设定为对象自身属性,这样的代码是不可重用的(因而要将其设置在原型中)。于是我们采取以下方式对效率做一些改进:
- 不要单独为继承关系创建新对象
- 尽量减少运行时方法搜索,例如toString()
4.1 代码分析
下面就是我们优化后的代码
这样做会令my.toString()
方法查找有什么不同么?文章来源:https://www.toymoban.com/news/detail-551848.html
- 首先,Js引擎同样会查看
my
对象中有么有toString()
方法,自然是找不到,于是它就去搜索该对象的原型属性。此时该原型属性已经指向了TwoDShape
的原型,而后者指向是Shape.prototype
.更重要的是,由于这里所采用的都是引用传递而不是值传递,所以这里的方法查询步骤直接精简为两步。 - 第一步:在本类中查找
- 第二步:在其原型属性中查找,因为
TwoDShape
得原型定义成了Shape.prototype
.所以直接就在Shape.prototpe
中查询,一下就找到了。
4.2 这种方式的缺点
这样简单的拷贝原型从效率上来说固然会更好一些,但是由于子对象与父对象指向的是同一个对象,所以一旦子对象对其原型进行了修改,父对象也会被修改。文章来源地址https://www.toymoban.com/news/detail-551848.html
4.3 代码实现
function Shape(){}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {
return this.name;
}
function TwoDShape(){}
TwoDShape.prototype = Shape.prototype;
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = "2D Shape";
function Triangle(side, height){
this.side = side;
this.height = height;
}
Triangle.prototype = TwoDShape.prototype;
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
var my = new Triangle(5, 10);
console.log(my.getArea()); //25
console.log(my.toString()); //Triangle
var supers = new Shape();
console.log(supers.name); //Triangle 一定要注意
到了这里,关于第3集丨JavaScript 使用原型(prototype)实现继承——最佳实战1的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!