JavaScript之继承

182 阅读6分钟

JavaScript之继承

本文记录JavaScript的几种继承方式

原型链继承

function Parent() {
	this.tpye = 'parent'
	this.age = 88
	this.like = ['🏀']
}
Parent.prototype.getAge = function () {
	return this.age
}
function Child() {
	this.type = 'child'
}

Child.prototype = new Parent()
const child = new Child()

console.log(child.getAge())  // 88
console.log(child.like)     // [ '🏀' ]
console.log(child.type)     // child

以上方法就实现了原型链继承,这种继承方法存在一些问题:

  • 引用类型的属性被所有实例共享
  • 创建子类型的实例时,不能向父类型传参数

如下: 我们只改变了child1like属性的值,child2like属性对应的值也会改变了。因为两个实例使用的是同一个原型对象,它们的内存空间是共享的

const child1 = new Child()
const child2 = new Child()

console.log(child1.like,  child2.like) // [ '🏀' ] [ '🏀' ]
child1.like.push('🏓️')
console.log(child1.like,  child2.like) // [ '🏀', '🏓️' ] [ '🏀', '🏓️' ]

构造函数继承

function Parent(fnName) {
	this.tpye = 'parent'
	this.age = 88
	this.like = ['🏀']
	this.fnName = fnName
}
Parent.prototype.getAge = function () {
	return this.age
}
function Child(fnName) {
	Parent.call(this, fnName)
	this.type = 'child'
}

const child1 = new Child('child1')
const child2 = new Child('child2')

console.log(child1.like,  child2.like) // [ '🏀' ] [ '🏀' ]
child1.like.push('🏓️')
console.log(child1.like,  child2.like) // [ '🏀', '🏓️' ] [ '🏀']
console.log(child1.fnName, child2.fnName) // child1 child2
/* 没有继承父实例原型中的属性和方法 */
console.log(child1.getAge, child2.getAge) // undefined undefined

我们借用call函数来实现构造函数继承

优点

  • 解决了原型链继承中实例共享问题
  • 创建子类型的实例时,可以向父类型传参数

缺点

  • 每次创建实例,都会调用一次父级构造函数
  • 无法继承父类型原型链中的属性和方法

组合继承

组合继承综合原型链继承构造函数继承,将两者优点集中起来。通过原型链继承原型上的属性和方法,通过构造函数继承实例中的属性

function Parent(fnName) {
	this.age = 88
	this.like = ['🏀']
	this.fnName = fnName
}
Parent.prototype.getAge = function () {
	return this.age
}
function Child(fnName) {
	Parent.call(this, fnName)
}

Child.prototype = new Parent()
Child.prototype.getFnName = function () {
	return this.fnName
}
const child1 = new Child('child1')
const child2 = new Child('child2')

console.log(child1.like,  child2.like) // [ '🏀' ] [ '🏀' ]
child1.like.push('🏓️')                 
console.log(child1.like,  child2.like) // [ '🏀', '🏓️' ] [ '🏀' ]

console.log(child1.getAge(), child2.getAge()) // 88 88
console.log(child1.getFnName(), child2.getFnName()) // child1 child2

组合继承弥补了原型链继承构造函数继承的缺点,是JavaScript中使用最多的继承模式。

原型式继承

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

类似于ES5Object.create实现,创建一个构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个构造函数的实例。其实就是对传入对象进行浅拷贝,并且返回浅拷贝得到的对象。

缺点

  • 原型链继承一样,存在引用类型的属性被所有实例共享的问题
const parent = {
	like: ['🏀'],
	age: 88,
	getAge() {
		return this.age
	}
}

const child1 = create(parent)
child1.like.push('🏸️')
console.log(child1.like) // [ '🏀', '🏸️' ]

const child2 = create(parent)
child2.like.push('⚽️')
console.log(child2.like) // [ '🏀', '🏸️', '⚽️' ] [ '🏀', '🏸️', '⚽️' ]

寄生式继承

使用原型式继承可以获取一份目标对象的浅拷贝,得到一个新的对象,再对这个对象进行扩展,这样的方式叫做寄生式继承。它的缺点和原型式继承一样,但是可以对目标对象进行扩展,添加对应的属性和方法

function createObj(o) {
	const newObj = Object.create(o)
	newObj.getAge = function () {
		return 88
	}
}

const child1 = createObj(parent),
	  child2 = createObj(parent)

