《JavaScript高级程序设计》读书笔记(三)

稍微大致翻了一下这本书后续的内容。为了尽快的进入ES6以及框架部分的学习,决定暂时跳过某些章节的学习。后面打算学习的章节有6、7、13、14、20、21、22、24章,剩下的章节等到用到或者时间富余的时候再看。

这篇博文总结了《JavaScript高级程序设计》的 6~7 章:面向对象的程序设计,以及函数表达式。

面向对象的程序设计

理解对象

1.ECMAScript中有两种属性:数据属性和访问器属性。

特性:描述属性的各种特征。目的是实现JavaScript,因此在JavaScript中不能直接访问。为了表示特性是内部值,放在两对方括号中,例如[[Enumerable]]

2.数据属性:包含一个数据值的位置,在这个位置可以读取和写入值。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。默认为 true。
  • [[Writable]]:表示能否修改属性的值。默认为 true。
  • [[Value]]:包含这个属性的数据值。默认为 undefined。

Object.defineProperty()方法可以修改属性默认的特性。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符对象的属性必须是:configurable、enumerable、writable 和 value 中的一或多个。

3.访问器属性:

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。默认为 true。
  • [[Get]]:在读取属性时调用的函数。默认为 undefined。
  • [[Set]]:在写入属性时调用的函数。默认为 undefined。

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var book = {
_year: 2016, // 下划线表示只能通过对象方法访问的属性
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue) {
if(newValue > 2016) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}); // year 是访问器属性
book.year = 2017;
console.log(book.edition); // 2

4.定义多个属性可用Object.defineProperties()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var book = {};
Object.defineProperties(book, {
_year: {
value: 2016
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2016){
this._year = newValue;
this.edition += newValue - 2016;
}
}
}
});

Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符。

1
2
3
4
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value); // 2016
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"

创建对象

5.工厂模式:虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题。

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
}
return o;
}
var p1 = createPerson('Kyon', 20, 'Software Engineer');
var p2 = createPerson('Someone', 19, 'Lawyer');

6.构造函数模式:以大写字母开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
var p1 = new Person('Kyon', 20, 'Software Engineer');
var p2 = new Person('Someone', 19, 'Lawyer');
// 对象的constructor属性指向其构造函数
console.log(p1.constructor); // function Person(name, age, job){...

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。

也可以使用call()(或者apply())在某个特殊对象的作用域中调用构造函数,调用后这个对象就拥有了所有属性和方法。

1
2
3
var o = new Object();
Person.call(o, 'Kyon', 20, 'Software Engineer');
o.sayName(); // 'Kyon'

使用构造函数的主要问题:每个方法都要在每个实例上重新创建一遍。

1
console.log(p1.sayName === p2.sayName); // false

7.原型模式:每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向函数的原型对象。这个对象包含可以由该类型的所有实例共享的属性和方法。

1
2
3
4
Person.prototype.sayName = function(){
console.log(this.name);
}
console.log(p1.sayName === p2.sayName); // true
  • 理解原型对象:

    – 只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,指向函数的原型对象。

    – 默认所有原型对象都会获得一个 constructor 属性,指向 prototype 属性所在函数。

    – 当调用构造函数创建一个新实例后,实例将有一个 __proto__属性,指向构造函数的原型对象,指针叫[[Prototype]],默认原型指向Object。

    – 实例和构造函数没有直接关系。

    – 读取属性:搜索先从对象实例本身开始,如果没找到,搜索原型对象。

    – 使用isPrototype()来检测构造函数和实例之间是否有关系。

    Object.getPrototypeOf()返回[[Prototype]]的值。

    – 使用hasOwnProperty()来检测属性存在于实例中还是原型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person(){}
Person.prototype.name = 'Kyon';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
};
var p1 = new Person();
var p2 = new Person();
console.log(Person.prototype.isPrototypeOf(p1)); // true
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(p1).name); // 'Kyon'
console.log(p1.hasOwnProperty("name")); // false
p1.name = "someone";
console.log(p1.hasOwnProperty("name")); // true
delete p1.name;
console.log(p1.hasOwnProperty("name")); // false
  • 原型与 in 操作符:在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
1
console.log('name' in p1); // true
  • 更简单的原型语法:用一个包含所有属性和方法的对象字面量来重写整个原型对象。
1
2
3
4
5
6
7
8
9
10
11
function Person(){}
Person.prototype = {
constructor: Person, // 默认的 prototype 对象被重写,需设置
name: 'Kyon',
age: 20,
job: 'Software Engineer',
sayName: function(){
console.log(this.name);
}
};

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍是最初的原型。

  • 原生对象的原型:通过原生对象的原型可以定义新方法。不推荐,可能导致命名冲突或意外重写原生方法。
  • 原型对象的问题:包含引用类型值的属性会被共享。

8.组合使用构造函数模式和原型模式:创建自定义类型最常见方式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 组合使用构造函数模式与原型模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Sam", "Judie"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var p1 = new Person('Kyon', 20, 'Software Engineer');
var p2 = new Person('Someone', 19, 'Lawyer');
p1.friends.push("Vue");
console.log(p1.friends); // "Sam, Judie, Vue"
console.log(p2.friends); // "Sam, Judie"
console.log(p1.sayName === p2.sayName); // true

