探究JavaScript中的继承

  在开始讲继承之前,我们首先来了解一下JavaScript中关于构造函数、原型和原型链的一些知识,因为JavaScript是中的继承和这几者之间息息相关。

一、构造函数、原型和原型链的三角关系

  首先来看一段简单的代码:

//构造函数Foo
function Foo(name) {
    this.name = name;
};

//Foo的原型对象prototype中的方法
Foo.prototype.sayHello = function() {
    console.log('Hello,my name is ' + this.name + "!");
};

//使用Foo的constructor构造器实例化一个f1对象
var f1 = new Foo('Chen');
f1.sayHello();//输出:Hello,my name is Chen!

//_proto_指向原型对象prototype
console.log(f1.__proto__ === Foo.prototype); //true
复制代码

  接下来通过上面代码来讲一下构造函数、原型和原型链分别是什么,有什么作用:

  • 构造函数:在JavaScript中,用new关键字来调用定义的构造函数。默认通过constructor构造器实例化并返回的是一个新对象,这个新对象具有构造函数定义的变量/属性和方法,包括prototype,如Foo(大写);
  • 原型:每个构造函数都会有一个原型对象prototype,该对象上定义的所有属性和方法都会被实例对象所继承,如f1中继承的sayHello方法,我们将在下文中继续谈到;
  • 原型链:每个对象都会在其内部初始化一个隐式属性——_proto_,当我们访问一个对象的属性 时,如果这个对象内部不存在这个属性/方法,如f1中内部不存在sayHello,那么他就会去_proto_里找这个属性/方法。_proto_是一个指向其构造函数原型对象的指针,而原型对象也有自己的原型,通过各自的_proto_一直指向各自的原型对象,直到某个对象的原型为null,这种一级一级的链结构称为原型链。

  他们的关系可以用下面这个图简单表示:


  通过对几个概念以及他们之间的关系的认识后,相信你对"JavaScript是一门原型语言"这句话也会有一定的理解。接下来我们进入下一个重点——继承。

二、什么是JavaScript继承,而又怎么样继承呢?

继承概念

  相信许多人对继承都有自己的理解和定义,在这里我也提一下我对继承的认识:A对象能够访问B对象的属性,同时,A对象也能够添加自己的新属性、方法或者覆盖已存在的B对象的属性、方法,以上这种方式就叫做继承。

继承方式
1. 对象冒充
// 父类构造函数
var Parent = function(name){
    this.name = name;
    this.sayHello = function(){
        console.log("Hello, " + this.name + "!");
    }
};

// 子类构造函数
var Children = function(name){
    this.method = Parent;
    this.method(name); // 实现继承的关键
    this.getName = function(){
        console.log(this.name);
    }
};

var p = new Parent("parentName");
var c = new Children("childrenName");

p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getName(); // 输出: childrenName
复制代码

分析:
  构造函数使用this关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使Parent成为Children的方法,然后调用它。Children就会收到Parent的中定义的属性和方法。那为什么不直接执行,非要转个弯把Parent赋值给Childrenmethod属性再执行呢?这跟this的指向有关,在函数内this是指向window的。当将Parent赋值给Childrenmethod时,this就指向了Children类的实例。

2. 使用call、applay、bind改变this方法
// 父类构造函数
var Parent = function(name){
    this.name = name;
    this.sayHello = function(){
        console.log("Hello, " + this.name + "!");
    }
};

// 子类构造函数
var Children = function(name){
    Parent.call(this, name); // 实现继承的关键
    this.getName = function(){
        console.log(this.name);
    }
};

var p = new Parent("parentName");
var c = new Children("childrenName");

p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getName(); // 输出: childrenName
复制代码

分析:
  这种方法是与对象冒充方法相似的方法,因为它也是通过callapplybind改变了this的指向而实现继承。这里使用call(),其他两个类似,具体不细讲。

3. 原型链继承
// 父类构造函数
var Parent = function(){
    this.name = 'parentName';
    this.sayHello = function(){
        console.log("Hello, " + this.name + "!");
    }
};

// 子类构造函数
var Children = function(){};
Children.prototype = new Parent(); // 实现继承的关键

var p = new Parent();
var c = new Children();

p.sayHello(); // 输出: Hello,parentName!
c.sayHello(); // 输出: Hello, parentName!
复制代码

分析:
  一开始我们提到,如果我们每个构造函数都有一个prototype,而这里我们将Parent作为Children的原型对象,Children通过_proto_来找到原型Parent,并调用sayHello,实现了继承。注意这里实例对象时候没有传参。

4. 混合方式
// 父类构造函数
var Parent = function(name){
    this.name = name;
};
Parent.prototype.sayHello = function(){
        console.log("Hello, " + this.name + "!");
};

// 子类构造函数
var Children = function(name,age){
    Parent.call(this, name); // 实现继承的关键
    this.age = age;
};

Children.prototype = new Parent();// 实现继承的关键
Children.prototype.getAge = function(){
    console.log(this.age);
};

var p = new Parent("parentName");
var c = new Children("childrenName",18);

p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getAge(); // 输出: childrenName
复制代码

分析:
  对象冒充的主要问题是必须使用构造函数方式,这不是最好的选择。不过如果使用原型链,就无法使用带参数的构造函数了。如何选择呢?答案很简单,两者都用。在JavaScript中创建类的最好方式是用构造函数定义属性,用原型定义方法。将两者混合在一起,就要实现将在原型链方式下传参给构造函数实例化对象。

5. 使用Object.create 方法
// 父类构造函数
var Parent = function(name){
    this.name = name;
};
Parent.prototype.sayHello = function(){
        console.log("Hello, " + this.name + "!");
};

// 子类构造函数
var Children = function(name,age){
    Parent.call(this, name); // 实现继承的关键
    this.age = age;
};

Children.prototype=Object.create(Parent.prototype);//实现继承的关键
Children.prototype.constructor = Children;
Children.prototype.getAge = function(){
    console.log(this.age);
};

var p = new Parent("parentName");
var c = new Children("childrenName",18);

p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getAge(); // 输出: childrenName
复制代码

分析:
  Object.create方法会使用指定的原型对象及其属性去创建一个新的对象,当执行Children.prototype = Object.create(Parent.prototype)这个语句后,Childrenconstructor就被改变为Parent,因此需要将Children.prototype.constructor重新指定为Children自身。

6. ES6中extends关键字实现继承
// 父类
class Parent {
//父类构造器
  constructor(name) {
    this.name = name;
  }
}
// 子构造函数
class Children extends Parent {
//子类构造器
  constructor(name, age) {
    this.age = age; // 这里会报错
    super(name);//代表父类构造器
    this.age = age; // 正确
  }
  
  console.log()
}

复制代码

分析:
  Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
  此外,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
  super虽然代表了父类Parent的构造函数,但是返回的是子类Children的实例,即super内部的this指的是Children,因此super()在这里相当于Parent.prototype.constructor.call(this)

三、总结

  结合前面的内容,可以发现JavaScript 中的面向对象部分一直是在向 Java 靠拢的。尤其增加了 class 和 extends 关键字之后,靠拢了一大步。但这些并没有改变JavaScript是基于原型这一实质在JavaScript,继承所做工作实际上是在构造原型链,所有子类的实例共享的是同一个原型。所以JavaScript中调用父类的方法实际上是在不同的对象上调用同一个方法,即“方法借用”,这种行为实际上是委托调用。

转载于:https://juejin.im/post/5c316d62f265da61407f0ebc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值