阮一峰JS教程读后感(十)对象的继承
一、对象的继承
1. prototype的作用?
在JavaScript中,每一个对象都有proto属性,而每一个函数都有prototype属性,假设构造函数的prototype位空,那么实例化后对象和构造函数就没有任何关系,通过构造函数生成的对象之间也没有任何关系,这个实例化的过程仅仅是执行了一遍构造函数而后开辟了一片内存空间而已。这样极容易造成内存的浪费。为什么呢,见如下代码:
function People(name, age, eyes, hands) { // 构造函数
this.name = name;
this.age = age;
this.eyes = eyes;
this.hands = hands;
this.cry = function cry() {
console.log('wa wa wa ...')
}
}
var boy = new People('bing', 0, 2, 2) // 实例化
var girl = new People('yanyan',0, 2, 2) // 实例化
boy // People {name: "bing", age: 22, eyes: 2, hands: 2, cry: ƒ}
girl // People {name: "yanyan", age: 22, eyes: 2, hands: 2, cry: ƒ}
通过观察上面的代码不难发现,像eyes,hands,以及cry方法明显可以所有的对象共用,但是这里没有,每次实例化后,新的对象中都有这些可复用属性和方法,着实浪费内存,所以我们把代码改进一下:
function People(name, age) { // 构造函数
this.name = name;
this.age = age;
}
People.prototype.eyes = 2;
People.prototype.hands = 2;
People.prototype.cry = function cry() { console.log('wa wa wa ...') }
var boy = new People('bing', 0) // 实例化
var girl = new People('yanyan', 0) // 实例化
boy // People {name: "bing", age: 0}
girl // People {name: "yanyan", age: 0}
boy.eyes // 2
boy.hands // 2
boy.cry // 'wa wa wa ...'
girl.eyes // 2
girl.hands// 2
girl.cry // 'wa wa wa ...'
上面的代码中我们把共有属性和方法定义在了构造函数的prototype属性上,这样实例化的对象仅仅包含一些私用的静态属性和静态方法,公有的属性和方法被定义在原型上,实例的proto指向构造函数的prototype,prototype上的属性和方法被称为实例属性和方法,这样不仅节省了内存,实例和构造函数也产生了一定的关系,如果以后需要修改这一类实例,可以直接通过修改其原型,代码如下:
People.prototype.color = 'yellow' // 在原型上添加属性
boy.color // 'yellow'
2. 原型链是什么?
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的。
Object.prototype.__proto__ // null
// 最顶层原型为 null
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
其实面向对象编程这一思想在各个语言中都大同小异,中心思想不变,实现方法殊途同归,在python中,通过定义类变量和类方法实现实例对象之间的属性和方法共享。
3. prototype的constructor属性有什么作用?
prototype的constructor默认指向该prototype对象所在的构造函数。通过对象原型的constructor属性可以知道该对象的构造函数,所以如果想在没有构造函数的情况下实例化一个和已有对象一样的对象实例我们有两种办法:
// 第一种办法
var newObj = Object.create(obj) // 以obj为原型创建新的对象
// 第二种方法
var newObj = new obj.constructor() // 通过obj的_proto_的constructor属性找到该对象的构造函数
另外值得注意的是,如果你修改了一个构造函数的原型,那么你还必须修改该构造函数的constructor属性
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
Person.prototype = {
method: function () {}
};
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
上面代码中,构造函数Person的原型对象改掉了,但是没有修改constructor属性,导致这个属性不再指向Person。由于Person的新原型是一个普通对象,而普通对象的constructor属性指向Object构造函数,导致Person.prototype.constructor变成了Object。
所以,修改原型对象时,一般要同时修改constructor属性的指向。
4. instanceof 的妙用
在前面的文章中我们介绍过一种可以防止调用构造函数时未使用new命令而导致出错的方法,现在我们回忆一下:
// 通过判断new.target 的指向是不是当前构造函数来判断是否使用了new命令
function People(name) {
if (new.target === People){
this.name = name;
return
}
return new People(name)
}
// 通过instanceof 判断现在this的指向是否继承了当前构造函数的原型
// 因为new命令的工作原理第2步是把空对象的_proto_指向构造函数的原型
function People(name) {
if ( this instanceof People) {
this.name = name;
return
}
return new People(name)
}
5. 构造函数继承
第一步:在子构造函数执行父级构造函数
function Person (name) {
super.call(this, args) // 这里的super即父级构造函数,args是父函数需要的参数
this.name = name
}
第二步:继承父级构造函数的原型
Person.prototype = Object.create(super.prototype)
Person.prototype.constructor = Person
这里使用Object.create(super.prototype)而不是直接赋值是因为复杂类型赋值都是引用式赋值,即不是真正的copy而只是引用,如果直接写Person.prototype = super.prototype,那么修改Person.prototype式,super.prototype也会被修改。
另外,有时我们只是想在一个构造函数中继承另一构造函数中某一方法而不是全部,我们可以这样写。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
这里采用call而不是直接赋值是为了保证在调用该方法时能把this绑定到该对象上,否则this可能会指向window。
6. 模块化思想和继承
ES5并不能很好的支持,需要参考ES6内容
二、Object对象相关方法
1. 通过Object.setPrototypeOf()来模拟new命令
function student(name, age) { //创建一个构造函数
this.name = arguments[1];
this.age = arguments[2];
}
student.prototype.study = function() { // 给构造函数的原型上添加方法
console.log('study...')
}
function instance(constructor, args) { //这是实例化函数
var obj = {};
Object.setPrototypeOf(obj, constructor.prototype)
constructor.apply(obj, arguments)
return obj
}
var student = instance(student, 'bing', 22)
student.name // 'bing'
student.age // 22
student.study() // 'study...'
通过上面的代码,我们没有使用new命令,成功实例化一个对象。
2. 对象的拷贝
如果要拷贝一个对象,需要做到下面两件事情。
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}