Home  >  Article  >  Web Front-end  >  Comprehensive analysis of the principles of JavaScript inheritance

Comprehensive analysis of the principles of JavaScript inheritance

不言
不言Original
2018-09-03 10:13:391267browse

This article brings you a comprehensive analysis of the principles of JavaScript inheritance. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

Inheritance

We know that JS is OO programming, and the characteristics of OO programming are naturally indispensable. After learning the prototype, let’s strike while the iron is hot and talk about one of the three major characteristics of OO programming-inheritance .

The word inheritance should be easier to understand. We are familiar with inheritance of property, inheritance of family business, etc. Their premise is that there is an heir, and then you are the heir, so that there is inheritance. That's right, inheritance in JS, as you understand, also appears in pairs.
Inheritance is to copy the properties of an object to the object that needs to be inherited

OO language supports two inheritance methods: interface inheritance and implementation inheritance, of which interface inheritance only Inheriting the method signature , while implementation inheritance inherits the actual method . Since functions in ECMAScript do not have signatures, interface inheritance cannot be implemented and only implementation inheritance is supported. The main way of inheritance is through the prototype chain. To understand the prototype chain, you must first know what a prototype is. For those who don’t understand, you can read this articleWhat is the JavaScript prototype? Detailed explanation of javascript prototypeIn fact, inheritance simply means

①There must be a parent on it

②And it obtains all instances and methods of this parent

Here is a small concept. The

mentioned above has no signature

. I didn’t quite understand the word when I saw it for the first time. After searching, I feel that this statement is quite acceptable. No signature

We know that JS is a weakly typed language, and its parameters can be represented by an array of 0 or more values. We can name parameters for JS functions. This is just for convenience, but It is not necessary, that is to say,

There is no necessary connection between whether we specify unnamed parameters and whether to pass parameters. We can name parameters but not pass them (the default is undefined at this time), or we can not name parameters but pass parameters
, this way of writing is legal in JS. On the contrary, strongly typed languages ​​have very strict requirements for this. Once several parameters are defined, several parameters must be passed. Named parameters must require a function signature to be created in advance, and future calls must be consistent with this signature. (That is to say, if you define several parameters, you need to pass several parameters)**, but js does not have these rules and regulations, and the parser will not verify the named parameters, so js does not have a signature. Give an example

function JSNoSignature () {  
  console.log("first params" + arguments[0] + "," + "second params" + arguments[1]);
}
JSNoSignature ("hello", "world");

This example is obvious. The named parameter is empty, but we can still pass parameters and call the method. JS doesn't care about the so-called parameter type, number of parameters, parameter position, incoming and outgoing parameters. All its values ​​are put into arguments. If you need to return a value, just return it without a declaration. This is called JS without signature.

Prototype chain

What is the prototype chain? The literal meaning is easy to understand, that is, stringing all prototypes together is called a prototype chain. Of course, this explanation is just for the convenience of understanding. The prototype chain is the main method to implement inheritance. The basic idea is to

use prototypes

to let one reference type inherit the properties and methods of another reference type . We know that each constructor has a prototype object, the prototype object contains a pointer to the constructor, and the instance contains an internal pointer to the prototype. At this time, if we make the prototype object equal to an instance of another type, then the prototype object at this time will contain a pointer to another prototype. Correspondingly, the other prototype also contains a pointer to another constructor. This recurring connection relationship constitutes the chain of instances and prototypes, which is the prototype chain.

To put it bluntly, it is instance → prototype → instance → prototype → instance... The connection is the prototype chain.

I think

Inheritance is a form of prototype chain

. After we know the prototype chain, we need to know how to use it. ECMA provides a set of basic modes of the prototype chain. The basic modes are as follows

The basic mode of the prototype chain

// 创建一个父类
function FatherType(){
    this.fatherName = '命名最头痛';
}

FatherType.prototype.getFatherValue = function() {
    return this.fatherName;
}

function ChildType(){
    this.childName = 'George';
}

// 继承了FatherType,即将一个实例赋值给函数原型,我们就说这个原型继承了另一个函数实例
// 将子类的原型指向这个父类的实例
ChildType.prototype = new FatherType();

ChildType.prototype.getChildValue = function() {
    return this.childName;
}

let instance = new ChildType();
console.log(instance.getFatherValue()); // 命名最头痛

When calling instance.getFatherValue(), it will go through three search steps

①Search instance

②Search ChildType.prototype
③Search FatherType.prototype. At this time, the method is found in this step and the attribute cannot be found. Or method, the search process always has to go forward one by one to the end of the prototype chain before stopping.

The prototype chain at this time is instance → ChildType.prototype → FatherType.prototype

After executing instance.getFatherValue(), this in getFatherValue is ChildType. At this time, ChildType will find the fatherName attribute according to the prototype chain. , eventually found in FatherType.


