avatar

目录
原型链与继承

原型链

原型链:每一个(实例)对象都有一个私有属性 __proto__(隐式原型),指向它构造函数的原型对象 prototype(显示原型),并从中继承方法和属性,同时该原型对象也有自己的隐式原型对象(__proto__),这样一层一层,最终指向 null,这就是原型链。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent(age) {
this.age = age;
}

Parent.prototype.getAge = function(){
console.log(this.age);
}

var p = new Parent(50);

p; // Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true

下图展示了原型链的运作机制。

原型链

prototype__proto__

其中原型对象 prototype 是构造函数的属性,__proto__ 是每个实例上都有的属性,这两个并不一样,但 p.__proto__Parent.prototype 指向同一个对象。

想要搞懂原型链,手画几次图就很快可以理解:

组合继承

继承

JavaScript 相比于其他面向类的语言,在实现继承时并没有真正对构造类进行复制,当我们使用 var Child = new Parent() 继承父类时,我们理所当然的理解为 Child “为 Parent 所构造”。实际上这是一种错误的理解。严格来说,JavaScript 才是真正的面向对象语言,而不是面向类语言。它所实现的继承,都是通过每个对象创建之初就存在的 prototype 属性进行关联、委托,从而建立联系,间接的实现继承,不是复制父类;行为委托,而不是模拟类

原型继承优点:假设把一个方法(公共)添加到构造函数本身里,但不是每个 Parent 实例都需要这个方法,这将浪费大量内存空间,因为每个实例对象都该方法,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!

ES5 最常见的几种继承:原型链继承、构造函数继承、组合继承、寄生组合继承

  1. 原型链继承

    JavaScript
    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
    27
    28
    29
    // 定义父类
    function Parent(name) {
    this.name = name;
    this.hobby = ["SCII"]
    }

    Parent.prototype.getName = function() {
    return this.name;
    };

    // 第 1 步:定义子类
    function Child(age) {
    this.age = age;
    }

    // 通过 Child 的 prototype 属性和 Parent 进行关联继承
    // 第 2 步: 将新创建的对象 (即副本) 赋值给子类型的原型
    Child.prototype = new Parent('Tim');

    // 第 3 步:为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性
    Child.prototype.constructor = Child;

    const c1 = new Child(16);
    c1.age // 16
    c1.hobby.push("CS")
    c1.getName(); // Tim

    const c2 = new Child(18);
    console.log(c2) // {age: 18, hobby: (2) ['SCII', 'CS']...)

    整个继承过程,都是通过原型链之间的指向进行委托关联,直到最后形成了“由构造函数所构造”的。

    问题:

    • 原型中包含的引用类型属性将被所有实例共享;
    • 子类在实例化的时候不能给父类构造函数传参;
  2. 构造(Call)函数继承

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 定义父类
    function Parent(name) {
    this.name = name;
    this.hobby = ["SCII"]
    this.getName = function(){
    return this.name;
    }
    }

    // 定义子类
    function Child(name,age) {
    // this 为 Child 的实例,
    Parent.call(this, name);
    }

    const c1 = new Child("Tim","250");
    c1.hobby.push("CS")
    c1.hobby // ['SCII', 'CS']
    c1.getName() // Tim

    const c2 = new Child("Timon","260");
    c2.hobby // ['SCII']
    c2.getName() // 'Timon'

    构造继承关键在于,通过在子类的内部调用父类,即通过使用 apply()call() 方法可以在将来新创建的对象上父类的成员和方法。Child 方法中把 Parent 当做普通函数执行,让 Parent 函数中的 this 指向 Child 的实例,相当于给 Child 的实例设置了很多私有的属性、方法

    解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。

    问题:

    • 只能继承父类私有属性、方法(因为把 Parent 当普通函数执行,和其原型上的属性、方法没有关系)
  3. 组合继承

    JavaScript
    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
    27
    function Parent (name) {
    this.name = name;
    this.hobby = ["SCII"]
    this.getName = function(){
    return this.name;
    }
    }

    Parent.prototype.getHobby = function () {
    return this.hobby
    }

    function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
    }

    Child.prototype = new Parent();

    const c1 = new Child('Tim', '250');
    c1.hobby.push("CS")
    console.log(c1) // {name: 'Tim', hobby: Array(2), age: '250', getName: ƒ}
    // c1._proto_ = {name: undefined, hobby: Array(1), getName: ƒ}

    const c2 = new Child('Timon', '260');
    console.log(c2) // {name: 'Timon', hobby: Array(1), age: '260', getName: ƒ}
    // c1._proto_ = {name: undefined, hobby: Array(1), getName: ƒ}

    组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来
    问题:

    • 调用两次父构造函数,效率低
      • Child.prototype = new Parent();
      • Parent.call(this, name);
    • 关键是 Child.prototype 上面创建不必要的、多余的属性
  4. 寄生组合继承 ⭐️

    JavaScript
    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
    27
    function Parent(name){
    this.name = name
    }
    Parent.prototype.getName = function(){
    console.log(this.Name)
    }

    function Child(name,age){
    Parent.call(this,name)
    this.age = age
    }
    // Object.create() 方法创建一个空(没有属性和方法或者说没有 prototype)对象,使用现有的对象来提供新创 的对象的 __proto__。
    // Object.create = function (obj){
    // function Fn (){}
    // Fn.prototype = obj;
    // return new Fn();
    // // or
    // let o = {};
    // o.__proto__ = obj; // 非法,IE 浏览器不允许修改对象的 __proto__
    // return o;
    // }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    Child.prototype.getAge  = function(){
    console.log(this.age)
    }
    let c1 = new Child("yooo~");

    寄生组合继承:构造函数继承 + 类似原型继承

    特点:父类私有和公有属性分别是子类实例的私有和公有属性方法(推荐)

    引用《JavaScript 高级程序设计》中对寄生组合式继承的夸赞就是:

    这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ES6 的继承

