JavaScript 原型与继承
原型基础
每个对象都有一个原型prototype
对象,通过函数创建的对象也会拥有这个原型对象。
原型是一个指向对象的指针。
原型对象的作用:
存储一些实例对象公用的方法或属性,也就是说一个构造函数中的公共方法或属性应该放入原型对象中
原型对象中的参数:
默认一个原型对象有一个方法
constructor
,即构造函数本身。原型对象和构造函数的关系:
构造函数怎么找到自己的原型对象:
使用属性
prototype
即可找到该原型对象,你可以为其添加公共方法或属性方便该构造函数的实例对象使用。实例对象怎么找到自己的原型对象:
使用属性
__proto__
即可找到该实例对象的原型对象
使用字面量创建出的对象可以调用其原型对象中的方法。
<script>"use strict"; let array = [1, 2, 3]; console.log(array); </script>
构造函数,实例对象,原型对象的关系。
获取原型对象
如果是一个构造函数,你想获取到原型对象为其实例化的对象添加公共方法,可以使用属性prototype
来获取。
如果是一个已经实例化好的对象,你想获取到其原型对象可以使用属性__proto__
来进行获取,也可以使用Object.getPrototypeOf()
方法来进行获取。
<script>"use strict"; function User() { }; // 构造函数 console.log(User.prototype); let u1 = new User(); console.log(u1.__proto__); console.log(Object.getPrototypeOf(u1)); console.log(u1.__proto__ === User.prototype); // true console.log(u1.__proto__ === Object.getPrototypeOf(u1)); // true console.log(User.prototype === Object.getPrototypeOf(u1)); // true </script>
原型对象设置方法
函数拥有多个原型,prototype
用于实例对象使用,__proto__
用于函数自身当做对象时使用。
注意函数本身也是一个实例对象,所以当将函数作为对象使用时使用__proto__
为它设置方法。
当函数作为构造函数时其供实例使用的方法应该存储在prototype
中,这是为了大幅度节省内存。
否则每一个实例对象都会创建出自己的方法。
<script>"use strict"; function User() { }; // 构造函数 User.__proto__.show = function (){ console.log("函数作为对象调用的方法..."); }; User.show(); // 函数作为对象调用的方法... // ============= User.prototype.show = function(){ console.log("该函数的实例对象调用的方法..."); }; let u1 = new User(); u1.show(); // 该函数的实例对象调用的方法... </script>
推荐使用prototype
来设置方法,因为将函数作为对象来使用的场景不多见。
设置方式有两种,第一种在原有的原型对象基础上增加新的方法,第二种是覆盖原本的原型对象,但是要注意添加参数constructor
来指向构造函数。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script>"use strict"; function User() { }; // 构造函数 // ========= 在原有的原型对象基础上新增一个方法 User.prototype.show = function(){ console.log("该函数的实例对象调用的方法..."); }; let u1 = new User(); u1.show(); // 该函数的实例对象调用的方法... </script>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script>"use strict"; function User() { }; // 构造函数 // ========= // 设置新的原型对象 User.prototype = { constructor: User, // 必须添加该参数,指向构造函数。 show() { console.log("方法1"); }, test() { console.log("方法2"); } }; let u1 = new User(); u1.show(); // 方法1 u1.test(); // 方法2 </script>
原型链关系图
原型对象也有自己的原型,最终的原型对象都是Object.prototype
<script>"use strict"; function User() { }; // 构造函数 User.prototype.show = function () { console.log("该函数的实例对象调用的方法..."); }; let u1 = new User(); console.log("User的实例对象的原型对象--->", u1.__proto__); </script>
<script>"use strict"; function User() { }; // 构造函数 User.prototype.show = function () { console.log("该函数的实例对象调用的方法..."); }; let u1 = new User(); // 验证上图关系 console.log(u1.__proto__.__proto__ === Object.prototype); // true </script>
原型对象与构造函数
原型对象中有一个constructor
的方法,即指向构造函数。
<script>"use strict"; function User() { }; // 构造函数 console.log(User.prototype.constructor === User); // true </script>
更改原型对象
使用Object.setPrototypeOf()
可设置对象的原型对象。
也可使用Object.create()
来设置对象的原型对象,这个方法下面会介绍到。
<script> "use strict"; function User() { }; // 构造函数 function Admin() { }; // 构造函数 User.prototype.show = function () { console.log("User中的show"); }; Admin.prototype.show = function () { console.log("Admin中的show"); }; let a1 = new Admin(); Object.setPrototypeOf(a1, User.prototype); // 将a1的原型对象设置为User的原型对象 console.log(Object.getPrototypeOf(a1)); // {show: ƒ, constructor: ƒ} a1.show(); // User中的show let a2 = new Admin(); a2.show(); // Admin中的show </script>
原型检测
使用instanceof
检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
使用isPrototypeOf
检测一个对象是否是另一个对象的原型链中
<script> "use strict"; function User() { }; // 构造函数 let u1 = new User(); console.log(u1 instanceof User); // true u1的原型链中包含User的原型对象吗? console.log(User.prototype.isPrototypeOf(u1)); // true User的原型对象在u1的原型链中吗? </script>
属性遍历
使用in
检测原型链上是否存在属性,使用 hasOwnProperty()
只检测当前对象的原型对象。
使用for/in
会按照原型链遍历。
<script> "use strict"; function User() { }; // 构造函数 User.prototype = { constructor: User, show() { console.log("User原型的show..."); } } let u1 = new User(); console.log("show" in u1); // true 会沿着原型链查找 console.log(u1.hasOwnProperty("show")); // false 只检测自己 for (let key in u1) { // for/in会遍历所有原型链 if (key === "show") { console.log("存在"); // 存在 } } </script>
原型借用
我们可以借用另一个原型对象中的方法,使用call()
或者apply()
来改变this
指向与传递参数即可。
如下示例,对象借用了数组中的排序方法对成绩进行排序,这里实在想不到太好的例子。所以就用这个了。
<script> "use strict"; let obj = { Html: 76, Css: 88, Js: 100, Python: 96, Linux: 77, }; // call传递一个新的this指向 let res = Array.prototype.sort.call(Object.entries(obj), function (v1, v2) { return v2[1] - v1[1]; }); obj = {}; // 清空对象 for (let i = 0; i < res.length; i++) { let [key, value] = res[i]; Object.assign(obj,{[key]:value}) }; console.log(obj); // {Js: 100, Python: 96, Css: 88, Linux: 77, Html: 76} </script>
this
this
不受原型继承影响,this
指向调用属性时使用的对象。
<script> "use strict"; function User(username) { this.username = username; }; // 构造函数 User.prototype = { constructor: User, show() { console.log(this.username); } } let u1 = new User("u1"); let u2 = new User("u2"); u1.show(); // u1 u2.show(); // u2 </script>
Object.create
该方法可以立即返回一个对象,参数1指定其原型对象,参数2可设置其属性或方法及其特征。
<script> "use strict"; // 无原型的对象 let obj_1 = Object.create(null, { username: { value: "云崖" } }); console.log(obj_1); // username: "云崖" // 有原型的对象,该对象原型指向为Array对象的原型 let obj_2 = Object.create(Array.prototype, { username: { value: "云崖" } }); console.log(obj_2); // Array {username: "云崖"} </script>
__proto__原理
__proto__
其实它并非一个真正意义上的属性而是使用getattr
以及setattr
进行实现的。
建议使用 Object.setPrototypeOf
与Object.getProttoeypOf
替代 __proto__
。
以下示例将展示__proto__
原理。
<script> "use strict"; function User(username) { this.username = username; Object.defineProperties(this, { __proto__: { get() { return User.prototype; }, set(value) { Object.setPrototypeOf(this, value); }, }, }); }; // 构造函数 </script>
继承与多态
Js
的继承是原型上的继承。Js
只有单继承,没有多继承,即一个对象只能有一个原型。
当一个对象开始找方法时不断的向上使用__proto__
来寻找方法。
调用相同方法,产生不同结果,这就是多态的体现。
继承实现
注意!Js
的继承是原型对象的继承,并不是类的继承。
当一个实例对象要找方法时会一层一层向上找,如果找到了方法就不再继续向上找了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> "use strict"; function A() { }; // 构造函数 A.prototype.f1 = function () { console.log("A的f1方法"); }; function B() { }; // 构造函数 Object.setPrototypeOf(B.prototype, A.prototype); // B的原型对象继承于A的原型对象 B.prototype.f2 = function () { console.log("B的f2方法"); }; function C() { }; // 构造函数 Object.setPrototypeOf(C.prototype, B.prototype); // C的原型对象继承于B的原型对象 C.prototype.f3 = function () { console.log("C的f3方法"); } let c1 = new C(); console.dir(c1); c1.f1(); c1.f2(); c1.f3(); </script>
以下示例不是在原型对象上继承,故是一种错误的做法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> "use strict"; function A() { }; A.prototype.f1 = function () { console.log("A的f1方法"); }; function B() { }; Object.setPrototypeOf(B, A.prototype); B.prototype.f2 = function () { console.log("B的f2方法"); }; function C() { }; Object.setPrototypeOf(C, B.prototype); C.prototype.f3 = function () { console.log("C的f3方法"); } let c1 = new C(); console.dir(c1); // 异常 c1.f1(); c1.f2(); c1.f3(); </script>
方法覆写
由于查找顺序是由下而上,所以我们在最近的原型对象中写入同名方法就不会继续向上查找了。
<script> "use strict"; function A() { }; // 构造函数 A.prototype.show = function () { console.log("A的show方法"); }; function B() { }; // 构造函数 Object.setPrototypeOf(B.prototype, A.prototype); // B的原型对象继承于A的原型对象 B.prototype.show = function () { console.log("B的show方法"); }; let b1 = new B(); b1.show(); // B的show方法 </script>
多态体现
同样的方法运用在不同的对象身上会产生不同的结果,这就是多态的体现。
<script> "use strict"; function User() { } User.prototype.show = function () { console.log(this.description()); // 调用相同方法,产生不同结果,这就是多态的体现 }; function Admin() { } Admin.prototype = Object.create(User.prototype); // Object.create() 也是可以改变对象的原型 Admin.prototype.description = function () { return "管理员在此"; }; function Member() { } Member.prototype = Object.create(User.prototype); Member.prototype.description = function () { return "我是会员"; }; function Enterprise() { } Enterprise.prototype = Object.create(User.prototype); Enterprise.prototype.description = function () { return "企业帐户"; }; for (const obj of [new Admin(), new Member(), new Enterprise()]) { obj.show(); } </script>
深究继承
继承是为了复用代码,继承的本质是将原型指向到另一个对象。
构造函数
如果多个构造函数在功能上极其相似,我们希望进行复用代码则可以利用其它构造函数来进行函数的构建。但是要注意如下问题:
此时 this
指向了window,无法为当前对象声明属性。
<script> "use strict"; function User(username) { this.username = username; // 严格模式抛出异常!此时的this指向在window } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); console.log(this.username); }, } function Admin(username) { User(username) } Object.setPrototypeOf(Admin.prototype, User.prototype); let a1 = new Admin("云崖"); a1.show(); </script>
解决上面的问题是使用 call()/apply()
方法改变this
指向,从而为每个生成的对象设置属性。
<script> "use strict"; function User(username) { this.username = username; } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); // this指向-->[object Object] console.log(this.username); // 云崖 }, } function Admin(username) { User.call(this, username) // 解决办法 } Object.setPrototypeOf(Admin.prototype, User.prototype); let a1 = new Admin("云崖"); a1.show(); </script>
原型工厂
原型工厂是将继承的过程封装,使用继承业务简单化。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> "use strict"; function extend(sub, sup) { // 原型工厂代码封装 Object.setPrototypeOf(sub.prototype,sup.prototype); // 使sub的原型对象继承于sup的原型对象 } function User(username) { this.username = username; } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); // this指向-->[object Object] console.log(this.username); // 云崖 }, } function Admin(username) { User.call(this, username) } extend(Admin,User); // 使用原型工厂封装 let a1 = new Admin("云崖"); a1.show(); </script>
对象工厂
在原型继承基础上,将对象的生成使用函数完成,并在函数内部为对象添加属性或方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> "use strict"; function User(name, age) { this.name = name; this.age = age; } User.prototype.show = function () { console.log(this.name, this.age); }; function Admin(name, age) { let instance = Object.create(User.prototype); // 创建了一个新对象 User.call(instance, name, age); instance.role = function () { console.log('admin.role'); } return instance; } let hd = Admin("管理员", 19); hd.show(); </script>
Mixin机制
由于Js
不支持多继承,所以想添加功能必须在某一个原型对象上不断的增加功能,这势必会让其本来的原型显得混乱不堪。
这种时候就可以使用Mixin
机制来实现。
注意:Minin
类应该当做工具箱来使用,而不应该作为其他类的父类被继承下来。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> "use strict"; function extend(sub, sup) { // 更改原型对象的函数 Object.setPrototypeOf(sub.prototype, sup.prototype); // 使sub的原型对象继承于sup的原型对象 } function Vehicle(name) { // 交通工具 this.name = name; } Vehicle.prototype = { constructor: Vehicle, whistle() { console.log(`${this.name}在鸣笛`); // 公用方法放父类中 }, } function Aircraft(name) { // 飞机 Vehicle.call(this, name); } extend(Aircraft, Vehicle) // 飞机的原型对象继承于交通工具。因此飞机具有了鸣笛方法 function Car(name) { // 汽车 Vehicle.call(this, name); } extend(Car, Vehicle) // 汽车的原型对象继承于交通工具。因此汽车具有了鸣笛方法 let Flyable_Mixin = { // 飞行器的功能 fly() { console.log(`${this.name}在飞`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Aircraft.prototype, Flyable_Mixin); //给飞机添加上飞机Mixin的功能 let Car_Mixin = { // 汽车的功能 reversing() { console.log(`${this.name}正在倒车入库`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Car.prototype, Car_Mixin); //给汽车添加汽车Mixin的功能 let c1 = new Car("法拉利"); let a1 = new Aircraft("波音747"); c1.whistle(); // 法拉利在鸣笛 c1.reversing(); // 法拉利正在倒车入库 a1.whistle(); // 波音747在鸣笛 a1.fly(); // 波音747在飞 </script>
super
super
会在其原型对象上找。
<script> "use strict"; let a = { username: "云崖" }; let b = { __proto__: a, show() { console.log(super.username); // super会去找__proto__,相当于拿到a.username }, }; b.show(); // 云崖 </script>
总结
其实Js
的继承处理的和其他语言还是有所不同,构造函数相当于父亲,这个父亲有一个背包就是原型对象。当他的儿子要去用方法时就去找父亲的背包,父亲的背包没找到就找爷爷的背包。
而在这个背包中有一张字条,就是父亲的名字。
以上就是原型对象与构造函数的关系。
使用继承时应当把公共方法丢给背包而不是父亲本身,这是与别的语言比较大的区别。