前端基本:详解面向对象、组织函数、原型与原型链、继续

纲要:

  • 一、明白对象

    • 1.1 属性范例
    • 1.2 属性要领
  • 二、建立对象

    • 2.1 简朴体式格局建立
    • 2.2 工场情势
    • 2.3 组织函数
    • 2.4 原型
  • 三、继承

    • 3.1 原型链
    • 3.2 借用组织函数
    • 3.3 组合继承(原型链+借用组织函数)
    • 3.4 原型式继承
    • 3.5 寄生式继承
    • 3.6 寄生组合继承
    • 3.6 总结
  • 四、ES6继承

    • Class关键字
    • extends继承
    • super关键字
    • 原生组织函数拓展
    • Mixin情势的完成

一、明白对象

ECMAScript中没有类的观点,因而它的对象也与基于类的言语的对象有所差别。
ECMA-262把对象定义为“无序属性的鸠合,其属性可以包含基本值,对象或许函数”。对象的每一个属性或要领都有一个名字,而每一个名字映射到一个值。我们可以把ECMAScript的对象设想成散列表:不过就是一组键值对,其值可以是数据或函数。
每一个对象都是基于一个援用范例建立的,这个援用范例可以是原生范例,也可以是开辟人员定义的范例。

1.1属性范例

ECMAScript中有两种属性:数据属性和接见器属性。

1.1.1数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个形貌其行动的特征。

  • [[Configurable]]:示意可否经由历程delete删除属性从而从新定义属性。可否修正属性的特征,或许可否把属性修正为接见器属性。直接在对象上定义的属性,它们的这个特征默许值为true。
  • [[Enumerable]]:示意可否经由历程for-in轮回返回属性。直接在对象上定义的属性,它们的这个特征默许值为true。
  • [[Writable]]:示意可否修正属性的值。直接在对象上定义的属性,它们的这个特征默以为true。
  • [[Value]]:包含这个属性的数据值。读取属性值的时刻,从这个位置读;写入属性值的时刻,把新值保留在这个位置。这个特征的默许值为undefined。

要修正属性默许的特征,必需经由历程ES5的Object.defineProperty()要领。这个要领吸收三个参数:属性地点的对象、属性的名字和一个形貌符对象。个中,形貌符(descriptor)对象的属性必需是:configurable、enumerable、writable和value。设置个中的一或多个值,可以变动对应的特征值。

var person = {};
Object.defineProperty(person, "name", {
    writable: false,      //不能修正属性的值....
    configurable: false,  //不能经由历程delete删除属性.....
    value: "Jason"        //写入属性值
});
console.log(person.name); //Jason
person.name = "Cor";
console.log(person.name); //Jason
delete person.name;
console.log(person.name); //Jason

注重,一旦把属性设置为不可设置的,就不能再把它变动成可设置的了。此时再挪用

Object.defineProperty()要领修正除writable以外的特征就会致使毛病。
var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Jason"
});
//抛出毛病
Object.defineProperty(person, "name", {
    comfogirable: true,    //这行代码修正了特征致使报错
    value: "Cor"
});

在挪用Object.defineProperty()要领时,如果不指定configurable、enumerable和writable特征的默许值都是false。

1.1.2接见器属性

接见器属性不包含数据值,它包含一对getter和setter函数(不过,这两个函数都不是必需的)。在读取接见器属性时回调去getter函数,这个函数担任返回有用的值;在写入接见器属性时,会挪用setter函数并传入新值,这个函数担任决议怎样处置惩罚数据。接见器属性有以下4个特征:

  • [[Configurable]]:示意可否经由历程delete删除属性从而从新定义属性,可否修正属性的特征,或许可否把属性修正为数据属性。关于直接在对象上定义的属性,这个特征默许值为true。
  • [[Enumerable]]:示意可否经由历程for-in轮回返回属性。关于直接在对象上定义的属性,这个特征的默许值为true。
  • [[Get]]:在读取属性时挪用的函数。默许值为undefined。
  • [[Set]]:在写入属性时挪用的函数。默许值为undefined。

