JS继承 -> ES6的class和decorator

如何继承 Date 对象?由一道题彻底弄懂 JS 继承

2018/01/25 · JavaScript
· Date,
继承

原文出处: 撒网要见鱼   

面向对象的语言都有一个类的概念,通过类可以创建多个具有相同方法和属性的对象,ES6之前并没有类的概念,在ES6中引入类class.

继承6种套餐

参照红皮书,JS继承一共6种

前言

见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会及时修正。

———-长文+多图预警,需要花费一定时间———-

故事是从一次实际需求中开始的。。。

某天,某人向我寻求了一次帮助,要协助写一个日期工具类,要求:

  • 此类继承自Date,拥有Date的所有属性和对象
  • 此类可以自由拓展方法

形象点描述,就是要求可以这样:

// 假设最终的类是 MyDate,有一个getTest拓展方法 let date = new MyDate();
// 调用Date的方法,输出GMT绝对毫秒数 console.log(date.getTime()); //
调用拓展的方法,随便输出什么,譬如helloworld!
console.log(date.getTest());

1
2
3
4
5
6
7
// 假设最终的类是 MyDate,有一个getTest拓展方法
let date = new MyDate();
 
// 调用Date的方法,输出GMT绝对毫秒数
console.log(date.getTime());
// 调用拓展的方法,随便输出什么,譬如helloworld!
console.log(date.getTest());

于是,随手用JS中经典的组合寄生法写了一个继承,然后,刚准备完美收工,一运行,却出现了以下的情景:

图片 1

但是的心情是这样的: 😳囧

以前也没有遇到过类似的问题,然后自己尝试着用其它方法,多次尝试,均无果(不算暴力混合法的情况),其实回过头来看,是因为思路新奇,凭空想不到,并不是原理上有多难。。。

于是,借助强大的搜素引擎,搜集资料,最后,再自己总结了一番,才有了本文。

———-正文开始前———-

正文开始前,各位看官可以先暂停往下读,尝试下,在不借助任何网络资料的情况下,是否能实现上面的需求?(就以10分钟为限吧)

ES5 面向对象

1.原型链继承

核心思想:子类的原型指向父类的一个实例

Son.prototype=new Father();

大纲

  • 先说说如何快速快速寻求解答
    • stackoverflow上早就有答案了!
    • 倘若用的是中文搜索。
  • 分析问题的关键
    • 经典的继承法有何问题
    • 为什么无法被继承?
  • 该如何实现继承?
    • 暴力混合法
    • ES5黑魔法
    • ES6大法
    • ES6写法,然后babel打包
  • 几种继承的细微区别
  • ES6继承与ES5继承的区别
  • 构造函数与实例对象
  • [[Class]]与Internal slot
  • 如何快速判断是否继承?
  • 写在最后的话

创建对象(四种模式简介,此外还有动态原型模式、寄生构造函数模式、稳妥构造函数模式等)

一、工厂模式


function createPerson (Name,Age,Job) {

      var man= new Object();

      man.name= Name;

      man.age= Age;

      man.job= Job;

      man.sayName= function () {

              alert(this.name)

    }

  return  man;

}

var personOne=  createPerson (“Erric”,26,”Engineer”);

var personTwo=  createPerson (“Lori”,26,”teacher”);

优点:解决了多个相似对象的创建问题

缺点: ①  对象识别问题无法解决(即怎么知道一个对象的类型)

二、构造函数模式

function Person (Name,Age,Job) {

      this.name = Name;

      this.age = Age;

      this.job= Job;

      this.sayName= function () {

              alert(this.name)

      }

}

var personOne=  new Person(“Erric”,26,”Engineer”);

var personTwo=  new Person(“Lori”,26,”teacher”);

注一:
若不使用new操作符直接调用函数,那么其属性和方法都会被添加到window对象里面(因为在全局作用域调用一个方法时,this总是指向window对象)

如: Person(“Erric”,26,”Enginee”)

        window.sayName()  //  弹出 “Erric”

          window.name            //  “Erric”

          window.age              //  26

注二: new 操作符实际上进行了以下操作

          ① 创建一个新的对象

          ② 将构造函数的作用域赋给新对象(this指向了这个新的对象)

          ③ 执行构造函数中的代码(为这个新对象添加属性)

          ④ 返回这个新的对象

优点:① 不用显式的创建对象

            ② 将属性和方法赋给了this对象

            ③ 没有return语句

