blog
blog copied to clipboard
JavaScript 面向对象精要 - Nicholas C.Zakas(二)
看一本js书好不好,主要看他对于面向对象的描述以及原型继承的描述。
第五章 继承
如何学习创建对象时理解面向对象js编程的第一步,而第二部是理解继承。
5.1 原型对象链和Object.prototype
JavaScript内建的继承方法被称之为原型对象链,又称之为原型对象继承。如果上一章所看,原型对象的属性可经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象。他也有自己的原型并继承其属性。因此可以说所有对象都继承自Object。
5.1.1 继承自 Object.prototype 的方法
前几章里用到的多个方法其实都是定义在Object.prototype上的。因此可以被其他对象继承,这些方法如下。
方法 | 定义 |
---|---|
hasOwnProperty() | 检查是否存在一个给定名字的自有属性 |
propertyIsEnumerable() | 检查一个自有属性是否可枚举 |
isPrototypeOf() | 检查一个对象是否是另一个对象的原型对象 |
valueOf() | 返回一个对象的值表达式 |
toString() | 返回一个对象的字符串表达式 |
这5种方法经由继承出现在所有对象种。当需要让对象在JavaScript中以一致的方式工作,最后尤为重要,有时候你甚至会想要自己定义他们。
1. valueOf
返回原本的值。当每一个操作符被用于一个对象时候就会调用valueOf的方法。默认返回对象实例本身。。原始封装类型重写valueOf,对于字符串返回字符串本身,对于Boolean返回一个布尔值。对number返回一个数字。
var now = new Date();
var earlier = new Date(2010, 1, 1);
console.log(now > earlier);
上面这个例子,now是一个代表当前时间的Date,而earlier是一个过去的时间,当使用<比较的时候,在两个对象都调用了valueOf()的方法,另外,你甚至可以对比两个Date相减来获得他们在epoch时间上的差值。
2.toString()
一旦value()返回的是一个引用值而不是原始值的时候,就会回退调用toString()方法,另外,当JavaScript期待一个字符串的时候,也会对原始值隐式调用toString()。例如,当加号操作符的一边是一个字符串,另一边会被自动转化成字符串。如果另一边是一个原始值,就会自动转化成字符串。不懂请看下面例子,重写Object的toString原型方法,'name'+{}; // namename
,字符串和布尔值相加会先将右边的布尔值转化成字符串,通过toString()这个方法。,首先如果右边是引用值,会先调用value的方法,如果value返回的还是一个引用值,那就调用toString()
const str = {
name: 'peng',
toString() {
return 'toString';
},
valueOf() {
return 'val';
},
};
Object.prototype.valueOf = function (){
return 'value'
}
Object.prototype.toString = function (){
return 'staring'
}
console.log(`name${str}`); // namestring
修改Object.prototype
修改Object会影响所有对象,这很危险。
Object.prototype.add = function (num) {
return this + num;
};
console.log({}.add(5)); // [object Object]5
这个新添加的属性是可枚举的.请看下面的例子,我给原型对象添加add方法,返回this+val,,这个会返回本对象并且因为他是引用值,所以会调用toString()方法,而toString方法又被我改写了,所以返回的就是'to stirng5'。 同时被新添加的add方法是实体字,说明他是可以被枚举的。
对象继承
对象继承是最简单的原型继承,你唯一需要做的就是指定新对象的[[Prototype]]指向原型对象。 同时可以使用Object.create()方法指定,他接受2个参数,第一个是需要被设置成新对象的[[Prototype]]的对象,,第二个参数和Object.definedProperties()中使用的一样。
var book = {
title: 'your now Principles of Object-Oriented Javascript.'
}
===========**the same as below**============
var book = Object.create(Object.prototype, {
title: {
configurable: true,
enumerable: true,
value: 'The Principles of Object-Oriented Javascript',
writable: true,
}
});
两种声明具有相同效果,第一种使用了字面量自定义属性,而第二种使用了Object.create,显示用了同样的操作,这种默认继承无趣,但是如果你继承其他对象就有意思了。
const person1 = {
name: 'peng',
sayName() {
console.log(this.name);
},
};
const person2 = Object.create(person1, {
name: {
configurable: true,
enumerable: true,
value: 'Greg',
writable: true,
},
});
person1.sayName(); // 'peng'
person2.sayName(); // 'Grey'
console.log(person1.hasOwnProperty('sayName'); // true
console.log(person2.hasOwnProperty('sayName'); // false
console.log(person1.isPrototypeOf(person2); // true
person2继承person1 继承Object
假设你通过Object.create()创建时第一个参数为null,那么继承指向空。看下图,他没有任何方法,因此同时,他也是一个完美的hash容器,因为你给他命名任何方法,都可以,不存在任何冲突。
无法转换,因为不存在toString方法来进行隐式转换。一个很有意思,你可以通过它创建一个没有原型的对象。
构造函数继承
当你声明一个类的时候,JavaScript引擎默认帮你做了如下事情,
function Person(){}
Person.prototype = Object.create(Object.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Person,
writable: true,
}
})
上面可以看出,你不需要做任何事情,这段代码帮你构造一个构造函数,其原型指向另一个继承自Function的的对象,并且它有一个新属性constructor,其值指向构造函数,如此循环下去,但是他们是一种循环式的自引用,众所周知,原型链仅仅只是一个指向对象的指针。这只是一种自己指向自己的循环引用point。下图例子说的很清楚,我这里只有一个a对象,a对象属性a指向它本身。那这就是循环自引用。而构造函数仅仅只是这样一个例子的复杂化而已。所有引用类型都是指针指向(point)
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArae = function () {
return this.length * this.width;
};
Rectangle.prototype.toString = function () {
return `[Rectangle ${this.length}x${this.width}]`;
};
// inherits from Rectangle
function Square(size) {
this.length = size;
this.width = size;
}
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
Square.prototype.toString = function () {
return `[Square ${this.length}x${this.width}]`;
};
const rect = new Rectangle(5, 10);
const square = new Square(6);
console.log(rect.getArae());
console.log(square.getArae());
console.log(rect.toString());
console.log(square.toString());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
console.log(square instanceof Object);
这个代码有两个构造函数:Rectangle和Square。Suqare构造函数的prototype属性被改写成Rectangle的一个对象实例。此时不需要给Rectangle的调用提供参数,因为他们不需要被使用,而且如果提供了,那么所有Square的对象实例都会享有共同的维度,用这种方法改变原型链,你需要确保构造函数不会再参数却是的时候抛出错误。(很多构造函数包含的初始化逻辑会需要参数)且构造函数不会改变任何全局状态。Square.prototype被改写后,constructor属性被重置为Square。 然后,rect作为Rectangle的实例对象呗创建,而square则被作为Square的实例创建。两个对象都有getArea()方法,因为他被继承自Rectangle.prototype和Object的对象实例。instanceof使用原型对象链检查对象类型。 Square.prototype并不需要真的被改写为一个Rectangle对象,毕竟Rectangle构造函数并没有真的为Square做什么必要的事情,事实上,唯一相关的部分是Square.prototype需要指向Rectangle.prototype,使得继承得以实现,这意味着你可以用Object.create()简化例子,代码如下
// inherits from Rectangle
function Square(size){
this.length= size;
this.width= size;
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Square,
writable: treu
}
});
Square.prototype.toString = function() {
return `[Square ${this.length}x${this.width}]`;
};
在这个版本的代码中,Square.prototype被改写成一个新的继承自Rectangle.prototype的对象,而Rectangle构造函数没有被调用。这意味着,你不再需要担心不参加构造函数会导致的错误。除此之外。
构造函数的窃取
啥意思?
由于JavaScript的继承通过原型对象继承来实现,因此不需要调用对象的父类的构造函数,如果你却是需要再子类构造函数中调用父类构造函数,那你就需要利用JavaScript函数工作的特性。 第二章提到过,call和apply方法允许你再调用函数是提供不同的this值,那正好是构造函数窃取的关键。而这个就是构造函数窃取的关键,只需要再子类构造函数中调用call或者apply调用父类的构造函数,并将新的创建的对象传进去即可。实际上,就是用自己的对象窃取父类的构造函数,如下例子。
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArae = function () {
return this.length * this.width;
};
Rectangle.prototype.toString = function () {
return `[Rectangle ${this.length}x${this.width}]`;
};
// inherits from Rectangle
function Square(size) {
Rectangle.call(this, size, size);
// optional: add new prototype or override existing ones here
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Square,
writable: true,
},
});
Square.prototype.toString = function () {
return `[Square ${this.length}x${this.width}]`;
};
const square = new Square(6);
console.log(square.getArae());
console.log(square.width);
console.log(square.length);
Square构造函数调用了Rectangle构造函数,并传入了this和size量词,一次作为length,另一次作为width,这样做会在新的对象上创建length,和width属性并让他们等于size,这是一种避免再构造函数理重新定义你希望继承的属性的手段。你可以在调用完父类的构造函数后继续添加新属性覆盖已有的属性。
这分两步走的过程在你需要完成自定义类型之间的继承是比较有用,你经常需要修改一个构造函数的原型对象,你也经常需要在子类的构造函数中调用父类的构造函数的原型对象。一般,需要修改prototype来继承方法并且构造函数窃取来设置属性,由于这种做法模范了那些基于类的语言的类继承。,通常呗成为伪类继承。
5。5 访问父类方法
前面例子中,Square类型有自己的toString方法,,子类覆盖父类,但是如果你要访问父类怎么办???代替方法是在通过call或apply调用父类的原型对象的方法时传入一个子类的对象。
function Rectangle(length, width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArae = function () {
return this.length * this.width;
};
Rectangle.prototype.toString = function () {
return `[Rectangle ${this.length}x${this.width}]`;
};
// inherits from Rectangle
function Square(size) {
Rectangle.call(this, size, size);
// optional: add new prototype or override existing ones here
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Square,
writable: true,
},
});
Square.prototype.toString = function () {
vartext = Rectangle.prototype.toString.call(this);
return text.replace("Rectangle, "Square");
};
在这个版本的代码中,Square.prototype.toString()通过call调用Rectangle.prototype.toString()。该方法只需要在返回文本结果钱用"Square","Rectangle"。这种做法看起来有一点冗长,但是唯一有效访问父类的方法。
第六章 对象模式
终于讲到这一节,非常经典的啊。该书也只生写最后10页。精华就在后面,坚持看下去吧。
本章讲述创建高可管理和创建对象的能力。
6.1 私有成员和特权对象
- 模块模式,俗称IIFE返回一个对象,而变量外部不可见,
var yourObject = (function (){
const private = 'pengliheng'
return {
a:1,
b:2
}
}());
console.log(yourObject);
上面创建了匿名函数立即执行,。同时这意味着,这个函数仅存在于被调用瞬间,一旦执行就立即销毁了。IIFE常见的用于浏览器端环境打包模式。适用于模块化。
6.1.2 构造函数的私有成员
模块定义单个对象的是由属性上十分有效i,但对于那些同样需要私有属性的自定义类型又要怎么做?你可以在构造函数内部使用类似模块来创建每个实例的私有数据。如下例子:
function Person(name) {
// define a variable only accessible inside of the Person constructor
let age = 25;
this.name = name;
this.getAge = function () {
return age;
};
this.growOlder = function () {
age += 1;
};
}
const person = new Person('Nicholas');
console.log(person.name);
console.log(person.getAge());
person.age = 100;
console.log(person.getAge());
person.growOlder();
console.log(person.getAge());
console.log(person);
上面代码中Person构造函数就有一个本地变量age。该变量被用于getAge()和growOlder()方法。当你创建Person的一个实例时候,该实例接受其自身的age变量,getAge()方法和growOlder()方法。这种做法很多时候都有类似模块模式,构造函数创建一个本地作用域返回this对象。上一章讨论过,将对象直接放在对象实例上不如放在其原型上面有效,如果你需要实例私有数据,这是唯一有效方法。 但是如果你需要所有实例都可以共享私有数据,就好像它被定义在原型上面那样,可以结合模块模式和构造函数,如下。
const Person = (function () {
// define a variable only accessible inside of the Person constructor
let age = 25;
function InnerPerson(name) {
this.name = name;
}
InnerPerson.prototype.getAge = function () {
return age;
};
InnerPerson.prototype.growOlder = function () {
age += 1;
};
return InnerPerson;
}());
const person = new Person('Nicholas');
console.log(person.name);
console.log(person.getAge());
person.age = 100;
console.log(person.getAge());
person.growOlder();
console.log(person.getAge());
console.log(person);
上面代码InnerPerson构造函数被定义在IIFE中。变量age被定义在构造函数外,但是在模块内部,并被两个原型对象的方法使用。IIFE返回InnerPerson构造函数作为全局作用域里面的Person构造函数使用,最终Person实例全部共享age作为闭包内部变量。
6.2 混入
JavaScript大量使用了伪类继承和原型对象继承,还有另一种就是混入。第一个对象接收者,通过直接赋值第二个对象提供者的属性从而接受了这些属性。
funtion mixin(receiver, supplier) {
for(var property in supplier){
if(supplier.hasOwnPrototype(property){
receiver[property] = supplier[property];
}
}
}
函数mixin()接受2个参数,接收者和提供者,,通过枚举方式,将所有可枚举的属性赋值给接收者,通过for...in循环所有可枚举属性。
作用域安全的构造函数
涨见识。。
如果不通过new就创建实例函数
var a =Array();
上图为什么呢,因为它上了安全套
function Person(name){
if(this instanceof Person){
this.name = name;
} else {
return new Person(name);
}
}
对于上面这个构造函数,当自己呗new调用的时候,就设置name属性,如果不被new调用的时候,则以new递归调用自己来为自己创建正确的属性。这么做就能保证行为一致性。
var person1 = new Person('peng');
var person2 = Person('peng');
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true
全书完。这是我写的最细的一本书了,短短92页,我每一页都给照抄下来了😭。
reference:JavaScript面向对象精要