接见器属性不能直接定义,必需应用Object.defineProperty()要领来定义。
注重,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或许同时定义value属性,不然会报错。

    var book = {
        _year: 2004,
        edition: 1
    };
    
    Object.defineProperty(book, "year", {
        get: function() {
            return this._year;
        },
        set: function(newValue) {    //接收新值的参数
            if(newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    });
    book.year = 2005;            //写入接见器,会挪用setter并传入新值
    console.log(book.edition);  //2
var obj = {};

Object.defineProperty(obj, 'p', {
  value: 123,
  get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value

Object.defineProperty(obj, 'p', {
  writable: true,
  get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

1.2属性要领

  • Object.getOwnPropertyDescriptor()

该要领可以猎取属性形貌对象。它的第一个参数是一个对象,第二个参数是一个字符串,对应当对象的某个属性名。注重,该要领只能用于对象自身的属性,不能用于继承的属性。

var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }
  • Object.getOwnPropertyNames()

该要领返回一个数组,成员是参数对象自身的悉数属性的属性名,不论该属性是不是可遍历。下面例子中,obj.p1是可遍历的,obj.p2是不可遍历的。然则Object.getOwnPropertyNames会将它们都返回。

var obj = Object.defineProperties({}, {
  p1: { value: 1, enumerable: true },
  p2: { value: 2, enumerable: false }
});

Object.getOwnPropertyNames(obj)
// ["p1", "p2"]

与Object.keys的行动差别,Object.keys只返回对象自身的可遍历属性的悉数属性名。下面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所以实例对对象都邑继承它,它自身的属性都是不可遍历的。

Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]

Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
//  'valueOf',
//  'constructor',
//  'toLocaleString',
//  'isPrototypeOf',
//  'propertyIsEnumerable',
//  'toString']
  • Object.defineProperty(),Object.defineProperties()

Object.defineProperty()要领许可经由历程属性形貌对象,定义或修正一个属性,然后返回修正后的对象。实例上面已引见。
如果一次性定义或修正多个属性,可以应用Object.defineProperties要领。

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { get: function () { return this.p1 + this.p2 },
    enumerable:true,
    configurable:true
  }
});

obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
  • Object.prototype.propertyIsEnumerable()

实例对象的propertyIsEnumerable要领返回一个布尔值,用来推断某个属性是不是可遍历。

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

二、建立对象

2.1 简朴体式格局建立

我们可以经由历程new的体式格局建立一个对象,也可以经由历程字面量的情势建立一个简朴的对象。

var obj = new Object();
或
var obj = {};
//为对象增添要领,属性
var person = {};
person.name = "TOM";
person.getName = function() {
    return this.name;
}

// 也可以如许
var person = {
    name: "TOM",
    getName: function() {
        return this.name;
    }
}

这类体式格局建立对象简朴,但也存在一些题目:建立出来的对象没法完成对象的反复应用,而且没有一种牢固的束缚,操纵起来可能会涌现如许或许那样意想不到的题目。以下面这类状况。

var a = new Object;
var b = new Object;
var c = new Object;
c[a]=a;
c[b]=b;
console.log(c[a]===a); //输出什么 false
该题的细致剖析请参考文章一条面试题

2.2 工场情势

当我们须要建立一系列相似对象时,明显上面简朴的对象建立体式格局已不可以了,这会使代码中涌现很对反复的编码,形成代码冗余难保护。就以person对象为例,如果我们在现实开辟中,须要一个名字叫做TOM的person对象,同时还须要别的一个名为Jake的person对象,虽然它们有很多相似之处,然则我们不得不反复写两次。没增添一个新的person对象,就反复一遍代码,听起来就是很崩溃的。

var perTom = {
    name: 'TOM',
    age: 20,
    getName: function() {
        return this.name
    }
};

var perJake = {
    name: 'Jake',
    age: 22,
    getName: function() {
        return this.name
    }
}

我们可以应用工场情势的体式格局处理这个题目。望文生义,工场情势就是我们供应一个模型,然后经由历程这个模型复制出我们须要的对象。须要若干,就复制若干。

var createPerson = function(name, age) {

    // 声明一个中心对象,该对象就是工场情势的模型
    var o = new Object();

    // 顺次增添我们须要的属性与要领
    o.name = name;
    o.age = age;
    o.getName = function() {
        return this.name;
    }

    return o;
}

// 建立两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);

工场情势协助我们处理了反复代码的题目,可以疾速的建立对象。然则这类体式格局依然存在两个题目:没有办法辨认对象实例的范例

var obj = {};
var foo = function() {}

console.log(obj instanceof Object);  // true
console.log(foo instanceof Function); // true
console.log(perTom instancceof (类名??));  //发明彷佛并不存在一个Person类

因而,在工场情势的基本上,我们须要应用组织函数的体式格局来处理这个题目。

2.3 组织函数

2.3.1 new关键字

在Javascript中,new关键字非常奇异,可以让一个函数变的异乎寻常。看下面这个例子。

function demo() {
    console.log(this);
}

demo();  // window,严厉情势下this指向undefined
new demo();  // demo

从这个例子我们可以看到,应用new以后,函数内部发作了一些变化,this指向发作了转变。那末new关键字究竟都做了什么事情呢?

// 先道貌岸然的建立一个组织函数,实在该函数与一般函数并没有辨别
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.getName = function() {
        return this.name;
    }
}

// 将组织函数以参数情势传入
function New(func) {

    // 声明一个中心对象,该对象为终究返回的实例
    var res = {};
    if (func.prototype !== null) {

        // 将实例的原型指向组织函数的原型
        res.__proto__ = func.prototype;
    }

    // ret为组织函数实行的效果,这里经由历程apply,将组织函数内部的this指向修正为指向实例对象res
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

    // 当我们在组织函数中明白指定了返回对象时,那末new的实行效果就是该返回对象(即在组织函数中明白写了return this;)
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }

    // 如果没有明白指定返回对象,则默许返回res,这个res就是实例对象
    return res;
}

// 经由历程new声明建立实例,这里的p1,现实吸收的恰是new中返回的res
var person1 = New(Person, 'tom', 20);
console.log(person1.getName());

// 固然,这里也可以推断出实例的范例了
console.log(p1 instanceof Person); // true

JavaScript内部会经由历程一些迥殊处置惩罚,将var p1 = New(Person, ’tom’, 20);等效于var person1 = new Person(’tom’, 20); 我们熟习的这类情势。详细是怎样处置惩罚的,暂时没法作出诠释,须要更深切的相识道理。

当组织函数显现的return,会涌现什么状况?

我们先来列出几种返回的状况,看一下返回什么效果:

//直接 return  
function A(){  
   return;  
}  
//返回 数字范例  
function B(){  
   return 123;  
}  
//返回 string范例  
function C(){  
   return "abcdef";  
}  
//返回 数组  
function D(){  
   return ["aaa", "bbb"];  
}  
//返回 对象  
function E(){  
   return {a: 2};  
}  
//返回 包装范例  
function F(){  
   return new Number(123);  
}  

//效果是:
A {}  
B {}  
C {}  
["aaa", "bbb"]  
Object {a: 2}  
Number {[[PrimitiveValue]]: 123}  
A {}  

连系组织函数我们来看一下效果:

function Super (a) {  
   this.a = a;  
   return 123;  
}  
Super.prototype.sayHello = function() {  
   alert('hello world');  
}  
function Super_ (a) {  
   this.a = a;  
   return {a: 2};  
}  
Super_.prototype.sayHello = function() {  
   alert('hello world');  
}  

new Super(1); 
new Super_(1);

//效果
Super {a: 1} 具有原型要领sayHello
Object {a: 2}

总结一下:在组织函数中 return 基本范例不会影响组织函数的值,而 return 对象范例 则会替代组织函数返回该对象。

2.3.2 组织函数建立对象