console.log(child1.getAge()) // 88
console.log(child1.like, child2.like) // [ '🏀' ] [ '🏀' ]
child2.like.push('🏓️')
console.log(child1.like, child2.like) [ '🏀', '🏓️' ] [ '🏀', '🏓️' ]

寄生组合继承

结合以上几种继承方法,得出最优解的寄生组合继承。

寄生组合继承继承父类型实例上的属性时,使用构造函数继承,继承父类原型中的属性和方法时候,使用原型式继承

function Parent() {
	this.age = 88
	this.like = ['🏀']
}
Parent.prototype.getAge = function () {
	return this.age
}

function Child () {
	Parent.call(this)
	this.name = 'child'
}

function inheritPrototype(Child, Parent) {
	Child.prototype = Object.create(Parent.prototype)
	Child.prototype.constructor = Child
}

inheritPrototype(Child, Parent)

const child1 = new Child()
	
console.log(child1.like, child1.getAge(), child1.name) // [ '🏀' ] 88 child


const child2 = new Child()

console.log(child2.like) // [ '🏀' ]

child2.like.push('🏓️')

console.log(child1.like, child2.like) // [ '🏀' ] [ '🏀', '🏓️' ]

寄生组合继承只调用一次Parent构造函数,避免了子类原型上不必要的属性,效率更高,而且,原型链仍然保持不变,因此instanceof操作符和isPrototypeOf()方法可以正常使用

ES6中的extends继承

Class使用extends继承,上面的寄生组合继承使用ES6来写就是

class Parent {
	age = 88
	like = ['🏀']
	getAge() {
		return this.age
	}
}

class Child extends Parent {
	constructor() {
		// 这里super相当于Parent.call(this),调用了父类的构造函数
		super() 
		this.name = 'child'
	}
}

const child1 = new Child()

console.log(child1.like, child1.getAge(), child1.name) // [ '🏀' ] 88 child

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

Class中,父类的静态方法也可以被子类继承

class Parent {
	static age = 88
}

class Child extends Parent {}

console.log(Child.age)

可以看出,Class中的继承存在两条继承链

  • Child.__proto__指向Parent

  • Child.prototype.__proto__指向Parent.prototype

class Parent {}

class Child extends Parent {}

console.log(Child.__proto__ === Parent) // true
console.log(Child.prototype.__proto__ === Parent.prototype) // true

最上面的继承使用babel编译后如下:

"use strict";

function _typeof(obj) {
	"@babel/helpers - typeof";
	if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
		_typeof = function _typeof(obj) {
			return typeof obj;
		};
	} else {
		_typeof = function _typeof(obj) {
			return obj &&
			typeof Symbol === "function" &&
			obj.constructor === Symbol &&
			obj !== Symbol.prototype
			? "symbol"
			: typeof obj;
		};
	}
	return _typeof(obj);
}

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

function _setPrototypeOf(o, p) {
	_setPrototypeOf =
	Object.setPrototypeOf ||
	function _setPrototypeOf(o, p) {
		o.__proto__ = p;
		return o;
	};
	return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
	var hasNativeReflectConstruct = _isNativeReflectConstruct();
	return function _createSuperInternal() {
		var Super = _getPrototypeOf(Derived),
		result;
		if (hasNativeReflectConstruct) {
			var NewTarget = _getPrototypeOf(this).constructor;
			result = Reflect.construct(Super, arguments, NewTarget);
		} else {
			result = Super.apply(this, arguments);
		}
		return _possibleConstructorReturn(this, result);
	};
}

function _possibleConstructorReturn(self, call) {
	if (call && (_typeof(call) === "object" || typeof call === "function")) {
		return call;
	}
	return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
	if (self === void 0) {
		throw new ReferenceError(
			"this hasn't been initialised - super() hasn't been called"
		);
	}
	return self;
}

function _isNativeReflectConstruct() {
	if (typeof Reflect === "undefined" || !Reflect.construct) return false;
	if (Reflect.construct.sham) return false;
	if (typeof Proxy === "function") return true;
	try {
		Boolean.prototype.valueOf.call(
			Reflect.construct(Boolean, [], function () {})
		);
		return true;
	} catch (e) {
		return false;
	}
}

function _getPrototypeOf(o) {
	_getPrototypeOf = Object.setPrototypeOf
	? Object.getPrototypeOf
	: function _getPrototypeOf(o) {
		return o.__proto__ || Object.getPrototypeOf(o);
	};
	return _getPrototypeOf(o);
}

