本文首发于个人博客:www.wyb.plus
1. 原型链深入分析
构造函数、原型和实例的关系 >>>
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针
原型链的概念 >>>
一个实例的原型可以是另一个构造函数的实例, 这个可以无限嵌套,这就是所谓原型链的基本概念.
function GrandFather() { this.age = 99; } GrandFather.prototype.sex = "男"; function Father() { this.name = "Father"; } Father.prototype = new GrandFather(); Father.prototype.height = 180; let son = new Father()
注意:不是Father的原型的constructor 属性被重写了,而是Father的原型指向了另一个对象---->GrandFather的原型,而这个原型对象的constructor 属性指向的是GrandFather 。
究极版原型链 >>>
2. 经典继承(伪造对象)
函数只不过是在特定环境中执行代码的对象!!!
这句话是我们本节的核心哲学
2.1 经典继承之call
当我们在一个全局环境中已经创造了若干的对象及其对应方法时, 有些时候需要实现一个方法借用的功能 :
例 : 对象o1有一个方法a是输出对象的name值, 而对象o2没有该方法, 那么如果我们也想输出o2的值的话, 那么我们就可以把对象o1的值借过来用用
let o1 = { name: "wangyubo", age: 18, add: function(num1, num2) { console.log(num1 + num2); } } let o2 = { name: "wyb", age: 28, sayAge: function() { console.log(this.age); } }
- o1本来没有sayAge的方法 , 从o2借过来之后 , 它就有了 , 并且输出了o1中的age
- o2本来没有add的方法 , 从o1借过来之后 ,它也有了 , 并且按照add方法传参之后输出了结果
call
的语法fn.call( {} ,参数1,参数2… ) // .call左侧是要借用的那个函数 // .call右侧括号中第一个参数是借用此方法的对象 , 后面是该函需要的参数
o1.add.call(o2,10,20) 等价于==> o2.function(num1,num2){ console.log(num1 + num2); }
参数是根据该函数需要的参数一一写入 , 可以是任意多个 , 但是只能一个一个写入
2.2 call的经典应用
在数组一章我们学到了很多的数组方法 , 但是我们日常应用中最常见的却是由DOM节点组成的类数组 , 而类数组中的方法极少,使用时极不方便 , 所以我们便可以用call来借用数组的方法给类数组使用
<body> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <script> let aLi = document.getElementsByTagName("li") Array.prototype.forEach.call(aLi, function(item, index, array) { item.style.cssText = "width:50px;height:50px;margin-bottom:10px;background-color:#bfc" }) </script> </body>
2.3 经典继承之apply
语法 >>>
fn.apply( {},[参数1,参数2…])
- apply方法和call在功能上并没有什么不同
- 唯一的别就是apply方法的参数都放在一个数组里面 , 这个数组可以是一个类数组或是标准数组
仍然用上面的例子
let o1 = { name: "wangyubo", age: 18, add: function(num1, num2) { console.log(num1 + num2); } } let o2 = { name: "wyb", age: 28, sayAge: function() { console.log(this.age); } }
但是这点确值得注意 , 因为数组是一个引用类型的值 ,
let o2 = {} function add(num1, num2, num3) { let arr = [num1, num2, num3] arr.forEach((item, index, array) => { item += 1 }) console.log(arr); } add.apply(o2, [1, 2, 3])
我给每一个item都+1,但是效果却没有
需要操作这个数组本身
item += 1 ==> array[index] +=1
2.3 补充
补充一个不相关的经典问题 var 和 let
var name = "wangyubo"; function sayName() { console.log(name); } sayName() // ==>wangyubo
var name = "wangyubo"; function sayName() { console.log(this.name); } sayName() // ==>wangyubo
let name = "wangyubo"; function sayName() { console.log(name); } sayName() //==>wangyubo
let name = "wangyubo"; function sayName() { console.log(this.name); } sayName() //==> 空/undefined
let 申明的变量并不挂载在window上
2.4 经典继承之bind
语法 >>>
fn.bind( {},参数1,参数2…)
- bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。
- 并在调用新函数时,将绑定时给定参数列表作为原函数的参数序列的前若干项。
示例 >>>
let obj = { name: "wangyubo" } function sayName(age, sex) { console.log(`${this.name}:${age}:${sex}`); } let newFN = sayName.bind(obj, 18);//使用bind会创建一个新函数,所以需要一个变量来接收 //绑定bind时传了一个参数,这就作为sayName方法的第一个参数永久不变了 newFN("男");//函数第一次执行时又传入一个参数,这就作为sayName的第二个也是最后一个参数了 newFN("22");//函数第二次执行时又传入一个参数,此时这个参数会顶替掉sayName的最后一个参数
//你认为上面代码中的认知对吗?其实是不准确的 //绑定bind时传了一个参数,这就作为sayName方法的第一个参数永久不变了,这是对的,并且,如果绑定了两个参数,那么sayName方法在执行时传入的参数都无效了 let obj = { name: "wangyubo" } function sayName(age, sex) { console.log(`${this.name}:${age}:${sex}`); } let newFN = sayName.bind(obj, 18, "男"); newFN(28, "女")
输出的是bind绑定时的两个参数
//如果bind绑定时参数未满,那么函数执行时传入的参数就有效,并且是同位顶替,而不是一一叠加 let obj = { name: "wangyubo" } function sayName(age, sex, height) {//sayName需要的参数增加到3个 console.log(`${this.name}:${age}:${sex}:${height}`); } let newFN = sayName.bind(obj, 18);//绑定时传入一个 newFN("男");//第一次执行时传入一个 newFN(180)//第二次执行时再传入一个
- 如果是一一叠加 , 那么第二次执行时函数已经有了三个参数 , height应该是输出180才对
- 而实际结果是
180
顶替了男
, 成了第二个参数// newFN("男"); // newFN(180) newFN("男", 180)//这样才是第二个参数和第三个参数
- 也就是说 , bind绑定时传入的参数是永久不变的 , 位置固定的
- 而函数执行时 , 每次执行都会重新计算传入参数的位置
2.5 伪造对象
function GrandFather() { this.sayName = function() { console.log(this.name); } } function Mother() { this.name = "wangyubo" } function Father() { console.log(this); GrandFather.call(this); } let son = new Father() son.name = "wangyubo" son.sayName()
通过结果可知 , this指向Father的实例对象 , son是继承自Father , 且son虽然使用了GrandFather和Mother的一些方法和属性却不是继承自GrandFather和Mother , 而是Father 借用了GrandFather和Mother的一些方法和属再继承给了son
- 通过伪造对象实现了借用原型链上的方法 , 而又不通过继承的方式
- 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向父类型构造函数传递参数
function GrandFather(name) { this.name = name; } function Father() { console.log(this); GrandFather.call(this, 'wangyubo'); } let son = new Father(); console.log(son.name);
- 那么实例对象也就能拿到这个属性
- 而且也不会延伸原型链
3. 组合继承
- 组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
- 简单点说 , 实例对象利用call , apple , bind借用原型上的一些方法和属性 , 而原型上的方法本来又会继承给实例对象 , 这种就叫组合继承
- 这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性
- 但是他会造成严重的属性重复问题 , 及其恶心
function GrandFather(name) { this.name = name; this.girlFriends = ["斋藤飞鸟", "新垣结衣"] } GrandFather.prototype.sayName = function() { console.log(this.name); } function Father(name, age) { GrandFather.call(this, name); this.age = age; } Father.prototype = new GrandFather(); Father.prototype.constructor = Father; Father.prototype.sayAge = function() { console.log(this.age); } let son = new Father('wangyubo', 18);
- name和girlFriends出现了两次 , 一级属性是源自于call方法借用的GrandFather里面的name和girlFriends , 第二次出现是GrandFather的实例对象Father继承了GrandFather原型对象上的属性
- 这种方法基本没什么人用 , 仅用于学术使用
4. 原型式继承
道格拉斯·克罗克福德在2006 年写了一篇文章,题为Prototypal Inheritance in JavaScript (JavaScript中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。
function obj(o) { function F() {}; F.prototype = o; return new F(); }
在obj内部先创建一个临时性的构造函数 , 然后将传入的对象作为这个构造函数的原型对象 , 最后返回这个临时构造函数的新实例
用法之一 :
function obj(o) { function F() {}; F.prototype = o; return new F(); } var person = { name: "wangyubo", girlFriends: ["斋藤飞鸟", "迪丽热巴"], } var newPerson = obj(person); newPerson.name = "wyb01"; newPerson.girlFriends.push("日向雏田") var newPerson02 = obj(person); newPerson02.name = "wyb02"; newPerson02.girlFriends.push("日向花火") console.log(person.girlFriends); console.log(newPerson.girlFriends); console.log(newPerson02.girlFriends);
通过person对象新生成的新对象 , 引用类型的属性仍然在共用一个地址 , 这其实就是浅拷贝 , 也就是说
newPerson.girlFriends.push("日向雏田") newPerson02.girlFriends.push("日向花火")
都是在操作原型对象 , 并非操作自己的属性
//当然 如果我在末尾给newPerson02.girlFriends重新赋值 newPerson02.girlFriends = ["新垣结衣"]; console.log(newPerson02.girlFriends); //那么newPerson02.girlFriends就有了新的内存地址了 ,也就不再和前两个一样了 //这和前面学的是一样的道理
这种模式的缺点就是所有的方法和属性都堆积在原型上
5. 寄生式继承
- 寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。
- 寄生式继承的思路与寄生构造函数和工厂模式类似:
- 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
- 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
function obj(o) { function F() {}; F.prototype = o; return new F(); } function createAnother(original) { var clone = obj(original);//通过调用函数来创建一个新对象 clone.sayHello = function() {//以某种方式扩展这个对象 console.log("Hello"); } return clone; }
和组合模式差不多 , 上面类似是构造函数 ,下面类似是原型
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
6. 寄生组合式继承
组合继承是JavaScript 最常用的继承模式;不过,它也有自己的不足。
- 组合继承最大的问题就是无论什么情况下,都会调用两次超类型(父类)构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
- 没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
function GrandFather(name) { this.name = name; this.girlFriends = ["斋藤飞鸟", "新垣结衣"] } GrandFather.prototype.sayName = function() { console.log(this.name); } function Father(name, age) { GrandFather.call(this, name);//第一次调用GrandFather超类构造函数 this.age = age; } Father.prototype = new GrandFather();//第二次调用GrandFather超类构造函数,也是第一次调用构造函数 Father.prototype.constructor = Father; Father.prototype.sayAge = function() { console.log(this.age); } let son = new Father('wangyubo', 18);//第二次调用构造函数
这种多次调用显得过于麻烦
所以就有了寄生组合式继承
- 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
- 其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
- 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function obj(o) { function F() {}; F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = obj(superType.prototype); //创建超类的原型对象 prototype.constructor = subType; //超类原型的构造函数指向子类构造函数 subType.prototype = prototype; //将超类原型对象赋值给子类原型对象 }
示例 :
function SuperType(name) { this.name = name; this.girlFriends = ["斋藤飞鸟", "新垣结衣"] } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); this.age = age } inheritPrototype(SubType, SuperType) SubType.prototype.sayAge = function() { console.log(this.age); } let instance = new SubType("wyb", 18)
- 这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性。
- 与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()。
- 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式