为了可以推断实例与对象的关联,我们就应用组织函数来搞定。
像Object和Array如许的原生组织函数,在运转时自动涌如今实行环境中。我们也可以建立自定义的组织函数,从而定义对象范例的属性和要领。比方,

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.getName = function() {
        console.log(this.name);
    }
}
var person1 = new Person("Jason", 18, "WEB”);
var person2 = new Person("Cor", 19, "WEB");
console.log(person1.getName());   //Jason
console.log(person1 instanceof Person);  //true

组织函数情势和工场情势存在一下差别之处

  • 没有显现的建立对象(new Object() 或许 var a = {})
  • 直接将属性和要领赋给this对象
  • 没有return语句

关于组织函数,如果你暂时不可以明白new的详细完成,就先记着下面这几个结论:

  • 与一般函数比拟,组织函数并没有任何迥殊的处所,首字母大写只是我们开辟中的商定划定,用于辨别一般函数
  • new关键字让组织函数具有了与一般函数差别的很多特性,new的历程当中,实行了下面的历程:

    • 声明一个中心对象,即实例对象
    • 将该中心对象的原型指向组织函数原型(res.__proto__ = func.prototype)
    • 将组织函数this,指向该中心对象
    • 返回该中心对象,及返回实例对象

2.3.3 把组织函数当一般函数

//看成组织函数应用
var person = new Person("Jason", 18, "web");
person.getName();        //“Jason"

//作为一般函数挪用
Person("Cor", 19, "web");    //增添到window
window.getName();        //“cor"

//在另一个对象的作用域中挪用
var o = new Object();
Person.call(o, "Kristen", 22, "web");
o.getName();               //"kriten"

当在全局作用域中挪用一个函数时,this对象老是指向Global对象(在浏览器中的window对象),末了应用了call() ( 或许apply() )在某个迥殊对象的作用域中挪用Person()函数。这里是在对象o的作用域挪用的,因而挪用后o就具有了一切属性和要领。

2.3.4 组织函数的题目

组织函数的主要题目:上述例子中,每一个getName要领完成的功用现实上是如出一辙的,然则由于离别属于差别的实例,就不得不一向不断的为getName分派空间。

person1.getName == person2.getName;  //false

我们对组织函数略加修正,在组织函数内部我们把getName属性设置成即是全局的getName函数。由于组织函数的getName属性包含的是一个指向函数的指针,因而person1和person2对象就同享了在全局作用域中定义的同一个getName()函数。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.getName = getName;
}

function getName() {
    console.log(this.name);
}

var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB”);
person1.getName == person2.getName;  //true

2.4 原型

我们建立的每一个函数,都可以有一个prototype属性,该属性指向一个对象,这个对象就是我们说的原型对象。原型对象的用处是:包含一切可以由组织函数实例同享的属性和要领。依据字面明白就是,prototype就是由组织函数建立的实例对象的原型对象,应用原型对象的长处就是可以让一切实例同享原型对象所包含的要领,属性。

2.4.1 明白原型对象

上面说了,每一个函数建立的时刻,都邑依据某些规则为该函数建立一个prototype属性,这个属性指向函数的原型对象。在默许状况下,一切原型的对象都邑自动获得一个constructor(组织函数)属性,这个属性包含一个指向prototype属性地点函数的指针。以上面的例子来讲,也就是Person.prototype.constructor指向Person。
建立了自定义组织函数以后,其原型对象默许只会获得constructor属性;至于别的要领,则都是从Object继承而来的。当挪用组织函数new一个新实例后(person1) ,实例都有一个__proto__属性,该属性指向组织函数的原型对象(Person.prototype),经由历程这个属性,让实例对象也可以接见原型对象上的要领。因而,当多有的实例都可以经由历程__proto__接见到原型对象时,原型对象的要领与属性就变成了共有要领与属性。

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

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

var person1 = new Person("Jason", 20);
var person2 = new Person("Tim", 40);

console.log(person1.getName == person2.getName);     //true


《前端基本:详解面向对象、组织函数、原型与原型链、继续》
ECMA-262第五版中管这个指针叫[[Prototype]],虽然在剧本中没有范例的体式格局接见[[Prototype]],单Firefox,Safari和Chrome在每一个对象上都支撑一个属性__proto__;而在其他完成中,这个属性对剧本则是完整不可见的。不过须要明白的真正主要一点就是,这个衔接存在于实例与组织函数的原型对象之间,而不是存在于实例与组织函数之间。
虽然一切的完成中都没法接见到[[Prototype]],然则可以经由历程isPrototypeOf()要领来肯定对象之间是不是存在这类关联。从本质上讲,如果[[Prototype]]指向挪用isPrototypeOf()要领的对象(Person.prototype),那末这个要领就会返回true。

console.log(Person.prototype.isPrototypeOf(person1))  //true
console.log(Person.prototype.isPrototypeOf(person2))  //true

ES5增添了一个新要领,叫Object.getPrototypeOf(),在一切支撑的完成中,这个要领返回[[Prototype]]的值,可以轻易的猎取一个对象的原型。

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
console.log(Object.getPrototypeOf(person1).getName());     //"Jason"
    
搜刮机制:

当我们接见对象的属性或许要领时,会优先接见实例对象自身的属性和要领。当代码实行到读取对象的属性a时,都邑实行一次搜刮。搜刮起首从对象的实例自身最先。如果在实例中找到属性a,则返回该属性的值;如果没找到,则继承搜刮之震动指向的原型对象,在原型对象中查找属性a,如果在原型中找到这个属性,则返回该属性。简朴的说,就是会一层层搜刮,若搜刮到则返回,没搜刮到则继承基层搜刮。
虽然可以经由历程实例接见原型的值,然则却不能经由历程对象实例重写原型的值。如果我们为水增添了一个属性,而且该属性名和实例原型中的一个属性同名,就会在实例中建立该属性,该属性户屏障原型中的雷同属性。由于搜刮的时刻,起首在实例自身搜刮,查找到后直接返回实例中属性的值,不会搜刮到原型中。

