原型链实现继承
在面向对象的语言中都存在一个大家熟悉的词 —— 继承。那在JS中也同样存在继承这个概念,在ES6
之前,JS还没有提出class
的概念。
这时候,JS是通过什么来实现继承的呢?答案就是:原型链
通过一段代码认识下原型链
function Father() {
this.fatherage = 45;
};
Father.prototype.getFatherAge = function() {
return this.fatherage;
}
function Son() {
this.sonage = 20;
}
// 继承 Father
/*
Son.prototype对象成为了Father的实例
*/
Son.prototype = new Father();
Son.prototype.getSonAge = function() {
return this.sonage;
}
let son_instance = new Son();
console.log(son_instance.getFatherAge()); // 45
/*
这里是因为改变委托关系导致的 constructor 属性的指向改变
下一篇 => JS原型(下)中会讲到
*/
console.log(Son.prototype.constructor); // Father()构造函数
console.log(son_instance.__proto__.__proto__.__proto__); // Object.Prototype
console.log(son_instance.__proto__.__proto__.__proto__.constructor); // Object()构造函数
我们可以通过代码发现,我构建的son_instance是Son的实例,但是我却可以通过son_instance调用Father原型对象中的方法,这究竟是如何实现的呢?以及后面几行代码为何打印出这些?
看懂这张图,你就能理解上面的问题:
扩展问题(new原理)
在上面的代码与图结合发现,其中实现Son
继承Father
的关键代码是Son.prototype = new Father();
,也就是让Son
的原型成为Father
的实例,这样我们就创造了一个如图所示的关系,让原本指向Object
的原型对象的Son.prototype
属性指向了Father.Prototype
,那你认为我将代码改为Son.prototype.__proto__ = Father.prototype;
能实现继承吗?
那到底行不行,我们可以通过代码来观察
function Father() {
this.fatherage = 45;
};
Father.prototype.getFatherAge = function() {
console.log('我被调用了');
console.log(this);
return this.fatherage;
}
function Son() {
this.sonage = 20;
}
// 继承Father
// 方式一:
// Son.prototype = new Father();
// 方式二:为什么这样不能实现继承?
Son.prototype.__proto__ = Father.prototype;
console.log(Son.prototype.__proto__); // {getFatherAge: ƒ (), constructor: ƒ Father()}
let son1 = new Son();
console.log(son1.getFatherAge());
// "我被调用了"
// Son {sonage: 20}
// undefined
这里的最后一行代码显示的是undefined
,这说明我们并没有实现继承,无法通过Son
的实例去调用到Father
的原型中的方法
那这是为什么呢?我们的继承不就是用[[Prototype]]
属性把Son.Prototype
和Father.Prototype
联系起来的吗?
答案就藏在这个new
关键字里,因为我们解读出new
关键字究竟做了些什么!
new原理
var fn = function () { };
var fnObj = new fn();
- 在内存中创建一个新对象
var obj = new object();
- 这个新对象的
[[Prototype]](也就是__proto__)
属性指向构造函数的原型对象
obj._proto_ = fn.prototype;
-
将构造函数中的this指向这个新对象
-
执行构造函数内部的代码(给新对象添加属性)
var result = fn.call(obj);
- 如果函数没有返回其他对象,那么返回新创建的这个对象
if (typeof(result) == "object"){
fnObj = result;
} else {
fnObj = obj;
}
属性设置与屏蔽
属性赋值并不简单!!
我们将分三种情况
第一种
let car = new Object();
// 原型上添加wheel属性
Object.prototype.wheel = 'prototype wheel';
// 此时原型对象上已存在wheel属性,为底层同名属性赋值
car.wheel = 4;
console.log(car.wheel); // 4 (取原型链最底层的wheel属性)
此时取原型链最底层的属性!
第二种
let car = new Object();
// 原型上添加wheel属性并设置为只读
Object.defineProperty(Object.prototype, "wheel", {
value: 'prototype wheel',
// 将wheel属性设置为只读
writable: false
})
// 此赋值语句被忽略(非严格模式)
car.wheel = 4;
console.log(car.wheel); // prototype wheel
第二种和第一种的区别在于将原型对象上的wheel
属性设置为只读,此时我们在car
对象上赋值wheel
属性时,此赋值语句会被忽略
原因:这样做时为了模拟类属性的继承,你可以把原型对象中的wheel
属性看作是父类中的属性,它会被car
继承(复制),这样一来car
中的wheel
属性也是只读,所以无法创建
let car = new Object();
// 原型上添加wheel属性并设置为只读
Object.defineProperty(Object.prototype, "wheel", {
value: 'prototype wheel',
// 将wheel属性设置为只读
writable: false
})
// 换一种定义方式
Object.defineProperty(car, "wheel", {
value: 4
})
console.log(car.wheel); // 4
但是我们如果不通过=
对car.wheel
赋值,而是通过==Object.defineProperty
==去设置的话,此时又可以获取到car
对象上的wheel
属性
第三种
// car 的原型对象
let vehicle = {
// 给 wheel 属性定义setter
set wheel(value) {
// 注意这里是 _wheel_
this._wheel_ = value;
}
};
let car = Object.create(vehicle);
car.wheel = 4;
console.log(car); // {_wheel_: 4}
原型对象上存在wheel
属性并且它是一个setter,那就一定会调用这个setter
。wheel
不会被添加到car
,也不会重新定义wheel
这个setter
隐式屏蔽
var anotherObj = {a: 2};
var obj = Object.create(anotherObj);
anotherObj.a; //2
obj.a; //2
anotherObj.hasOwnProperty("a"); //true
obj.hasOwnProperty("a"); //false
obj.a++;
anotherObj.a; //2
obj.a; //3
obj.hasOwnProperty("a"); //true
这里的obj.a++;
就是obj.a = obj.a + 1
;分两步就是:
- 先从整个原型链获取
=
右边的obj.a
属性的值并加1 - 接着用将值3赋值给
obj
对象中新建的屏蔽属性a
(也就是赋值给=
的左值)