资讯专栏INFORMATION COLUMN

【呆萌の研究】JavaScript常见的继承方式

马永翠 / 833人阅读

摘要:构造函数构造操作符调用的函数就是构造函数。其和其构造函数的指向相同。而构造函数属性指向的对象带有属性,指向函数自身。,回归构造函数继承,仔细看看诞生的嘻嘻和哈哈两位同学可以看到两个实例都拥有了和两个属性,因为方法的运行类似于执行了和。

最近在看《JavaScript设计模式》,然后开篇复习了JavaScript中的几种继承方式,自己似乎也没有怎么仔细探究过,目前自己没怎么碰到过应用的场景(噗),所以借这次机会好好来屡屡思路。

方式1 类式继承

例子

function Person() {
    this.telephone = ["000-0000-0000"];
}

function Student(className) {
    this.className = className;
}

Student.prototype = new Person();
var Haha = new Student(1);
var Xixi = new Student(2);

创建好父类和子类。联系他们的方式是把学生的prototype指向一个人的实例。

问:prototype是什么?
几乎任何对象有一个[[prototype]]属性,在标准中,[[prototype]]一个隐藏属性,指向的是这个对象的原型。而它的指向是由构造该对象的方法决定的:
1.对象字面量构造:其[[prototype]]指向Object.prototype。

var person = {};

2.构造函数构造:new操作符调用的函数就是构造函数。其[[prototype]]和其构造函数的prototype指向相同。而构造函数prototype属性指向的对象带有constructor属性,指向函数自身。

function Person(){}
var person = new Person();

此图为Person的prototype内容,可以看到constructor属性实际指向的就是Person()函数。(小绿色框框内和外面绿色框框其实是同一个内容)。

3.Object.create构造的。

var person = {};
var Haha = Object.create(person);

这里对象Haha的[[prototype]]指向对象person。也可以写null,此时对象Haha就没有原型。

首先要分清楚类和实例,在控制台显示中,只有类才会有prototype属性,而实例是拥有一个名为_proto_的属性,它会指向构造它函数的原型,两者本质都是一个指针。

function Person() {
   this.telephone = ["000-0000-0000"];
}
var Hehe = new Person(); 
console.log(Person.prototype);
console.log(Hehe);

以上代码运行结果:

可以瞧见,这里Hehe的_proto_是指向了Person.prototype

问:new关键字的作用是什么?
new关键字运作的过程如下,引用自《JavaScript》高级程序设计:

1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。  2、属性和方法被加入到 this 引用的对象中。 
3、新创建的对象由 this 所引用,并且最后隐式的返回 this。

简单来说,它创建了一个空对象,指定了原型,把属性方法进行拷贝,并把this指向进行了改变。假如我们把上面的代码改成:

function Person() {
 this.telephone = ["000-0000-0000"];
}
        
var Hehe = Person();
console.log(Hehe.telephone);

去掉new关键词赋予Person(),会报错,而输出window.telphone得到的就是["000-0000-0000"]。因为函数的返回值(没有返回值所以是undefined)赋予给了Hehe,尝试去读取undefined的属性,报错了。而此时函数运行中的this是全局变量window。

So,回归类式继承,仔细看看诞生的嘻嘻和哈哈两位同学

会发现,各自都有自己的班级名属性,但是原型指向的是同一个Person实例,所以如果嘻嘻有两个号码,或者他要更改自己的号码,那哈哈的电话号码也会发生变化,他们只能共享这个电话号码。

方式2 构造函数继承

例子

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

Person.prototype.showName = function() {
    console.log(this.name);
}

function Student(name, className) {
    this.className = className;
    Person.call(this, name);
}
var Haha = new Student("Haha", 1);
var Xixi = new Student("Xixi", 2);

问:call函数的运作过程?
call函数和apply函数的作用相同,不同之处就是apply函数只能传入2个参数,而call函数可以有多个。F.call(thisArg,[arg1……]) 函数的运作过程如下(来源网络):

1.先判断F是否为一个函数,如果不是一个函数,那么将抛出TypeError异常。 
2.创建一个内部类型空列表list
3.然后如果参数除去thisArg外还有其他参数的话,就将这些值添加到list中
4.thisArg和list作为F内部属性[[Call]]的参数传入调用进行函数的执行操作

简而言之就是它把一个函数的对象上下文改成了由 thisArg指定的新对象。

So,回归构造函数继承,仔细看看诞生的嘻嘻和哈哈两位同学

可以看到两个实例都拥有了className和name两个属性,因为call方法的运行类似于执行了Haha.name="Haha"Xixi.name="Xixi"
但是因为没有与父类的原型相联系,所以父类原型中的方法,不能得到继承。运行Haha.showName()会得到报错。