function Person() {
}

Person.prototype.name = "Jason";
Person.prototype.age = 29;
Person.prototype.job = "Web";
Person.prototype.getName = function() {
    console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();
person1.name = "Cor";
person1.getName();         //"Cor"
person2.getName();        //"Jason"

若想可以接见原型中的属性,只需用delete操纵符删掉实例中的属性即可。

delete person1.name;
person1.getName();       //"Jason"
    

hasOwnProperty()

该要领可以检测一个属性是存在在实例中,照样存在于原型中。这个要领继承于Object,只需在给定属性存在于对象实例中时,才会返回true

console.log(person1.hasOwnProperty("name"));        //false
person1.name = "Cor";
console.log(person1.hasOwnProperty("name"));        //true;

2.4.2 in操纵符

in操纵符有两种应用体式格局:零丁应用和在for-in轮回中应用。
在零丁应用时,in操纵符在经由历程对象能接见到给定属性时就会返回true,不管属性存在于实例照样原型中。

console.log("name" in person1); true

in的这类迥殊性做经常使用的场景之一,就是推断当前页面是不是在挪动端翻开。

   isMobile = 'ontouchstart' in document;

// 很多人喜好用浏览器UA的体式格局来推断,但并非很好的体式格局

2.4.3 更简朴的原型语法

可以用一个包含一切属性和要领的对象字面量来重写全部原型对象。

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    getName : function() {
        console.log(this.name)
    }
};

用对象字面的要领和本来的要领会有辨别:constructor属性不再指向Person了。由于这类写法,本质上是修正了Person.prototype对象的援用,将援用从本来的默许值修正为了这个新对象{}的援用,constructor属性也变成了新对象{}的constructor属性(指向Object组织函数),不再指向Person。只管instanceof操纵符能返回准确的效果,然则constructor已没法肯定对象的范例了。

var friend = new Person();

console.log(friend instanceof Object);  //true
console.log(friend instanceof Person);    //true
console.log(friend.constructor == Person);  //false
console.log(friend.constructor == Object);  //true

如果construct的值很主要,我们可以像下面如许特地将它设置回恰当的值。

function Person(){}

Person.prototype = {
    constructor: Person,
    name : "Jason",
    age : 29,
    job : "Web",
    getName : function() {
        console.log(this.name)
    }
};

2.4.4 原型的动态性

由于在原型中查找值的历程是一次搜刮,因而我们在原型对象上所做的任何修正都可以马上从实例上反应出来——纵然是先建立了实例后修正原型也一样。下面这个例子中,friend实例是在增添sayHi要领之前建立的,但它依然可以接见新要领。这是由于实例与原型之间只不过是一个指针,而非一个副本,因而就可以在原型中找到新的sayHi属性并返回值。

var friend = new Person();

Person.prototype.sayHi = function() {
    alert("hi");
};

friend.sayHi();        //"hi"

然则,如果是经由历程{}这类重写原型对象的状况,就和上边不一样了。由于new实例时,实例中的__proto__属性指向的是最初原型,而把原型修正为新的对象{}就即是切断了组织函数与最初原型之间的联络,同时实例中依然保留的是最初原型的指针,因而没法接见到组织函数的新原型中的属性。请记着:实例只与原型有关,与组织函数无关。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name)
    }
};

friend.sayName();   //error,friend.sayName is not a function

如图,重写原型对象切断了现有原型与使命之前已存在的对象实例之间的联络;friend实例援用的依然是最初的原型,因而接见不到sayName属性。
注重,若想应用对象字面量重写原型,要在建立实例之前完成。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

2.4.5 原生对象的原型

原型建立对象的主要性不仅体如今建立自定义对象方面,就连一切原生的援用范例都采纳这类情势建立。一切原生援用范例(Object、Array、String,等等)都在其组织函数的原型上定义了要领。

console.log(Array.prototype.sort);            //function(){…}        
console.log(String.prototype.substring);      //function(){...}

经由历程原生对象的原型,我们也可以自定义新的要领。

String.prototype.startsWith = function(text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
    

稳固一下原型相干的学问点,我们以window这个对象为例,来检察一下各个属性值
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

三、继承

3.1原型链

原型对象实在也是一般的对象。险些一切的对象都多是原型对象,也多是实例对象,而且还可以同时是原型对象与实例对象。如许的一个对象,恰是构成原型链的一个节点。
我们晓得一切的函数都有一个叫做toString的要领,那末这个要领究竟是从哪来的呢?先声明一个函数:function add() {};,经由历程下图来看一下这个函数的原型链状况。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

个中add是Function对象的实例。而Function的原型对象同时又是Object原型的实例。如许就构成了一条原型链。原型链的接见,实在跟作用域链有很大的相似之处,他们都是一次单向的查找历程。因而实例对象可以经由历程原型链,接见到处于原型链上对象的一切属性与要领。这也是foo终究可以接见到处于Object原型对象上的toString要领的缘由。
我们再来看一个例子:

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

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

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

//继承了SuperType
//SubType的原型对象即是SubperType的实例,
//如许SubType内部就会有一个指向SuperType的指针从而完成继承
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());  //true

SubType继承了superType,而继承是经由历程建立SuperType实例,并将该实例赋给SubType.prototype完成的。本来存在于SuperType的实例中的一切属性和要领,如今也存在于SubType.prototype中了。这个例子中的实例、组织函数和原型之间的关联如图:
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

注重,instance.constructor如今指向的是SuperType,是由于subtype的原型指向了另一个对象SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。

    经由历程完成原型链,本质上扩大了前面说的原型搜刮机制。在经由历程原型链完成继承的状况下,搜刮历程就得以沿着原型链继承向上。那上面这个例子来讲,挪用instance.getSuperValue()会阅历三个搜刮步骤:
  1. 搜刮instance实例;未搜到;
  2. 搜刮SubType.prototype,未搜刮到;
  3. 搜刮SuperType.prototype,找到该属性,返回值。

