作为 Js 新学习者,最近好好整理了下 Js 中的原型链和继承机制。结合文档和网课,我把这个话题相关的文字给码了下。
对象 Object 在了解继承和原型链之前,首先我们需要掌握一个对象 Object 是如何被创建的。
总的来说,在 JavaScript 中我们有以下的几种方式来创建一个新的 Object。
原生的 Object 方法
对象字面量
工厂模式
构造函数
Prototype 原型
原生 Object 1 2 3 4 5 6 7 8 9 10 11 var user = new Object ();user.name = "张三" ; user.pwd = "123456" ; user.showInfo = function ( ) { document .write(this .name + "-" + this .pwd + "<br />" ) } user.showInfo();
对象字面量
对象定义的一种简写形式
简化创建包含大量属性的对象的过程
在为函数传递大量可选参数时,可考虑使用对象字面量
1 2 3 4 5 6 7 8 9 10 var user = { name: "张三" , pwd: "123456" , showInfo: function ( ) { document .write(this .name + "-" + this .pwd + "<br />" ); } }
除此之外,
var arr = [] 其实是 var arr = new Array() 的语法糖,
function Foo() {...} 其实是 var Foo = new Function {...} 的语法糖
工厂模式
软件工程领域的一种设计模式
抽象了创建对象的过程
通过函数封装 创建对象的细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function createUser (name, pwd ) { var user = new Object (); user.name = name; user.pwd = pwd; user.showInfo = function ( ) { document .write(this .name + "-" + this .pwd + "<br />" ); } return user; } var user1 = createUser("张三" , "123456" );var user2 = createUser("李四" , "654321" );
构造函数 (!) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function User (name, pwd ) { this .name = name; this .pwd = pwd; this .showInfo = function ( ) { document .write(this .name + "-" + this .pwd + "<br />" ); } } var user1 = new User("张三" , "123456" );var user2 = new User("李四" , "654321" );
构造函数也就是一个函数,只不过它于普通的函数又有点不同:
没有显示的创建对象;
直接将属性和方法赋给this;
没有return语句;
原型 prototype
每个函数都有一个 prototype (原型) 属性
其是一个指针,指向一个对象 (Object)
这个对象的用途是包含可以由特定类型的所有实例的属性和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function User (name, pwd ) { this .name = name; this .pwd = pwd; } User.prototype.showInfo = function ( ) { document .write(this .name + "-" + this .pwd + "<br />" ); } var user1 = new User("张三" , "123456" );var user2 = new User("李四" , "654321" );
上述用构造函数+prototype这种方法构造对象,也叫做用混合方式 构造对象,即
构造函数:来写函数的属性
原型 prototype:来写函数的方法
继承 关于 prototype 和继承 上一节里我们用到了 prototype,那么 prototype 到底是什么呢?
prototype 属性包含一个对象 (prototype 对象)
所有实例对象需要共享的属性和方法 ,都放在这个对象里
实例对象一旦创建,将自动引用 (共享) prototype 对象的属性和方法
prototype 对象好像是实例对象的原型,而实例对象则好像“继承” 了 prototype 对象一样
几种继承的方式 更改 prototype 指向 常见的方法
Student.prototype = new Person()
Student 的 prototype 指向一个 Person 的实例,所有“学生”的实例,就能继承 Person 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function Person ( ) { this .foot = 2 ; this .head = 1 ; } function Student (name, no ) { this .name = name; this .no = no; } Student.prototype = new Person(); Student.prototype.constructor = Student;
注意
如果替换了 prototype 对象,即 o.prototype = {};
要为新的 prototype 对象加上 constuctor 属性,并将这个属性指回原来的构造函数,即 o.prototype.constructor = o;
直接继承 prototype 1 2 3 4 5 6 7 8 9 10 11 12 13 function Person ( ) {}Person.prototype.foot = 2 ; Person.prototype.head = 1 ; function Student (name, no ) { this .name = name; this .no = no; } Student.prototype = Person.prototype Student.prototype.constructor = Student;
这样继承的优点 在于效率比较高 (不用执行和建立 Person 的实例了),
而缺点 是 Student.prototype 和 Person.prototype 现在指向了同一个对象,任何对 Student.prototype 的修改,都会反映到 Person.prototype。
利用空对象作为中介 为了解决上述更改 Student.prototype 都会反映到 Person.prototype 的问题,我们可以利用空对象作为中介来进行继承。其优点在于:
空对象,几乎不占用内存
修改 Student 的 prototype 对象,不会影响到 Person 的 prototype 对象
可以封装成函数
1 2 3 4 5 6 7 var F = function ( ) {};F.prototype = Person.prototype; Student.prototype = new F(); var stu1 = new Student("张三" , "s001" );Student.prototype.constructor = Student;
将上述代码封装成一个函数,即
1 2 3 4 5 6 function extend (Child, Parent ) { var F = function ( ) {} F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; }
(有 ES6 的 extends 那味儿了)
等到需要用到继承的时候,我们便可以直接通过 extend(Student, Person) 来实现了,省去了不少工夫。
构造函数绑定来进行继承 为什么要绑定? 假设有以下代码,提问 下面的 alert 会弹出什么结果?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person ( ) { this .foot = 2 ; this .head = 1 ; this .favorColor = ["red" , "yellow" ]; } function Student (name, no ) { this .name = name; this .no = no; } Student.prototype = new Person(); var stu1 = new Student("张三" , "s001" );stu1.favorColor.push("blue" ); var stu2 = new Student("李四" , "s002" );alert(stu2.favorColor);
答案为 red, yellow, blue,而不是 red, yellow,为什么会这样呢?
原因是在这里的所有实例即 stu1、stu2,共享了父函数的非基本类型 属性,如数组 或对象 ,一个实例对属性的修改会影响所有实例。
构造函数绑定
在子类型构造函数的内部调用父类型构造函数
通过 call() 或 apply() 方法
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Person ( ) { this .foot = 2 ; this .head = 1 ; this .favorColor = ["red" , "yellow" ]; } function Student (name, no ) { Person.call(this ); this .name = name; this .no = no; } var stu1 = new Student("张三" , "s001" );stu1.favorColor.push("blue" ); alert(stu1.foot); alert(stu1.head); alert(stu1.favorColor); var stu2 = new Student("李四" , "s002" );alert(stu2.favorColor);
组合继承 上述代码只能实现属性的继承,那么如何对方法也进行继承呢?我们可以用组合继承的方法。
组合继承也叫伪经典继承:
其将原型链继承和构造函数继承组合在一块
原型链实现对原型属性和方法的继承
借用构造函数实现对实例属性的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Person ( ) { this .foot = 2 ; this .head = 1 ; this .favorColor = ["red" , "yellow" ]; } Person.prototype.sayColor = function ( ) { alert("Hello, 我最爱的颜色是: " + this .favorColor); } function Student (name, no ) { Person.call(this ); this .name = name; this .no = no; } Student.prototype = new Person(); Student.prototype.constructor = Student;
拷贝继承 通过对父类原型的属性遍历拷贝,也能把父类的属性放到子类上。简单且暴力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function Student (name, no ) { this .name = name; this .no = no; } function extend2 (Child, Parent ) { var p = Parent.prototype; var c = Child.prototype; for (var i in p) { c[i] = p[i]; } } extend2(Student, Person); var stu1 = new Student("张三" , "s001" );
原型链 原型规则 了解以上继承知识之后,我们便能将原型链的概念好好梳理下了。原型有以下几点规则:
所有的引用类型 ,即数组 、对象 、函数 ,都具有对象特性,即可自由扩展属性 (除了 null)。
所有的引用类型 ,都有一个 __proto__ 属性,属性值是一个普通的对象。
所有的函数 ,都有一个 prototype 属性,属性值也是一个普通的对象。
所有的引用类型 ,__proto__ 属性值都指向它的构造函数 的 prototype 属性值。
当试图得到一个对象 的某个属性时,如果这个对象本身没有这个属性,那么会去它的 __proto__ (即它的构造函数的 prototype) 中寻找。
这样为了得到某个属性,沿着其 __proto__ 属性进行寻找的链条,我们叫做原型链 。
instanceof f instanceof Foo 可用来判断元素 f 是否为 Foo 的原型,
其判断逻辑就是:对 f 的 __proto__ 一层一层往上,看是否能对应到 Foo.prototype,即
f.__proto__ = Foo.prototype 或 f.__proto__.__proto__... = Foo.prototype