JavaScript
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
// 定义父类
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
this.fn = function(){console.log("私有方法,占用每一个实例内存")}
}
// 改方法实际上是声明到了 Father.prototype 上
show() {
console.log(`我叫:${this.name}, 今年${this.age}岁`,"公共方法,只在 prototype 里开辟内存,每个实例都可以访问");
}
};

// 通过 extends 关键字实现继承
// 相当于 Son.prototype.__proto__ = Father.prototype
class Son extends Father {
// constructor 可以不写,默认为 constructor(...args){ super(...args) }
// 如果写了 constructor 必须写 super
constructor(name,age){
super(name,age); //→ 相当于 Father.call(this,name,age)
this.gender = 'ABO'
}
};

let son = new Son('Tim', 250);
son.show(); // 我叫:Tim, 今年 250 岁,公有。。。。

继承简单应用

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🌰
class login extends React.Component{
constructor(props){
super(props)
}
componentWillMonet(){
// React.Component.prototype.setState=...
this.setState()
}
render(){
}
}

🌰
class Utils{
query(){}
}
class Dialog extends Utils{
constrcutor(){
super()
this.query();
}
}

ES6 中新增了 class 关键字来定义类,通过保留的关键字 extends 实现了继承。实际上这些关键字只是一些语法糖,底层实现还是通过原型链之间的委托关联关系实现继承。

小结

区别于 ES5 的继承,ES6 的继承实现在于使用 super 关键字调用父类,反观 ES5 是通过 call 或者 apply 回调方法调用父类。

class (ES6) 与 ES5 继承区别

来源:Understanding ECMAScript 6

  1. class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const bar = new Bar(); // it's ok
    function Bar() {
    this.bar = 42;
    }

    const foo = new Foo(); // ReferenceError: Foo is not defined
    class Foo {
    constructor() {
    this.foo = 42;
    }
    }
  2. class 声明内部会启用严格模式。

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 引用一个未声明的变量
    function Bar() {
    baz = 42; // it's ok
    }
    const bar = new Bar();

    class Foo {
    constructor() {
    fol = 42; // ReferenceError: fol is not defined
    }
    }
    const foo = new Foo();
  3. class 的所有方法(包括静态方法和实例方法)都是不可枚举的。

    JavaScript
    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
    // 引用一个未声明的变量
    function Bar() {
    this.bar = 42;
    }
    Bar.answer = function() {
    return 42;
    };
    Bar.prototype.print = function() {
    console.log(this.bar);
    };
    const barKeys = Object.keys(Bar); // ['answer']
    const barProtoKeys = Object.keys(Bar.prototype); // ['print']

    class Foo {
    constructor() {
    this.foo = 42;
    }
    static answer() {
    return 42;
    }
    print() {
    console.log(this.foo);
    }
    }
    const fooKeys = Object.keys(Foo); // []
    const fooProtoKeys = Object.keys(Foo.prototype); // []
  4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function Bar() {
    this.bar = 42;
    }
    Bar.prototype.print = function() {
    console.log(this.bar);
    };

    const bar = new Bar();
    const barPrint = new bar.print(); // it's ok

    class Foo {
    constructor() {
    this.foo = 42;
    }
    print() {
    console.log(this.foo);
    }
    }
    const foo = new Foo();
    const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
  5. 必须使用 new 调用 class

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Bar() {
    this.bar = 42;
    }
    const bar = Bar(); // it's ok

    class Foo {
    constructor() {
    this.foo = 42;
    }
    }
    const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
  6. class 内部无法重写类名。

    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function Bar() {
    Bar = 'Baz'; // it's ok
    this.bar = 42;
    }
    const bar = new Bar();
    // Bar: 'Baz'
    // bar: Bar {bar: 42}

    class Foo {
    constructor() {
    this.foo = 42;
    Foo = 'Fol'; // TypeError: Assignment to constant variable
    }
    }
    const foo = new Foo();
    Foo = 'Fol'; // it's ok

拓展: 实现一个 new

new 创建过程

  1. 像普通函数一样执行,形成一个私有的作用域
    • 形参复制
    • 变量提升
  2. 默认创建一个对象,让函数的 this 指向这个对象,这个对象就是当前类的一个实例
  3. 代码执行
  4. 把对象返回
  1. 首先创建一个空的对象,空对象的__proto__属性指向构造函数的原型对象
  2. 把上面创建的空对象赋值构造函数内部的 this,用构造函数内部的方法修改空对象
  3. 如果构造函数返回一个非基本类型的值,则返回这个值,否则上面创建的对象

关于为什么最后要判断 return ret instanceof Object ? ret : obj instanceof Object 来判断是否是对象,包含 Array,Object,Function、RegExp、Date 构造函数是可以自己指定返回一个对象的

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _new(fn,...arg){
let obj = Object.create(fn.prototype);
let ret = fn.call(obj,...arg);
//return ret instanceof Object ? ret : obj;
return obj;
}

function A(d) {
this.d = d;
return {
a: 6
};
}
console.log(new A(123)); // {a: 6}
console.log(_new(A, 123)); // A {d: 123}
文章作者: Tim
文章链接: http://w3ctim.com/post/4c129b4e.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Tim

评论