function _classCallCheck(instance, Constructor) {
	if (!(instance instanceof Constructor)) {
		throw new TypeError("Cannot call a class as a 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);
	}
}

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

function _defineProperty(obj, key, value) {
	if (key in obj) {
		Object.defineProperty(obj, key, {
			value: value,
			enumerable: true,
			configurable: true,
			writable: true
		});
	} else {
		obj[key] = value;
	}
	return obj;
}

var Parent = /*#__PURE__*/ (function () {
	function Parent() {
		_classCallCheck(this, Parent);
		
		_defineProperty(this, "age", 88);
		
		_defineProperty(this, "like", ["🏀"]);
	}
	
	_createClass(Parent, [
		{
			key: "getAge",
			value: function getAge() {
				return this.age;
			}
		}
	]);
	
	return Parent;
})();

var Child = /*#__PURE__*/ (function (_Parent) {
	_inherits(Child, _Parent);
	
	var _super = _createSuper(Child);
	
	function Child() {
		var _this;
		
		_classCallCheck(this, Child);
		
		// 这里super相当于Parent.call(this),调用了父类的构造函数
		_this = _super.call(this);
		_this.name = "child";
		return _this;
	}
	
	return Child;
})(Parent);

var child1 = new Child();
console.log(child1.like, child1.getAge(), child1.name); // [ '🏀' ] 88 child

主要看一下这一段,这就是寄生组合继承中继承原型的方法

function _inherits(subClass, superClass) {
  // 判断extends后面跟着的父类,只能是一个函数或者null
	if (typeof superClass !== "function" && superClass !== null) {
		throw new TypeError("Super expression must either be null or a function");
	}
  // 将Child.prototype.__proto__指向Parent.prototype
	subClass.prototype = Object.create(superClass && superClass.prototype, {
		constructor: { value: subClass, writable: true, configurable: true }
	});
  //将Child.__proto__指向Parent
	if (superClass) _setPrototypeOf(subClass, superClass);
}

Object.create方法还可以传入第二个参数,为新创建的对象添加指定的属性值和对应的属性描述符。具体看MDN中的描述

_possibleConstructorReturn

看一下这个函数在何处调用

function _createSuper(Derived) {
 var hasNativeReflectConstruct = _isNativeReflectConstruct();
 return function _createSuperInternal() {
  /* 这里的Derived就是Child,获取Child.__proto__,那就是Parent,
  下面这一堆代码就是调用父类构造函数,得到返回值,result = Parent.call(this) */
  var Super = _getPrototypeOf(Derived),
  result;
  if (hasNativeReflectConstruct) {
   var NewTarget = _getPrototypeOf(this).constructor;
   result = Reflect.construct(Super, arguments, NewTarget);
  } else {
   result = Super.apply(this, arguments);
  }
  // 传入子类实例,和父级构造函数执行后的返回值
  return _possibleConstructorReturn(this, result);
 };
}


// 在Child中有这个调用,传入了Child本身
var _super = _createSuper(Child);


function _possibleConstructorReturn(self, call) {
	if (call && (_typeof(call) === "object" || typeof call === "function")) {
		return call;
	}
	return _assertThisInitialized(self);
}

简化写就是var _this = _possibleConstructorReturn(this, Parent.call(this));,得到了_this,然后根据子类构造函数逻辑对_this执行对应操作并返回

function Child() {
  var _this;

  _classCallCheck(this, Child);

  // 这里super相当于Parent.call(this),调用了父类的构造函数,拿到返回值
  _this = _super.call(this);
      // 根据子类构造函数逻辑对`_this`执行对应操作并返回
  _this.name = "child";
  return _this;
}

我们可能在父类构造函数中返回一个值,如果父类型构造函数返回一个object/function类型的值,就返回该值,如果返回null/undefined/基本类型的值就返回子类的this

class Parent {
	constructor() {
		return { age: 1 }
	}
}

class Child extends Parent {}

const child = new Child()

console.log(child) // { age: 1 }

extends继承总结如下

  • 建立Child和Parent之间的原型链关系

  • Child.__proto__指向Parent

  • Child.prototype.__proto__指向Parent.prototype

  • 调用Parent.call(this),根据Parent构造函数的返回值类型确定子类构造函数this的初始值_this

  • 根据子类构造函数对_this进行扩展或者其他操作等,并且最终返回_this