9.动态原型模式、寄生构造函数模式、稳妥构造函数模式

继承

10.许多OO语言都支持两种继承方式:接口继承(只继承方法签名)和实现继承(继承实际方法)。由于函数没有签名,ECMAScript 中无法实现接口继承,而实现继承主要依靠原型链实现。

11.原型链的构造是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能访问超类型的所有属性和方法。

12.p182开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name); // 借用构造函数
this.age = age;
}
SubType.prototype = new SuperType(); // 原型链
SubType.prototype.constructor = SubType; // construcotr在上一句中被重写
SubType.prototype.sayAge = function () {
console.log(this.age);
}
var instance = new SubType('Kyon', 20);
instance.sayName(); // Kyon
instance.sayAge(); // 20

说实话,这部分看得我脑壳疼。要不我们直接用ES6引入的class可好?等我哪一天沐浴更衣虔诚焚香再来看…

函数表达式

递归

1.在严格模式下,不能通过脚本访问arguments.callee来实现递归。可以用命名函数表达式来实现。

1
2
3
4
5
6
7
var factorial = (function f(num){
if(num <= 1){
return 1;
} else {
return num * f(num-1);
}
});

闭包

2.闭包是指有权访问另一个函数作用域中的变量的函数(匿名函数的 function 关键字后没有标识符,二者不能混用)。

3.创建闭包的常见方式:在一个函数内部创建另一个函数。

1
2
3
4
5
6
7
8
9
function outer(){
var name = "Kyon";
return function(){
console.log(name);
}
}
var inner = outer();
inner(); // Kyon
inner = null; // 解除对outer内部的匿名函数的引用,以释放内存

在外部函数内部定义的内部函数将外部函数的活动对象(作为变量对象使用)添加到它的作用域链中;外部函数执行完毕后,其活动对象不会被销毁,因为内部函数的作用域链仍在引用这个活动对象;外部函数执行完毕后,内部函数仍然可以访问到其定义的所有变量。

4.由于闭包会携带包含它的函数的作用域,过度使用可能导致内存占用过多,要慎重使用。

5.返回的函数并没有立刻执行,而是等到调用f()才执行。因此返回函数不能引用任何循环变量,或者后续会发生变化的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function count(){
var arr = [];
for(var i=1; i<=3; i++){
arr[i] = function(){
return i * i;
};
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
// 返回的函数引用了变量i,但并非立即执行。执行时i已变成4
f1(); // 16
f2(); // 16
f3(); // 16

一定要引用循环变量的方法:再创建一个匿名函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。

1
2
3
4
5
6
7
8
9
10
11
12
function count(){
var arr = [];
for(var i=1; i<=3; i++){
arr[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
// i的当前值复制给参数num,匿名函数内部又创建并返回一个访问num的闭包,使得result数组中的每个函数都有自己num变量的一个副本

这里用了一个“创建一个匿名函数并立即执行”的语法:

1
2
3
(function(x){
return x * x;
})(3); // 9

6.闭包中使用 this 对象:匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

1
2
3
4
5
6
7
8
9
10
11
12
var name = "The Window";
var object = {
name: "My Object",
getName: function(){
return function(){
return this.name;
};
}
};
console.log(object.getName()); // "The Window"(非严格模式)

把外部作用域中的 this 对象保存在一个闭包能够访问的变量中,就可以让闭包访问该对象了(想访问作用域中的 arguments对象同理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window";
var object = {
name: "My Object",
getName: function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(object.getName()); // "My Object"

7.补充:利用闭包可以实现私有变量的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用JavaScript创建一个计数器
'use strict';
function create_counter(initial){
var x = initial || 0;
return {
inc: function(){
x += 1;
return x;
}
}
}
// 使用
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12

模仿块级作用域

8.用匿名函数模仿块级作用域:

JavaScript将 function 关键字当作一个函数声明的开始,而函数声明后不能加圆括号。

将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随的另一对圆括号会立即调用这个函数。

1
2
3
4
5
6
7
(function(){
// 这里是块级作用域
})();
function(){
// 这里是块级作用域
}(); // Error!

9.这种技术经常用于限制向全局作用域中添加过多的变量和函数;同时可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

1
2
3
4
5
6
(function(){
var now = new Date();
if(now.getMonth() == 11 && now.getDate() == 25){
console.log("Merry Christmas!");
}
})();

私有变量

10.任何在函数中定义的变量,都可以认为是私有变量。

11.有权访问私有变量和私有函数的公有方法被称为特权方法。两种在对象上创建特权方法的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1.构造函数中定义特权方法
function MyObject(){
// 私有变量和私有函数
var privateVar = 10;
function privateFunc(){
return false;
}
// 特权方法
this.publicMethod = function(){
privateVar++;
return privateFunc();
};
}
// 2.利用私有和特权成员
function Person(){
this.getName = function(){
return name;
};
this.setName = function(value){
name = value;
};
}

12.可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。