この記事は、JavaScript 継承の原則についての包括的な分析を提供します。必要な方は参考にしていただければ幸いです。
JS が OO プログラミングであることはわかっており、OO プログラミングの機能は当然不可欠です。プロトタイプを学習した後は、鉄は熱いうちに打ち、OO プログラミングの 3 つの主要な機能の 1 つである継承について話しましょう。
相続という言葉はもっと分かりやすいはずです。私たちは財産の相続、家業の相続などに慣れています。彼らの前提は、相続人がいて、あなたが相続人であるため、相続が存在するということです。そうです、JS の継承は、ご存知のとおり、ペアでも表示されます。
継承とは、オブジェクトのプロパティを継承する必要のあるオブジェクトにコピーすることです
OO言語は、インターフェース継承と実装継承の2つの継承方法をサポートします。このうち、インターフェース継承のみメソッドの署名が継承されます。継承 は実際のメソッド を継承します。 ECMAScript の関数にはシグネチャがないため、実装の継承のみがサポートされています。プロトタイプ チェーンを理解するには、まずプロトタイプを理解する必要があります。理解できない場合は、この記事を読んでください JavaScript プロトタイプとは何ですか? 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()); // 命名最头痛
②ChildType.prototype の検索
③FatherType.prototype の検索という 3 つの検索が行われます。このステップで属性またはメソッドが見つからない場合、検索プロセスは常に行う必要があります。一つずつ前進し、プロトタイプチェーンの最後に到達するまで停止しません。 この時のプロトタイプチェーンはinstance→ChildType.prototype→FatherType.prototypeですinstance.getFatherValue()を実行すると、getFatherValueのこれがChildTypeとなり、ChildTypeはプロトタイプチェーンに従って、fatherName属性を探します。そして最後に FatherType でそれを見つけます。
この時点では、instance.constructor は FatherType を指しています
所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的,因此,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object。prototype,这也就是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。
Array类型也是继承了Object类型的。
因此,我们可以总结一下,在原型链的最顶端就是Object类型,所有的函数默认都继承了Object中的属性。
在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 継承の原理の包括的な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。