>  기사  >  웹 프론트엔드  >  JavaScript 상속 원칙에 대한 종합 분석

JavaScript 상속 원칙에 대한 종합 분석

不言
不言원래의
2018-09-03 10:13:391223검색

이 글은 JavaScript 상속의 원리에 대한 포괄적인 분석을 제공합니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

상속

우리는 JS가 OO 프로그래밍이라는 것을 알고 있으며 당연히 OO 프로그래밍의 기능은 필수입니다. 프로토타입을 배운 후 철이 뜨거워지는 동안 OO 프로그래밍의 세 가지 주요 기능 중 하나인 상속에 대해 이야기해 보겠습니다.

상속이라는 단어를 이해하기가 더 쉬울 것입니다. 우리는 재산 상속, 가업 상속 등에 익숙합니다. 그들의 전제는 상속인이 있고 상속인이 있으므로 상속이 있다는 것입니다. 맞습니다. 아시다시피 JS의 상속도 쌍으로 나타납니다.
상속은 상속이 필요한 객체에 객체의 속성을 복사하는 것입니다

OO 언어는 인터페이스 상속과 구현 상속이라는 두 가지 상속 방법을 지원합니다. 그 중 인터페이스 상속은 메서드 시그니처만 상속하는 반면. 상속 실제 메소드 를 상속합니다. ECMAScript의 함수에는 시그니처가 없으므로 인터페이스 상속은 구현할 수 없으며 구현 상속만 지원됩니다. 프로토타입 체인을 이해하려면 먼저 프로토타입이 무엇인지 알아야 합니다. 이해가 안가는 친구 여러분, 이 글을 읽어보세요JavaScript 프로토타입이 무엇인가요? 자바스크립트 프로토타입에 대한 자세한 설명 사실 상속은 단순히

① 부모가 있어야 하며

② 이 부모의 모든 인스턴스와 메소드를 얻음을 의미합니다

여기 위에서 언급한 작은 개념이 있습니다

서명이 없습니다

. 나는 이 용어를 처음 봤을 때 그 용어를 잘 이해하지 못했습니다. 검색한 후에는 이 진술이 상당히 수용 가능하다는 것을 알았습니다. 서명 없음

JS는 약한 유형의 언어이며 해당 매개변수는 0개 이상의 값 배열로 표시될 수 있습니다. 이는 단지 편의를 위한 것이지만 꼭 필요한 것은 아닙니다. 즉,

이름이 지정되지 않은 매개변수와 매개변수 전달 여부 사이에 필요한 연결이 없습니다. 매개변수에 이름을 지정할 수 있지만 전달할 수는 없습니다(현재 기본값은 정의되지 않음). 또는 매개변수 이름을 지정할 수 없지만 매개변수를 전달할 수 있습니다
. JS에서 작성하는 것은 합법적인 반면, 강력한 유형의 언어는 이에 대해 매우 엄격한 요구사항을 갖습니다. 일단 여러 매개변수가 정의되면 여러 매개변수를 전달해야 합니다. 이름이 지정된 매개변수를 사용하려면 함수 서명을 미리 생성해야 하며 향후 호출은 이 서명과 일치해야 합니다. (즉, 여러 매개변수를 정의하면 여러 매개변수를 전달해야 합니다.)** 하지만 js에는 이러한 규칙과 규정이 없으며 파서가 명명된 매개변수를 확인하지 않으므로 js에는 서명이 없습니다. 예를 들어주세요

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

이 예는 매우 분명합니다. 명명된 매개변수는 비어 있지만 여전히 매개변수를 전달하고 메서드를 호출할 수 있습니다. JS는 소위 매개변수 유형, 매개변수 수, 매개변수 위치, 들어오고 나가는 매개변수에 대해 신경 쓰지 않습니다. 값을 반환해야 하는 경우 선언 없이 반환하면 됩니다. 이를 서명 없는 JS라고 합니다.

프로토타입 체인

프로토타입 체인이란 무엇인가요? 문자 그대로의 의미는 이해하기 쉽습니다. 즉, 모든 프로토타입을 하나로 묶는 것을 프로토타입 체인이라고 합니다. 물론 이 설명은 단지 이해를 돕기 위한 것입니다. 프로토타입 체인은 상속을 구현하는 주요 방법입니다. 기본 아이디어는 프로토타입을 사용하여 한 참조 유형이 다른 참조 유형의 속성과 메서드를 상속하도록 하는 것입니다. 우리는 각 생성자에 프로토타입 개체가 있고, 프로토타입 개체에 생성자에 대한 포인터가 포함되어 있으며, 인스턴스에 프로토타입에 대한 내부 포인터가 포함되어 있다는 것을 알고 있습니다. 이때 프로토타입 객체를 다른 유형의 인스턴스와 동일하게 만들면 이때 프로토타입 객체에는 다른 프로토타입에 대한 포인터가 포함됩니다. 이에 따라 다른 프로토타입에도 이 반복 연결 관계가 포함됩니다. 프로토타입 체인인 인스턴스와 프로토타입의 체인을 구성합니다.

직접 말하면 인스턴스 → 프로토타입 → 인스턴스 → 프로토타입 → 인스턴스... 연결은 프로토타입 체인입니다. 저는

상속이 프로토타입 체인

의 한 형태라고 생각합니다. 프로토타입 체인을 알고 나면 어떻게 사용하는지 알아야 합니다. ECMA는 프로토타입 체인의 기본 모드 세트를 제공합니다.

프로토타입 체인의 기본 모드

// 创建一个父类
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()); // 命名最头痛
호출할 때 인스턴스.getFatherValue()를 사용하면 3번의 검색을 거치게 됩니다. Step

①인스턴스 검색

②ChildType.prototype 검색

③FatherType.prototype 검색 이 단계에서 해당 속성이나 메서드를 찾을 수 없으면 검색 프로세스를 항상 수행해야 합니다. 하나씩 앞으로 나아갑니다. 프로토타입 체인의 끝에 도달할 때까지 멈추지 않습니다.

이때 프로토타입 체인은 인스턴스 → ChildType.prototype → FatherType.prototype입니다.

instance.getFatherValue()를 실행한 후 getFatherValue의 이는 ChildType입니다. 이때 ChildType은 프로토타입 체인에 따라 fatherName 속성을 찾습니다. 마지막으로 FatherType 에서 찾으세요.

이때 instance.constructor는 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原型和继承

위 내용은 JavaScript 상속 원칙에 대한 종합 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.