3.1.1 默许原型

一切函数的默许原型都是Object的实例,因而默许原型都邑包含一个内布指针,指向Object.prototype。这也恰是一切自定义范例都邑继承toString()、valueOf()等默许要领的根本缘由。所以上面的例子展现的原型链中还应当包含另一个继承条理。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

3.1.2 肯定原型和实例的关联

有两种体式格局可以确认:instanceof操纵符以及isPrototypeOf()要领。

console.log(instance instanceof Object);    //true    
console.log(instance instanceof SuperType);    //true
console.log(instance instanceof SubType);    //true

console.log(Object.prototype.isPrototypeOf(instance));      //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance));   //true

由于原型链的关联,可以说instance是Object,SuperType,SubType中任何一个范例的实例,因而都返回true。一样的,只如果原型链中涌现过的原型,都可以说是该原型链所派生实例的原型。

3.1.3 郑重定义要领

子范例(SubType)有时刻须要重写超范例(SuperType)中的某个要领,或许须要增添超范例中没有的要领,但不论怎样,给原型增添要领肯定要在替代原型语句以后。防备涌现2.4.4中涌现的题目。
别的要注重,在经由历程原型链完成继承时,不能应用对象字面量建立原型要领,如许做会重写原型。

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

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

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

//继承超范例
SubType.prototype = new SuperType();

//增添新要领
SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

//重写超范例中的要领
SubType.prototype.getSuperValue = function(){
    return false;
}

//应用字面量增添新要领,会致使上面代码无效
SubType.prototype = {
    getSubValue : function(){
        return this.subproperty;
    },
    someOtherMethod : function(){
        return false;
    }
}

3.1.4 原型链的题目

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red, blue, green, black"

var instance2 = new SubType();
console.log(instance2.colors);    //"red, blue, green, black"

这个例子中的SuperType组织函数定义了一个colors属性,该属性包含一个数组(援用范例值)。SuperType的每一个实例都邑有各自包含本身数组的colors属性。当SubType经由历程原型链继承了SuperType以后,SubType.prototype就变成了SuperType(),所以它也用用了一个colors属性。就跟特地建立了一个SubType.prototype.colors属性一样。效果SubType得一切实例都邑同享这一个colors属性。
原型链的第二个题目是:在建立子范例的实例时,不能向超范例的组织函数通报参数。现实上,应当说是没有办法在不影响一切对象实例的状况下,给超范例的组织函数通报参数。所以在现实应用中很少会零丁应用原型链。

3.2 借用组织函数

在子范例组织函数中挪用超范例组织函数。函数只不过是在特定环境中实行代码的对象,因而经由历程应用apply()和call()要领也可以在新建立的对象上实行组织函数。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //继承了SuperType,同时还通报了参数
    SuperType.call(this, "Jason”);

    //实行上边这个语句,相当于把SuperType组织函数的属性在这里复制了一份
    //this.name = "Jason”;
    //this.colors = ["red", "blue", "green"];

    //实例属性
    this.age = 18;
}

var instance1 = new SubType();
instance1.colors. push("black");
console.log(instance1.colors);         //"red,blue,green,black"
console.log(instance1.name);        //"Jason"
console.log(instance1.age);            //18

var instance2 = new SubType();
console.log(instance2.colors);        //"red,blue,green"

为了确保SuperType组织函数不会重写子范例属性,可以在挪用超范例组织函数以后,再增添须要在子范例的定义的私有属性。
这个体式格局完成继承依然存在题目:

  • 要领都在组织函数中定义,每一个实例建立后都邑为组织函数的属性分派本身的内存,复用要领就无从谈起。
  • 而且,纵然在超范例的原型中定义了公有属性,但这些属性关于子范例而言是不可见的,所以采纳这类体式格局继承,就要把一切属性写在组织函数中

所以这类体式格局在现实开辟中是很少零丁如许应用的。

3.3 组合继承(原型链+借用组织函数)

组合继承(combination inheritance),有时刻也叫做典范继承,指的是将原型链和借用组织函数的继承体式格局组合到一块,从而发挥二者的长处的一种继承体式格局。其背地的思绪是应用原型链完成对原型属性和要领的继承,而经由历程借用组织函数来完成对实例属性的继承。如许既可以经由历程在原型上定义要领完成了函数复用,又可以在组织函数中定义要领保证每一个实例都有本身的私有属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
};

function SubType(name, age){
    //继承属性
    SuperType.call(this, name);
    
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new SubType("Jason", 18);
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"
instance1.sayName();            //"Jason"
instance1.sayAge();                //18

var instance2 = new SubType("Cor", 20);
console.log(instance2.colors)    //"red,blue,green"
instance2.sayName();            //"Cor"
instance2.sayAge();                //20
    

组合继承的题目

组合继承虽然是如今javascript最经常使用的继承情势,然则它也有不足。组合继承最大的题目就是不管什么状况下,都邑挪用两次超范例的组织函数:一次是在建立子范例原型的时刻,另一次是在子范例组织函数的内部。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);          //第二次挪用SuperType
 
    this.age = age;
}

SubType.prototype = new SuperType();     //第一次挪用SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

有解释的两行代码是挪用SuperType组织函数的代码,第一次挪用SuperType组织函数时,SubType.prototype会有SuperType的实例属性。第二次挪用SuperType的组织函数时SubType会在组织函数中增添了SuperType的实例属性。当建立SubType的实例它的[[Prototype]]和自身上都有雷同属性。依据搜刮机制自身的属性就会屏障SubType原型对象上的属性。即是原型对象上的属性是过剩的了。如图所示,有两组name和colors属性:一组在实例上,一组在Subtype原型中。这就是挪用两次组织函数的效果。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

3.4 原型式继承

基本头脑是借助原型可以基于已有的对象建立新对象,同时还没必要因而建立自定义范例。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