方式3 组合继承

例子

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

Person.prototype.showName = function() {
    console.log(this.name);
}

function Student(name, className) {
    this.className = className;
    Person.call(this, name);
}
Student.prototype = new Person();
Student.prototype.showClassName = function() {
     console.log(this.className);
}

var Haha = new Student("Haha", 1);
var Xixi = new Student("Xixi", 2);

组合继承综合了类式继承和构造函数继承,在把父类的属性继承后,把子类的原型指向了父类实例,这样就可以继承父类原型的方法了。
但是这里相当于使用了两次父类函数,并且子类不是父类的实例,子类的原型是父类的实例,所以还会有更好的方法。

方式4 原型继承
function inheritObject(o) {
 function F() {}
 F.prototype = o;
 return new F();
}

var person = {
 name: "unknown",
 telephone: ["000-0000-0000"]
}

var Xixi = inheritObject(person);
Xixi.name = "Xixi";
Xixi.telephone.push("111-1111-1111");

var Haha = inheritObject(person);
Haha.name = "Haha";

仔细看看诞生的嘻嘻和哈哈两位同学

这里.name给Xixi实例添加了一个自己的name属性,而push操作是直接影响原型中引用变量,所以改进之后又有了下面这种方式。
在这里我产生了一个疑问,为什么name属性是自己添加新的,而telephone是采用原来的。于是添加了一个age属性,执行Xixi.age++操作。

这里可以看到实例重新添加了一个age属性,所以我们可以说只要是改变原型属性的值,就会把新的属性加在实例上,引用不改变是因为引用的地址还没有改变。

方式5 寄生式继承

寄生式继承是在原型继承的基础之上,我们需要再添加一下代码:

function createPerson(obj) {
 var o = inheritObject(obj);
 o.getName = function(){
  console.log(name);
 }
 return o;
}

这样就给得到的对象添加了公共方法。

方式6 寄生组合式继承

寄生组合式继承是为了弥补组合式继承的缺点,是在寄生式继承+构造函数继承组合而成的:

function inheritObject(o) {
 function F() {}
 F.prototype = o;
 return new F();
}
function inheritPrototype(subClass, superClass) {
 //复制一份父类原型
 var p = inheritObject(superClass.prototype);
 //修正重写子类原型导致constructor属性被修改
 p.constructor = subClass;
 //设置子类原型
 subClass.prototype = p;
}

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

Person.prototype.showName = function() {
    console.log(this.name);
}

function Student(name, className) {
    this.className = className;
    Person.call(this, name);
}

inheritPrototype(Student, Person);

Student.prototype.showClassName = function() {
     console.log(this.className);
}

var Xixi = new Student("Xixi",2);
var Haha = new Student("Haha",1);

以下为嘻嘻和哈哈的内容:

可以对比一下组合式继承的结果:

不同的地方在于把子类原型的构造函数改成了实例对应的构造函数,在组合继承中子类原型直属并没有constructor属性。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93124.html

相关文章

  • 萌の研究JavaScriptの闭包

    摘要:为什么会产生闭包究其根本,是因为代表的函数包含的作用域。而在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位直到作为作用域链终点的全局执行环境。 前言 此文的内容主要是来自看书的总结+小小的实践哦~会不断更新总结。 什么是闭包 书上是这样定义闭包的: 有权访问另一个函数作用域中变量的函数。 举一个例子: function test(){ va...

    CHENGKANG 评论0 收藏0
  • 萌の研究】圣杯布局引发对margin负值研究

    摘要:问题起源以前一直就听说圣杯布局,但是没有怎么去用过,然后这次偶然接触到了,就学习了一下。继续试验我们可以尝试改变的值,去看看位置的变化。为了方便我们计算,另外写了一个类似的布局,内容区的宽度是,三个的宽度也都是。 问题の起源 以前一直就听说圣杯布局,但是没有怎么去用过,然后这次偶然接触到了,就学习了一下。这是一个我从别人写的文章中复制过来的,关于圣杯布局的比较简单的说明 通过缩放页面就...

    zhangke3016 评论0 收藏0
  • 萌の体验】vue.js初次体验

    摘要:官方默认项目是存放了一个为的打开文件夹有一个,还有一个名为组件的文件夹,里面放了一个文件。部分我们会发现这几排字就是显示在页面的几排文字部分这其中的这个文件引入了,还有上述的。结合查询其他说法,就是说它会把是的元素以形式替换。 前言 我很早就想来学习学习vue.js啦,终于有了那么一些空闲的时间可以拿来学习,于是从前天开始我就每天抽一个多小时来体验vue.js。当然啦,因为是小白入门,...

    wdzgege 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<