缺点:① 
每个方法都要在每个实例上重新创建一遍(personOne和personTwo中的sayName方法不是同一个方法,每个函数都是一个对象,故每 
定义了一个函数就实例化了一个对象)。

           
此问题也可以通过将方法单独抽出来解决(但是方法一多,都移到全局的话封装性就无从谈起),如下:

            function Person (Name,Age,Job) {

                    this.name = Name;

                      this.age = Age;

                      this.job= Job;

                      this.sayName= sayName

            }

            function sayName() {

                    alert(this.name)

              }

            var personOne=  new Person(“Erric”,26,”Engineer”);

            var personTwo=  new Person(“Lori”,26,”teacher”);

            ② 若是将公共的sayName方法移到全局,那么又没有封装性可言了。


三、原型模式

function Person () {

}

Person.prototype.name= “Erric”

Person.prototype.age= “28”

Person.prototype.job= “Job”

Person.prototype.sayName= function () {

        alert(this.sayName)

}

优点:①  解决了函数共用的问题,不用每个实例都创建一遍方法。

缺点:①  不能传参

            ②
如果实例中修改了原型中的属性(引用类型)或方法,那么这个属性或方法会被彻底的修改,而影响到其他实例。


四、构造函数+原型组合模式

function Person (Name,Age,Job) {

          this.name= Name

          this.age= Age

          this.job= Job

}

Person.prototype.sayName= function () {

          alert(this.name)

}

//
上面往原型上添加属性和方法的也可如下写,但是此时原型的constructor不指向Person构造函数,而是指向Object,因为Person.prototype就像一个新的对象实例,它的__proto__指向Object原型。

//  Person.prototype= {

          constructor: Person,            //
重新再实例中定义constructor的指向,覆盖Object原型中的constructor指向

          sayName: function () {

                  alert(this.name)

          }

}

var personOne=  new Person(“Erric”,26,”Engineer”);

var personTwo=  new Person(“Lori”,26,”teacher”);


原型对象的理解(重要)

1.首先得明白以下三点:

① 每个函数(含构造函数)都有一个prototype属性,指向Person原型

② 每个实例都有一个__proto__属性,也指向Person原型

③ 每个原型都有一个constructor属性,指向其对应的构造函数

构造函数、实例、原型三者关系如下图:

图片 2

2.万物皆对象,说明原型链的最开始点都是Object,所以任何一个引用类型的
instanceof Object都会返回true。


2.构造函数继承

核心思想:借用apply和call方法在子对象中调用父对象

function Son(){Father.call(this);}

先说说如何快速快速寻求解答

遇到不会的问题,肯定第一目标就是如何快速寻求解决方案,答案是:

  • 先去stackoverflow上看看有没有类似的题。。。

于是,借助搜索引擎搜索了下,第一条就符合条件,点开进去看描述

图片 3

类的继承(两种方式)

一、原型链继承

        对于什么是原型链?

       
每个构造函数都有一个原型对象,原型对象的constructor指向这个构造函数本身,而实例的__proto__属性又指向原型对象。这个假设一个实例的__proto__内部指针指向其原型,而它的原型又是另一个类型的实例,那么它的原型又将指向另一个原型,另一个原型也包含一个指向它的构造函数的指针,假设另一个原型又是另一个类型的实例,这样层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。

实现原型链的继承方式基本如下:

function Father () {

      this.appearance = “beautiful”

}

Father.prototype.sayHappy = function () {

        alert(“快乐”)

}

function Child () {

          this.name= “Jhon”

}

Child.prototype= new Father()        //  继承了父类的方法和属性

Child.prototype.addArr= [1,2,3,4,5]

var child= new Child()
child.sayHappy()          //  弹出“快乐”
child.appearance        //  “beautiful”

child.addArr                      //  [1,2,3,4,5]

原型链继承的缺点:①  不能传参  ②
若原型上的方法时引用类型的话,不小心被修改了的话会影响其他实例。


二、借助构造函数继承(利用calll和apply改变this指针)

基本思路:在子类型构造函数的内部调用超类型的构造函数。

function Father (Hobby){

      this.hobby= Hobby

}

Father.prototype.sayHappy = function () {

      alert(“快乐”)

}

function Child () {

      this.name= “Jhon”

      Father.call(this,”Play Games”)          // 
或者Father.apply(this,[“Play Games”]),继承了Father的属性和方法

}

var child =  new Child()
child.sayHappy                //
没有反应,原型上的方法和属性不会继承
child.hobby                      //  “Play Games”

借助构造函数继承的缺点:① 
方法都在构造函数中定义,函数的复用无从谈起    ② 
超类中的方法对子类不可见。


三、组合继承(也叫经典继承,将原型链和借助构造函数继承相结合)

思路:1.原型链实现对原型属性和方法的继承;

            2.构造函数实现对实例属性的继承,且调用基类的构造函数;

function Father(Hobby) {

          this.hobby= Hobby;

          this.exGF = [‘cuihua’, ‘erya’]

}

Father.prototype.sayHappy = function () {

          alert(“快乐”)

}

function Child () {

          this.name= “Jhon”

          Father.call(this,”Play Games”)          // 
或者Father.apply(this,[“Play Games”]),继承了Father的属性和方法

}

Child.prototype= new Father()

Student.prototype.sayName= function () {

          alert(this.name);

}

var liHua= new Child()

liHua.sayHappy()

liHua.sayName()


检测对象属性的两种方法:

object.hasOwnProperty(属性名),这个方法检测的是对象实例的属性(若是返回true),不能检测原型上的属性。

in操作符,检测对象所有的属性,包含原型和实例上的额,有的话就返回true.


判断一个原型是否在某个实例的原型链上:

Person.prototype.isPropotypeOf(personOne)    //  true

Object.prototype.isPropotypeOf(personOne)      //  true

判断一个构造函数是否在实例的原型链中出现过:

personOne instanceof Person                //  true

personOne instanceof Object                //  true


3.组合继承(1+2)(常用)

核心思想:1+2,但记得修正constructor

function Son(){Father.call(this);}

Son.prototype=new Father();

Son.prototype.constructor = Son;

stackoverflow上早就有答案了!

先说说结果,再浏览一番后,确实找到了解决方案,然后回过头来一看,惊到了,因为这个问题的提问时间是6 years, 7 months ago
也就是说,2011年的时候就已经有人提出了。。。

感觉自己落后了一个时代>_。。。

图片 4

而且还发现了一个细节,那就是viewed:10,606 times,也就是说至今一共也才一万多次阅读而已,考虑到前端行业的从业人数,这个比例惊人的低。
以点见面,看来,遇到这个问题的人并不是很多。

ES6 面向对象

ES6中引入了Class(类)这个概念,通过关键字class可以创建一个类。类的数据类型就是函数,类的所有方法都定义在prototype属性上。

class Person () {
        constructor (x,y) {
              this.name= x
              this.age= y
        }
        sayName () {
                alert(“快乐”)
        }
}
var liHua= new Person(“张俊泽”,26)

注:
可以理解为constuctor中的属性和方法为ES5中的构造函数部分,和constructor同级的是ES5中原型上的方法和属性。


ES6的继承通过extends关键字实现

class Father(){}
class Child extends Father {
        constructor(x,y,color){
                  super(x,y)
                  this.color= color
        }
        toString() {
                retunr “世界和平!”
        }
}

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。


类的prototype和__proto__属性

Class作为构造函数的语法唐,同时有prototype和__proto__属性,因此存在两条继承链:

①  子类的__proto__,表示构造函数的继承,总是指向父类

② 
子类的prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class Father {

}

class Child extends Father{

          constructor () {

                  super()

          }

}

var childOne= new Child()

Child.__proto__ ==  Father        //  true

childOne.__proto__ ==  Child.prototype        //  true

Child.prototype.__proto__ ==  Fahter.prototype            //  true

4.原型式继承

核心思想:返回一个临时类型的一个新实例,现提出了规范的原型式继承,使用Object.create()方法。

var person={name:”xiaoming”,age:16}

var anotherperson=Object.create(person,{name:”xiaowang”})

倘若用的是中文搜索。

用中文搜索并不丢人(我遇到问题时的本能反应也是去百度)。结果是这样的:

图片 5

嗯,看来英文关键字搜索效果不错,第一条就是符合要求的。然后又试了试中文搜索。
图片 6

图片 7效果不如人意,搜索前几页,唯一有一条看起来比较相近的(segmentfault上的那条),点进去看

图片 8
图片 9

怎么说呢。。。这个问题关注度不高,浏览器数较少,而且上面的问题描述和预期的有点区别,仍然是有人回答的。
不过,虽然说问题在一定程度上得到了解决,但是回答者绕过了无法继承这个问题,有点未竟全功的意思。。。

5.寄生式继承

核心思想:创建一个仅用于封装继承过程的函数,该函数在内部使用某种方式增强对象

function createAnother(original){

var clone=object(original);

clone.name=”ahaha”;

return clone;

}