在object()函数内部,先建立了一个暂时性的组织函数,然后传入的对象作为这个组织函数的原型,末了返回了这个暂时组织函数的一个新实例。从本质上讲,object()对传入个中的对象实行了一次复制。
在ECMAScript5经由历程新增Object.create()要领范例了原型式继承。这个要领吸收两个参数:第一个用于做新对象原型的对象;第二个参数可选,为新对象定义分外的属性的对象。在传入一个参数的状况下,

Object.create()和object()要领的行动雷同。
var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
yetAnotherPerson.friends.push("Barbie");

console.log(yetAnotherPerson.name);    //"Greg"
console.log(anotherPerson.name);       //"Jason"
console.log(person.friends);           //"Cor,Court,Sam,Rob,Barbie"
console.log(anotherPerson.__proto__);               //"Cor,Court,Sam,Rob,Barbie"

这类原型式继承,请求必需有一个对象可以作为另一个对象的基本,把这个它通报给object()对象,然后再依据需求对获得的对象加以修正。在这个例子中,person对象可以作为另一个对象的基本,把它传入object()函数后返回一个新对象。这个新对象是将person作为原型,这意味着person.friend不仅属于person,而且被anotherPerson和yetAnotherPerson同享。

3.5 寄生式继承

寄生式(parasitic)继承是与原型式继承严密相干的一种思绪。寄生式继承的思绪与寄生组织函数和工场情势相似,即建立一个仅用于封装继承历程的函数,该函数在内部以某种体式格局来加强对象,末了再像真的是它做了一切事情一样返回对象。

function createAnother(original){
    var clone = object(original);     //经由历程挪用函数建立一个新对象
    clone.sayHi = function(){         //以某种体式格局来加强这个对象
         alert("Hi");
    };
    return clone;                     //返回这个对象
}

//应用createAnother
var person = {
    name: "Jason",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();     //"hi"

这个例子中的代码基于person对象返回了一个新对象——anotherPerson。新对象不仅具有person的一切属性和要领,而且另有本身的sayHi()要领。

3.6 寄生组合式继承

所谓寄生组合式继承,即经由历程借用组织函数来继承实例属性,经由历程寄生式继承体式格局来继承原型属性。其基本思绪就是:没必要为指定子范例的原型二挪用超范例的组织函数,我们须要的只是超范例原型的一个副本罢了。本质上就是,应用寄生式继承超范例的原型,然后将效果指定给子范例的原型。寄生组合式继承的基本情势以下:

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //建立对象
    prototype.constructor = subType;               //加强对象
    subType.prototype = prototype                  //指定对象
}

这个实例的inheritPrototype()函数完成了寄生组合式继承的最简朴情势。这个函数接收两个参数:子范例组织函数和超范例组织函数。在函数内部,第一步是建立超范例原型的一个副本。第二步是为建立的副本增添constructor属性,从而填补因重写原型而落空的默许的constructor属性。末了一步,将新建立的对象(即副本)赋值给子范例的原型。

function object(o){
    function F(){}    //建立个暂时组织函数
    F.prototype = o;  //superType.prototype
    return new F();   //返回实例
}

function inheritPrototype(subType, superType){
    /*  建立对象
        传入超范例的原型,经由历程暂时函数举行浅复制,F.prototype的指针就指向superType.prototype,在返回new F()    
    */
    var prototype = object(superType.prototype);   
    prototype.constructor = subType;               //加强对象
    /*  指定对象
        子范例的原型即是F范例的实例,当挪用组织函数建立一个新实例后,该实例会包含一个[[prototype]]的指针指向组织函数的原型对象,所以subType.prototype指向了超范例的原型对象如许完成了继承,由于组织函数F没有属性和要领如许就子范例的原型中就不会存在超范例组织函数的属性和要领了。
    */
    subType.prototype = prototype                  //new F();
}

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);          
 
    this.age = age;
}

inheritPrototype(SubType, SuperType);
//即等价于:
SubType.prototype = Object.create(Super.Prototype);
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var ins1 = new SubType("Jason", 18);

《前端基本:详解面向对象、组织函数、原型与原型链、继续》

3.7 总结

这里我们对六种继承体式格局的基本头脑,详细完成,优瑕玷做一个简朴的总结,稳固一下我们上面学到的学问。
继承体式格局:原型链继承
基本头脑:应用原型链来完成继承,超类的一个实例作为子类的原型
详细完成:

// 子类
function Sub(){ 
    this.property = ‘Sub Property’;
}
Sub.prototype = new Super();
// 注重这里new Super()天生的超类对象并没有constructor属性,故需增添上
Sub.prototype.constructor = Sub;

优瑕玷:
长处:

  • 简朴明了,轻易完成
  • 实例是子类的实例,现实上也是父类的一个实例
  • 父类新增原型要领/原型属性,子类都能接见到

瑕玷:

  • 一切子类的实例的原型都同享同一个超类实例的属性和要领
  • 在建立子范例的实例时,不能向超范例的组织函数通报参数。现实上,应当说是没有办法在不影响一切对象实例的状况下,给超范例的组织函数通报参数

继承体式格局:借用组织函数
基本头脑:经由历程应用call、apply要领可以在新建立的对象上实行组织函数,用父类的组织函数来增添子类的实例
详细完成:

// 子类 
function Sub(){ 
    Super.call(this);
    this.property = 'Sub Property’;
}

优瑕玷:
长处:

  • 简朴明了,直接继承超类组织函数的属性和要领
  • 可以通报参数

瑕玷:

  • 没法继承原型链上的属性和要领
  • 实例只是子类的实例,不是父类的实例

继承体式格局:组合继承
基本头脑:应用组织继承和原型链组合。应用原型链完成对原型属性和要领的继承,用借用组织函数情势完成对实例属性的继承。如许既经由历程在原型上定义要领完成了函数复用,又能保证每一个实例都有本身的属性
详细完成:

// 子类
function Sub(){
  Super.call(this);
  this.property = 'Sub Property’;
}
Sub.prototype = new Super();
// 注重这里new Super()天生的超类对象并没有constructor属性,故需增添上
Sub.prototype.constructor = Sub;

优瑕玷:
长处:

  • 处理了组织函数的两个题目
  • 既是父类实例,也是子类实例

瑕玷:

  • 挪用两次超范例的组织函数,致使子类上具有两份超类属性:一份在子类实例中,一份在子类原型上,且搜刮时实例中属性屏障了原型中的同名属性

继承体式格局:原型式继承
基本头脑:采纳原型式继承并不须要定义一个类,传入参数obj,天生一个继承obj对象的对象
详细完成:

function object(obj){ 
    function F(){}; 
    F.prototype = obj; 
    return new F();
}

优瑕玷:
长处:

  • 直接经由历程对象天生一个继承该对象的对象

瑕玷:

  • 不是类式继承,而是原型式继承,缺少了类的观点

继承体式格局:寄生式继承
基本头脑:建立一个仅仅用于封装继承历程的函数,然后在内部以某种体式格局加强对象,末了返回对象
详细完成:

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
function createSubObj(superInstance){
  var clone = object(superInstance);
  clone.property = 'Sub Property’;
  return clone;
}

优瑕玷:
长处:

  • 原型式继承的一种拓展

瑕玷:

  • 照旧没有类的观点

继承体式格局:寄生组合式继承
基本头脑:经由历程借用组织函数来继承属性,经由历程原型链的混成情势来继承要领,没必要为了指定子范例的原型而挪用超范例的组织函数,只须要超范例的一个副本。本质上,就是应用寄生式继承来继承超范例的原型,然后再将效果指定给子范例的原型
详细完成:

function inheritPrototype(Super,Sub){
  var superProtoClone = Object.Create(Super.prototype);
  superProtoClone.constructor = Sub;
  Sub.prototype =  superProtoClone;
}
function Sub(){
  Super.call(this);
  Sub.property = 'Sub Property’;
}
inheritPrototype(Super,Sub);

优瑕玷:
长处:

  • 圆满完成继承,处理了组合式继承带两份属性的题目

瑕玷:

  • 过于烦琐,故不如组合继承

四、ES6继承

4.1 Class关键字

ES6中经由历程class关键字定义类。

class Parent {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){
        console.log("I can speek chinese");
    }
}

// 经babel转码以后,代码是:
"use strict";

var _createClass = function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }

    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {
    function Parent(name, age) {
        _classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {
            console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();

可以看出类的底层照样经由历程组织函数去建立的。
注重一点,经由历程ES6建立的类,是不许可直接挪用的。即在ES5中,可以直接运转组织函数Parent()。然则在ES6中就不可,在转码的组织函数中有 _classCallCheck(this, Parent);语句,防备经由历程组织函数直接运转。直接在ES6运转Parent(),报错Class constructor Parent cannot be invoked without ‘new’,转码后报错Cannot call a class as a function。
转码中_createClass要领,它挪用Object.defineProperty要领去给新建立的Parent增添种种属性。defineProperties(Constructor.prototype, protoProps)是给原型增添属性。如果你有静态属性,会直接增添到组织函数上defineProperties(Constructor, staticProps)。

4.2 extends继承

Class可以经由历程extends关键字完成继承,这比ES5的经由历程修正原型链完成继承,要清楚和轻易很多。

class Parent {
    static height = 12
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){
        console.log("I can speek chinese");
    }
}
Parent.prototype.color = 'yellow'


//定义子类,继承父类
class Child extends Parent {
    static width = 18
    constructor(name,age){
        super(name,age);
    }
    coding(){
        console.log("I can code JS");
    }
}

var c = new Child("job",30);
c.coding()

转码以后的代码变成了如许

"use strict";

var _createClass = function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }

    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {
    function Parent(name, age) {
        _classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {
            console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();
Parent.height = 12;  //注重,该要领并不在转码后的组织函数function Parent中,
Parent.prototype.color = 'yellow';

//定义子类,继承父类

var Child = function (_Parent) {
    _inherits(Child, _Parent);

    function Child(name, age) {
        _classCallCheck(this, Child);

        return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
    }

    _createClass(Child, [{
        key: "coding",
        value: function coding() {
            console.log("I can code JS");
        }
    }]);

    return Child;
}(Parent);

Child.width = 18;


var c = new Child("job", 30);
c.coding();

可以看到,组织类的要领没变,只是增添了_inherits中心要领来完成继承,我们来重点剖析一个这个要领做了什么。

  • 起首推断父类的实例
  • 然后实行
subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
        value: subClass,
        enumerable: false,
        writable: true,
        configurable: true
    }
});

//这段代码翻译一下就是
function F(){}
F.prototype = superClass.prototype
subClass.prototype = new F()
subClass.prototype.constructor = subClass
  • 末了,subClass.__proto__ = superClass。

_inherits要领的中心头脑,总结一下就是下面这两句话:

subClass.prototype.__proto__ = superClass.prototype
subClass.__proto__ = superClass

《前端基本:详解面向对象、组织函数、原型与原型链、继续》

那为何如许一捣腾,它就完成了继承了呢?
起首 subClass.prototype.__proto__ = superClass.prototype保证了c instanceof Parent是true,Child的实例可以接见到父类的属性,包含内部属性,以及原型属性。其次,subClass.__proto__ = superClass,保证了Child.height也能接见到,也就是静态要领。

class Parent {}
class Child extends Parent {}

// for static propertites and methods
alert(Child.__proto__ === Parent); // true

// and the next step is Function.prototype
alert(Parent.__proto__ === Function.prototype); // true

// that's in addition to the "normal" prototype chain for object methods
alert(Child.prototype.__proto__ === Parent);

在内置对象中没有静态继承

请注重,内置类没有静态 [[Prototype]] 援用。比方,Object 具有 Object.defineProperty,Object.keys等要领,但 Array,Date 不会继承它们。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

Date 和 Object 之间毫无关联,他们自力存在,不过 Date.prototype 继承于 Object.prototype,仅此罢了。
形成这个状况是由于 JavaScript 在设想早期没有斟酌应用 class 语法和继承静态要领。

4.3 super关键字

super这个关键字,既可以当函数应用,也可以当对象应用。在这两种状况下,它的用法完整差别。

  • 第一种状况,super作为函数挪用时代表父类的组织函数。ES6请求,子类的组织函数必需实行一次super函数。

子类B的组织函数当中的super()代表挪用父类的组织函数,必需实行,不然会报错。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

注重,super虽然代表了父类A的组织函数,然则返回的是子类B的实例,即super内部的this指的是B的实例,因而super()在这里相当于A.prototype.constructor.call(this)。
new.target指向当前正在实行的函数。可以看到,在super()实行时,它指向的是子类B的组织函数,而不是父类A的组织函数。也就是说,super()内部的this指向的是B。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

注重,作为函数时,super()只能用在子类的组织函数当中,用在其他处所会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}
  • 第二种状况,super作为对象时,在一般要领中,指向父类的原型对象;在静态要领中,指向父类。
class A {
  constructor() {
    this.x = 2;
    this.y = 8;
  }
  p() {
    console.log(this.x);
  },
}
A.prototype.z = 10;

class B extends A {
  constructor() {
    super();
    this.x = 5;
    console.log(super.p());  //5;
  }
  getY() {
    return super.y;
  }
  getZ() {
    return super.z;
  }
}

let b = new B();
b.getY();  //undefined
b.getZ();  //10

在一般要领中,super指向A.prototype,所以super.p()就相当于A.prototype.p()。然则这里须要注重两点:

  • super指向的是父类原型,所以定义在父类实例上的要领或属性,没法经由历程super挪用。所以在B类的getY()要领中挪用super.y猎取不到值。然则定义在父类原型上的要领就可以猎取到,如getZ()要领中。
  • ES6划定,在子类一般要领中经由历程super挪用父类的要领时,要领内部的this指向当前子类的实例。在B类中挪用super.p(),this指向B类实例,输出的效果为5。super.p()现实上实行的是super.p.call(this)。

由于this指向子类实例,所以如果经由历程super对某个属性赋值,这时候super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined,super猎取不到父类的实例属性
    console.log(this.x); // 3
  }
}

在静态要领中,super作为对象指向父类,而不是父类的原型。别的,在子类的静态要领中经由历程super挪用父类的要领时,要领内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

注重,

  • 应用super的时刻,必需显式指定是作为函数、照样作为对象应用,不然会报错。
    class A {}
    
    class B extends A {
        constructor() {
            super();
            console.log(super); // 报错
        }
    }
  • 由于对象老是继承其他对象的,所以可以在恣意一个对象中,应用super关键字。
    var obj = {
        toString() {
            return "MyObject: " + super.toString();
        }
     };
    
    obj.toString(); // MyObject: [object Object]

在内置对象中没有静态继承

内置类没有静态 __proto__援用。比方,Object 具有 Object.defineProperty,Object.keys等要领,但 Array,Date 不会继承它们。
Date 和 Object 之间毫无关联,他们自力存在,不过 Date.prototype 继承于 Object.prototype,仅此罢了。
形成这个状况是由于 JavaScript 在设想早期没有斟酌应用 class 语法和继承静态要领。
《前端基本:详解面向对象、组织函数、原型与原型链、继续》

4.4 原生组织函数拓展

原生组织函数是指言语内置的组织函数,通经常使用来天生数据结构。ECMAScript的原生组织函数大抵有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

之前,原生组织函数是没法继承的。比方,不能本身定义一个Array的子类。

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0
colors.length = 0;
colors[0]  // "red"

上面这个例子中定义了一个继承Array的MyArray类。然则,我们看到,这个类的行动与Array完整不一致。
之所以会发作这类状况,是由于子类没法获得原生组织函数的内部属性,经由历程Array.apply()或许分派给原型对象都不可。原生组织函数会疏忽apply要领传入的this,也就是说,原生组织函数的this没法绑定,致使拿不到内部属性。
ES5 是先新建子类的实例对象this,再将父类的属性增添到子类上,由于父类的内部属性没法猎取,致使没法继承原生的组织函数。比方,Array组织函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性没法在子类猎取,致使子类的length属性行动不正常。
ES6 许可继承原生组织函数定义子类,由于 ES6 是先新建父类的实例对象this,然后再用子类的组织函数润饰this,使得父类的一切行动都可以继承。下面是一个继承Array的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

extends关键字不仅可以用来继承类,还可以用来继承原生的组织函数。 Array,Map 等内置类也可以扩大。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

注重,继承Object的子类,有一个行动差别

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr === true  // false

上面代码中,NewObj继承了Object,然则没法经由历程super要领向父类Object传参。这是由于 ES6 转变了Object组织函数的行动,一旦发明Object要领不是经由历程new Object()这类情势挪用,ES6 划定Object组织函数会疏忽参数。

4.5 Mixin情势的完成

Mixin 指的是多个对象合成一个新的对象,新对象具有各个构成成员的接口。它的最简朴完成以下。

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b’}

上面代码中,c对象是a对象和b对象的合成,具有二者的接口。
下面是一个更完整的完成,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

上面代码的mix函数,可以将多个对象合成为一个类。应用的时刻,只需继承这个类即可。

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

参考资料:
《详解面向对象、组织函数、原型与原型链》 https://segmentfault.com/a/11…
《JavaScript进修笔记-面向对象设想》 https://segmentfault.com/a/11…
《js对象建立要领汇总及对照》 https://segmentfault.com/a/11…
《Class的继承》 http://es6.ruanyifeng.com/#do…
《ES6 Class继承与super》 https://segmentfault.com/a/11…
《ES6类以及继承的完成道理》 https://segmentfault.com/a/11…

    原文作者:haru
    原文地址: https://segmentfault.com/a/1190000019250357
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