At this time instance.constructor points to FatherType

默认的原型

所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的,因此,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object。prototype,这也就是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。
Array类型也是继承了Object类型的。
因此,我们可以总结一下,在原型链的最顶端就是Object类型,所有的函数默认都继承了Object中的属性。

原型和实例关系的确认

isPrototypeOf方法

javascript原型是什么?javascript原型的详细解说中我们有提到过isPrototypeOf方法可以用于判断这个实例的指针是否指向这个原型,这一章我们学习了原型链,这里做个补充,按照原型链的先后顺序,isPrototypeOf方法可以用于判断这个实例是否属于这个原型的。

依旧用上面那个例子
// 注意,这里用的是原型,Object.prototype,FatherType.prototype,ChildType.prototype
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(FatherType.prototype.isPrototypeOf(instance)); // true
console.log(ChildType.prototype.isPrototypeOf(instance)); // true

下面再介绍另一种方法,通过instanceof操作符,也可以确定原型和实例之间的关系

instanceof操作符

instanceof操作符是用来测试原型链中的构造函数是否有这个实例

function FatherType(){
    this.fatherName = '命名最头痛';
}

FatherType.prototype.getFatherValue = function() {
    return this.fatherName;
}

function ChildType(){
    this.childName = 'George';
}

// 继承了FatherType
ChildType.prototype = new FatherType();

// 创建实例
let instance = new ChildType();

// 为ChildType原型上添加新方法,要放在继承FatherType之后,这是因为new FatherType()会将ChildType原型上添加的新方法全部覆盖掉

ChildType.prototype.getChildValue = function() {
    return this.childName;
}

// 此时getFatherValue被重写了
ChildType.prototype.getFatherValue = function() {
    return true
}

console.log(instance.getFatherValue()); // true

②通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。这部分的例子和解释在javascript原型是什么?javascript原型的详细解说中已经表述过了。一样的道理,只不过把原型换成了原型链罢了。

原型链的bug

原型链虽然强大,可以用它来实现继承,但是也是存在bug的,它最大的bug来自包含引用类型值的原型。也就是说原型链上面定义的原型属性会被所有的实例共享。
它还有另外一个bug,即在创建子类型的实例时,不能向父类型(超类型)的构造函数中传递参数。或者说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
基于以上这两个原因,实践过程中很少会单独使用原型链

借用构造函数

其设计思想就是在子类型构造函数的内部调用父类(超类)构造函数。
由于函数只不过是在特定环境中执行代码的对象,因此通过apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。

function FatherType() {
  this.name = 'George';
} 

function ChildType() {
  //通过call方法改变this的指向,此时FatherType中的this指的是ChildType,相当于在构造函数中定义自己的属性。
  FatherType.call(this);
}

let instance1 = new ChildType(); 
instance1.name = '命名最头痛';
console.log(instance1.name); // '命名最头痛'

let instance2 = new ChildType();
console.log(instance2.name); // George

通过上述方法很好解决了原型属性共享问题,此外,既然是一个函数,它也能传相应的参数,因此也能实现在子类型构造函数中向超类型构造函数传递参数。

function FatherType(name){
  this.name = name
}
function ChildType(){
  FatherType.call(this, "George");
  this.age = 18
}
let instance = new ChildType();
console.log(instance.name);  // George
console.log(instance.age);   // 18

借用构造函数的问题
借用构造函数,方法都在构造函数中定义,那么函数的复用就无从谈起,而且在父类(超类型)的原型定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

组合继承

