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来指向构造函数。
<script>"use strict"; function User() { }; // 构造函数 // ========= 在原有的原型对象基础上新增一个方法 User.prototype.show = function(){ console.log("该函数的实例对象调用的方法..."); }; let u1 = new User(); u1.show(); // 该函数的实例对象调用的方法... </script>
<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的继承是原型对象的继承,并不是类的继承。
当一个实例对象要找方法时会一层一层向上找,如果找到了方法就不再继续向上找了。
<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>


以下示例不是在原型对象上继承,故是一种错误的做法。
<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>
原型工厂
原型工厂是将继承的过程封装,使用继承业务简单化。
<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>
对象工厂
在原型继承基础上,将对象的生成使用函数完成,并在函数内部为对象添加属性或方法。
<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类应该当做工具箱来使用,而不应该作为其他类的父类被继承下来。


<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的继承处理的和其他语言还是有所不同,构造函数相当于父亲,这个父亲有一个背包就是原型对象。当他的儿子要去用方法时就去找父亲的背包,父亲的背包没找到就找爷爷的背包。
而在这个背包中有一张字条,就是父亲的名字。
以上就是原型对象与构造函数的关系。
使用继承时应当把公共方法丢给背包而不是父亲本身,这是与别的语言比较大的区别。

浙公网安备 33010602011771号