分析问题的关键

借助stackoverflow上的回答

6.寄生组合继承

核心思想:3+5

function inheritPropertype(son,father){

var prototype=object(father.prototype);//创建

prototype.constructor=son;//增强

son.prototype=prototype;//指定

}

在阮一峰老师的解说下,他将继承分成了两种,构造函数的继承非构造函数的继承

构造函数的继承:

1.apply或call

2.prototype,即子类原型属性指向父类实例

3.直接的prototype,子类原型=父类原型

4.利用空对象作为中介,这种方法类似寄生继承,但是会变成子类->中介->父类这样的继承关系。好处是当子类对原型进行变动时,对父类没有影响。

function extend(Child, Parent) {

    var F = function(){};

    F.prototype = Parent.prototype;//继承方法2

    Child.prototype = new F();//继承方法1

    Child.prototype.constructor = Child;//修正

    Child.uber =
Parent.prototype;//为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。只是为

                                                       
//了实现继承的完备性,纯属备用性质。

  }

5.拷贝继承,将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。

  function extend2(Child, Parent) {

    var p = Parent.prototype;

    var c = Child.prototype;

    for (var i in p) {

      c[i] = p[i];

      }

    c.uber = p;

  }

非构造函数的继承:

1.原型式继承。

2.浅拷贝

  function extendCopy(p) {

    var c = {};

    for (var i in p) {

      c[i] = p[i];

    }

    c.uber = p;

    return c;

  }

子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

3.深拷贝

  function deepCopy(p, c) {

    var c = c || {};

    for (var i in p) {

      if (typeof p[i] === ‘object’) {

        c[i] = (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i], c[i]);

      } else {

         c[i] = p[i];

      }

    }

    return c;

  }

经典的继承法有何问题

先看看本文最开始时提到的经典继承法实现,如下:

/** * 经典的js组合寄生继承 */ function MyDate() { Date.apply(this,
arguments); this.abc = 1; } function inherits(subClass, superClass) {
function Inner() {} Inner.prototype = superClass.prototype;
subClass.prototype = new Inner(); subClass.prototype.constructor =
subClass; } inherits(MyDate, Date); MyDate.prototype.getTest =
function() { return this.getTime(); }; let date = new MyDate();
console.log(date.getTest());

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
/**
* 经典的js组合寄生继承
*/
function MyDate() {
    Date.apply(this, arguments);
    this.abc = 1;
}
 
function inherits(subClass, superClass) {
    function Inner() {}
    
    Inner.prototype = superClass.prototype;
    subClass.prototype = new Inner();
    subClass.prototype.constructor = subClass;
}
 
inherits(MyDate, Date);
 
MyDate.prototype.getTest = function() {
    return this.getTime();
};
 
 
let date = new MyDate();
 
console.log(date.getTest());

就是这段代码⬆,这也是JavaScript高程(红宝书)中推荐的一种,一直用,从未失手,结果现在马失前蹄。。。

我们再回顾下它的报错:

图片 10

再打印它的原型看看:

图片 11

怎么看都没问题,因为按照原型链回溯规则,Date的所有原型方法都可以通过MyDate对象的原型链往上回溯到。
再仔细看看,发现它的关键并不是找不到方法,而是this is not a Date object.

嗯哼,也就是说,关键是:由于调用的对象不是Date的实例,所以不允许调用,就算是自己通过原型继承的也不行

ES6的class语法糖

不知道为什么标题都是跟吃的有关

可能是因为到了半夜吧(虚

在学ES6之前,我们苦苦背下JS继承的典型方法

学习ES6后,发现官方鸡贼地给我们一个语法糖——class。它可以看作是构造函数穿上了统一的制服,所以class的本质依然是函数,一个构造函数。

class是es6新定义的变量声明方法(复习:es5的变量声明有var
function和隐式声明 es6则新增let const class
import),它的内部是严格模式。class不存在变量提升

例:

//定义类

classPoint{

    constructor(x,y){

        this.x=x;

        this.y=y;

    }

    toString(){

        return'(‘+this.x+’, ‘+this.y+’)’;

    }

}

constructor就是构造函数,不多说,跟c++学的时候差不多吧,this对象指向实例。

类的所有方法都定义在类的prototype属性上面,在类的内部定义方法不用加function关键字。在类的外部添加方法,请指向原型,即实例的__proto__或者类的prototype。

Object.assign方法可以很方便地一次向类添加多个方法。

Object.assign(Point.prototype,{toString(){},toValue(){}});

相关文章