组合继承也叫伪经典继承,其设计思想是将原型链和借用构造函数的技术组合到一块,发挥二者之长的一种继承模式,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function FatherType(name){
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

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

// 借用构造函数实现对实例的继承
function ChildType(name, age){
  // 使用call方法继承FatherType中的属性
  FatherType.call(this, name);
  this.age = age
}

// 利用原型链实现对原型属性和方法的继承
ChildType.prototype = new FatherType(); //将FatherType的实例赋值给ChildType原型
ChildType.prototype.constructor = ChildType; // 让ChildType的原型指向ChildType函数
ChildType.prototype.sayAge = function(){
  console.log(this.age)
}
let instance1 = new ChildType('命名最头痛', 18);
instance1.colors.push('black');
console.log(instance1.colors);         // 'red, blue, green, black'
instance1.sayName();
instance1.sayAge();

var instance2 = new ChildType('命名最头痛', 18);
console.log(instance2.colors);         // 'red, blue, green'
instance2.sayName();                   // '命名最头痛'
instance2.sayAge();                    // 18

组合继承方式避免了原型链和借用构造函数的缺陷,是JS中常用的继承方式。

原型链继承

原型链继承没有使用严格意义上的构造函数,其思想是基于已有的对象创建新对象

// 此object函数返回一个实例, 实际上object()对传入其中的对象执行了一次浅复制.
function object(o) {
  function F() {}  // 创建一个临时构造函数
  F.prototype = o; // 将传入的对象作为构造函数的原型
  return new F();  // 返回这个临时构造函数的新实例
}

let demo = {
  name: 'George',
  like: ['apple', 'dog']
}

let demo1 = object(demo);
demo1.name = '命名';     // 基本类型
demo1.like.push('cat'); // 引用类型共用一个内存地址

let demo2 = object(demo);
demo2.name = '头痛';    // 基本类型
demo2.like.push('chicken') // 引用类型共用一个内存地址
console.log(demo.name) // George
console.log(demo.like) // ["apple", "dog", "cat", "chicken"]

原型链继承的前提是必须要有一个对象可以作为另一个对象的基础。通过object()函数生成新对象后,再根据需求对新对象进行修改即可。 由于新对象(demo1, demo2)是将传入对象(demo)作为原型的,因此当涉及到引用类型时,他们会共用一个内存地址,引用类型会被所有实例所共享,实际上相当于创建了demo对象的两个副本。

Object.create()方法

ECMA5中新增Object.create()方法规范化了原型式继承。该方法接收两个参数
①基础对象,这个参数的实际作用是定义了模板对象中有的属性,就像上面例子中的demo,只有一个参数情况下,Object.create()与上例子中的object相同
②这个是可选参数,一个为基础对象定义额外属性的对象, 该对象的书写格式与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

// 只有一个参数
var demoObj = {
  name: 'George',
  like: ['apple', 'dog', 'cat']
}
let demo1Obj = Object.create(demoObj);
demo1Obj.name = '命名';
demo1Obj.like.push('banana');

let demo2Obj = Object.create(demoObj);
demo2Obj.name = '头痛';
demo2Obj.like.push('walk');

console.log(demoObj.like) //["apple", "dog", "cat", "banana", "walk"]

// 两个参数
var demoObj = {
  name: 'George',
  like: ['apple', 'dog', 'cat']
}

let demo1Obj = Object.create(demoObj, {
  name: {
    value:'命名'
  },
  like:{
    value: ['monkey']
  },
  new_val: {
    value: 'new_val'
  }
});
console.log(demoObj.name) // George
console.log(demo1Obj.name) // 命名
console.log(demo1Obj.like) // ["monkey"]
console.log(demo1Obj.new_val) // new_val
console.log(Object.getOwnPropertyDescriptor(demo1Obj,'new_val')) // {value: "new_val", writable: false, enumerable: false, configurable: false}

如果只想让一个对象与另一个对象保持类型的情况下,原型式继承是完全可以胜任的,不过要注意的是,引用类型值的属性始终都会共享相应的值。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,其设计思想与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回一个对象。

// 这个函数所返回的对象,既有original的所有属性和方法,也有自己的sayHello方法
function createAnother(original) {
  let clone = Object.create(original);
  clone.sayHello = function(){            
    console.log('HELLO WORLD')
  }
  return clone;
}

let person = {
  name: 'George',
  foods: ['apple', 'banana']
}

let anotherPerson = createAnother(person);
anotherPerson.sayHello();  // HELLO WORLD

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其背后思想:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。说白了就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(childType, fatherType){
  let fatherObj = Object.create(fatherType.prototype);  // 创建对象
  fatherObj.constructor = childType;   // 弥补重写原型而失去的默认constructor属性
  childType.prototype = fatherObj;     // 指定对象
}

上例是寄生组合式继承最简单的形式,这个函数接受两个参数:子类型构造函数和超类型构造函数,在函数内部,①创建了父类型原型的一个副本,②为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。③将新创建的对象(即副本)赋值给子类型的原型。

function FatherType(name){
  this.name = name;
  this.foods = ['apple', 'banana'];
}
FatherType.prototype.sayName = function(){
  console.log(this.name)
}
function ChildType(name, age){
  FatherType.call(this, name);
  this.age = age;
}
inheritPrototype(ChildType, FatherType);
ChildType.prototype.sayAge = function(){
  console.log(this.age)
}

总结

JS继承的主要方式是通过原型链实现的

实例-原型-实例-原型...无限链接下去就是原型链

所有引用类型的默认原型都是Object

instanceof操作符和isPrototypeOf方法都可以用于判断实例与原型的关系,其区别是,前者用的是原型,后者用的是构造函数

给原型添加方法的代码一定要放在继承之后,这是因为,在继承的时候被继承者会覆盖掉继承者原型上的所有方法

Object.create()方法用于创建一个新对象,其属性会放置在该对象的原型上

继承有6种方式,分别是原型链,借用构造函数,组合继承,原型式继承,寄生式继承和寄生组合式继承

相关推荐:

JavaScript中的继承之类继承_javascript技巧

JavaScript原型和继承

The above is the detailed content of Comprehensive analysis of the principles of JavaScript inheritance. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn