JavaScript最强大的特性是其灵活性。支持不同的编程风格,可以采用函数式编程,也可以采用面向对象编程。

简介

##灵活性

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
27
28
29
30
31
//函数式编程
function startAnimation() {}
function stopAnimation() {}
//面向对象编程1
var Anim = function () {}
Anim.prototype.start = function () {}
Anim.prototype.stop = function () {}

//面向对象编程2
var Anim = function () {}
Anim.prototype = {
start: function () {},
stop: function () {}
}

//面向对象编程3
Function.prototype.method = function (name, fn) {
this.prototype[name] = fn;
}

var Anim = function () {}
Anim.method('start', function () {});
Anim.method('stop', function () {});

//面向对象编程4
Function.prototype.method = function (name, fn) {
this.prototype[name] = fn;
return this;
}
var Anim = function () {}
Anim.method('start', function () {}).method('stop', function () {});

弱类型

在JavaScript中,定义变量时不必声明其类型,变量可以根据所赋的值改变类型,JavaScript会根据需要进行类型转换,所以,一般不用为类型错误操心。

函数

在JavaScript中,函数是一等对象,它可以存储在变量中,可以作为参数传给其他函数,可以作为返回值,这正是构建面向对象架构的基础。

JavaScript具有函数级的作用域,这意味着定义在函数的变量在函数外部不能访问。JavaScript的作用域又是词法性质的(lexically scoped)。意味着函数运行在定义它的作用域中,而不是在调用它的作用域中。

对象

在JavaScript中,一切都是对象,即便是原始类型,在必要的时候也会被自动包装成对象,而且所有的对象都是易变的(mutable),如你可以为函数添加属性,意味着你可以对先前定义的类和实例化的对象进行修改。

在JavaScript中,任何东西都可以在运行时修改,这是JavaScript中很少进行类型检查的原因之一。

继承

JavaScript中继承是基于对象(原型)继承,可以模仿基于类的(类式)继承。

设计模式

用于创建不同类型的对象的套路被称为设计模式(design pattern)。

设计模式优点:

  • 可维护性:降低模块间的耦合度
  • 沟通:提供一套通用术语
  • 性能:提供运行速度

设计模式缺点:

  • 复杂性:获得可维护性的代价就是代码变得复杂
  • 性能:多数的设计模式会对性能有所拖累

接口

可重用面向对象设计的第一条原则:针对接口而不是实现编程。

在JavaScript中没有内置的创建或者实现接口的方法。

在JavaScript中必须用手工的方法保证某个类实现了某个接口。

注释法描述接口

属性检查模仿接口

鸭式辨型模仿接口

封装

为对象创建私有成员是任何面向对象语言中最基本的特性之一。通过将一个方法或者属性声明为私有的,可以让对象的实现细节对其他对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束。

JavaScript是一种面向对象的语言,但是它不具备将成员声明成公用或私用的任何内置机制。

通过对象体现封装

JavaScript中创建对象有三种方式:

  • 门户开发型(fully exposed) :只提供公用成员。
  • 使用下划线表示方法或者属性的私用性:只是一种约定。
  • 使用闭包:实现真正的私有性
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//门户开放型
//无法检查isbn数据完整性--添加验证
var Book = function (isbn, title, author) {
if (isbn = undefined) throw new Error('Book Constructor requires an isbn');
this.isbn = isbn;
this.title = title || 'No title specified';
this.author = author || 'No author specified';
}
Book.prototype.display = function () {};

//添加验证
//可以直接修改isbn的值,使得验证无效,因为该验证只是在构造器中--使用get/set
var Book = function (isbn, title, author) {
if (!this.checkIsbn(isbn)) throw new Error('Book: Invaild IsBN');
this.isbn = isbn;
this.title = title;
this.author = author;
}

Book.prototype = {
checkIsbn: function (isbn) {
if (isbn == undefined || typeof isbn != 'string') {
return false;
}
isbn = isbn.replace(/-/, '');
if (isbn.length != 10 && isbn.length != 13) {
return false;
}

var sum = 0;
if (isbn.length === 10) {
if (!isbn.match(/^\d{9}/)) {
return false;
}

for (var i = 0; i < 9; i++) {
sum += isbn.charAt(i) * (10 - i);
}

var checksum = sum % 11;
if (checksum === 10) checksum = 'X';
if (isbn.charAt(9) != checksum) {
return false;
}
} else {
if (!isbn.match(/^\d{12}/)) {
return false;
}
for (var i = 0; i < 12; i++) {
sum += isbn.charAt(i) * ((i % 2 === 0) ? 1 : 3);
}

var checksum = sum % 10;
if (isbn.charAt(12) != checksum) {
return false;
}
}
return true;
},
display: function () {}
};
//GETTER/SETTE
//使用赋值器,每次赋值都会验证,但仍然可以直接设置,因为属性是公共属性--特权方法
var Book = function (isbn, title, author) {
this.setIsbn(isbn);
this.setTitle(title);
this.setAuthor(author);
}

Book.prototype = {
checkIsbn: function (isbn) {},
getIsbn: function () {
return this.isbn;
},
setIsbn: function (isbn) {
if (!this.checkIsbn(isbn)) throw new Error('Book :Invalid isbn');
this.isbn = isbn;
},
getTitle: function () {
return this.title;
},
setTitle: function (title) {
this.title = title || 'No title specified';
},
getAuthor: function () {
return this.author;
},
setAuthor: function (author) {
this.author = author || 'No author specified';
},
display: function () {}
}
//特权方法:利用函数作用域
//特权方法太多,会占用内存,因为每个对象,都有特权方法的副本--静态方法和属性
var Book = function (newIsbn, newTitle, newAuthor) {
var isbn, title, author; //私有属性
function checkIsbn(isbn) {}; //私有方法
//特权方法
this.getIsbn = function () {
return isbn;
};
this.setIsbn = function (newIsbn) {
if (!checkIsbn(newIsbn)) throw new Error('Book :Invalid isbn');
isbn = newIsbn;
};
this.getTitle = function () {
return title;
};
this.setTitle = function (newTitle) {
title = newTitle || 'No title specified';
};
this.getAuthor = function () {
return author;
};
this.setAuthor = function (newAuthor) {
author = newAuthor || 'No author specified';
};
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
}

Book.prototype = {
display: function () {}
};

//闭包:立即执行函数,正是实现私有化
//实例化是调用内层构造函数
var Book = (function () {
var numOfBooks = 0;//静态属性
function checkIsbn(isbn) {}//静态方法

return function (newIsbn, newTitle, newAuthor) {
var isbn, title, author;
this.getIsbn = function () {
return isbn;
};
this.setIsbn = function (isbn) {
if (!checkIsbn(isbn)) throw new Error('Book :Invalid isbn');
isbn = isbn;
};
this.getTitle = function () {
return title;
};
this.setTitle = function (title) {
title = title || 'No title specified';
};
this.getAuthor = function () {
return author;
};
this.setAuthor = function (author) {
author = author || 'No author specified';
};

this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
}
})();

// public static method
Book.convertToTitleCase = function (inputString) {}
Book.prototype = {
display: function () {}
}

通过常量体现封装

常量就是一些不能被修改的变量。在JavaScript中,可以通过创建只有取值器没有赋值器的私有变量模仿常量。因为常量不会因为对象的实例不同而变化,所以将常量设计成私有静态属性合乎情理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Class = (function () {
//静态私有变量
var constants = {
UPPER_BOUND: 100,
LOWER_BOUND: -100
};
//构造器
var ctor = function (args) {}
//私有静态方法(只读访问)
ctor.getConstant = function (name) {
return constants[name];
}

return ctor;
})();

Class.getConstant('UPPER_BOUND');

继承

类式继承

在JavaScript中每个函数对象【Function】都有一个名为prototype的属性,当访问对象的某个成员时,JavaScript会现在当前对象中查找,如果没有找到,会在prototype属性所指向的对象中查找,如果还没有找到,就会沿着原型链(_proto_)逐一访问每个原型对象,直到原型链顶端Object.prototype对象。

这意味着继承一个类,就是将子类的prototype属性设置成超类的一个实例即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//类式继承
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
}

function Author(name, books) {
Person.call(this, name);
this.books = books;
}

Author.prototype = new Person();
Author.prototype.constructor = Author;//设置原型就将子类的构造器改变,可以重置
Author.prototype.getBooks = function () {
return this.books;
}

//定义一个构造器函数时,其默认的prototype对象是一个Object类型的实例,其constructor属性会自动设置成构造函数本身,如果手工将其prototype设置为其他对象,那么新对象自然不会具有源对象的constructor属性值,所以需要重新设置其constructor属性。

原型继承

类式继承分两步,首先用一个类的声明定义对象结构,其次实例化该类以创建一个新对象。这种方式创建的对象都有一套该类所有实例属性的副本。

使用原型继承,只需要创建一个对象即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//原型继承
var Person = {
name: 'CHC',
getName: function () {
return this.name;
}
}

var Author = clone(Person);
Author.books = [];
Author.getBooks = function () {
return this.books;
}

function clone(object) {
function F() {}
F.prototype = object;
return new F();
}

原型继承更能节约内存,原型链读取成员的方式使得所有克隆的对象都共享每个属性和方法的唯一一份实例,相比之下,类式继承创建的每个对象在内存中都有自己的一套属性和方法。

链式调用

在JavaScript中对象是作为引用被传递的,所以可以让每个方法都传回对象的引用。如果让一个类的每个方法都返回this值,那么它就成了一个支持方法的链式调用的类。

链式调用有助于简化代码的编写工作,在某种程度上也让代码更简洁,易读。

方法的链式调用可以视为选择网页上一个元素并对其进行一个或者多个操作的过程。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
(function () {
//use a private class.
function _$(els) {
this.elements = [];
for (let i = 0; i < els.length; i++) {
var element = els[i];
if (typeof element === "string") {
element = document.getElementById(element);
}
this.elements.push(element);
}
}
//原型上添加方法,返回对象本身
_$.prototype = {
each: function (fn) {
for (let i = 0; i < this.elements.length; i++) {
fn.call(this, this.elements[i]);
}
return this;
},
setStyle: function (prop, val) {
this.each(function (el) {
el.style[prop] = val;
});
return this;
},
show: function () {
var that = this;
this.each(function (el) {
that.setStyle('display', 'block');
});
return this;
},
addEvent: function (type, fn) {
var add = function (el) {
if (window.addEventListener) {
el.addEventListener(type, fn, false);
} else if (window.attachEvent) {
el.attachEvent('on' + type, fn);
}
};
this.each(function (el) {
add(el);
});
return this;
}
};
//统一访问入口
window.$ = function () {
return new _$(arguments);
}
})();

//链式调用
$(window).addEvent('load', function () {
$('test1', 'test2').show()
.setStyle('color', 'red')
.addEvent('click', function () {
$(this).setStyle('color', 'green');
});
});

##构建库

一个JavaScript库中包含的基本特性:

  • Event:添加和删除事件监听器
  • DOM:样式管理
  • Ajax:XMLHttpRequest规范化管理
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
27
28
29
30
31
32
33
34
35
36
37
Function.prototype.method = function (name, fn) {
this.prototype[name] = fn;
return this;
}

(function () {
function _$(els) {}
//Event
_$.method('addEvent', function (type, fn) {})
.method('getEvent', function (e) {})
//DOM:Class
.method('addClass', function (className) {})
.method('removeClass', function (className) {})
.method('replaceClass', function (oldClass, newClass) {})
.method('hasClass', function (className) {})
//DOM:style
.method('getStyle', function (prop) {})
.method('setStyle', function (prop, val) {})
//ajax
.method('load', function (url, mehtod) {});

window._$ = function () {
return new _$(arguments);
};
})();

//如果某个库已经定了_$函数,那么我们的这个库就会改写它
//简单的解决办法是在源代码中为_$起个别名,但如果类库给别人使用,每次都需要该名字,显然不行
//可以使用安装器Installer:自己命名
window.installHelper = function (scope, interface) {
scope[interface] = function () {
return new _$(arguments);
}
}
//使用
installHelper(window, '$');
$('example').show();

链式获取数据

链式调用很适合于赋值器的方法,但对于取值器的方法,返回的是数据,而不是this。如果需要链式调用,可以使用回调技术。

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
27
28
29
30
31
//不用回调
window.API = window.API || function () {
var name = 'Hello world';
this.setName = function (newName) {
name = newName;
return this;
}
this.getName = function () {
return name;
}
}

var o = new API;//调用构造函数实例化一个对象,后面添加括号也可以不加括号,括号不是执行的意思,是提升优先级
console.log(o.getName());//Hello world
console.log(o.setName('chc').getName());//chc

//使用回调
window.API2 = windown.API2 || function () {
var name = 'Hello world';
this.setName = function (newName) {
name = newName;
return this;
}
this.getName = function (callback) {
callback.call(this, name);
return this;
}
}

var o2 = new API2;
o2.getName(console.log).setName('chc').getName(console.log);//Hello world chc

单体模式

单体(singleton)模式是JavaScript中最基本也是最有用的模式之一。它可以用来划分命名空间,以减少全局变量的数量,还可以用分支(branching)技术用来分装浏览器之间的差异,借助单体,可以把代码组织的更一致,从而更容易阅读和维护。

基本结构

最简单的单体实际上就是一个对象字面量,它把一批有关联的方法和属性组织在一起。

1
2
3
4
5
6
7
//Basic Singleton
var Singleton = {
attribute1: true,
attribute2: 10,
method1: function () {},
method2: function () {}
};

单体,传统定义,一个只能被实例化一次并且可以通过一个访问点访问的类,在JavaScript中,单体可以理解为,一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。

命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function findProduct() {}
var findProduct = function () {}
//由于在JavaScript中什么都可以被改写,如上,可以会不经意擦除一个变量,函数甚至整个类,这种错误查找也不方便
//为了避免无意改写变量,最好的解决办法之一就是用单体对象将代码组织在命名空间之中
var MyNamespace = {
findProduct: function () {}
}
var findProduct = function () {};
//命名空间自带描述性,可以增强代码的文档性
//命名空间可以进一步分割
var GiantCorp = {};
GiantCorp.Common = {};//通用处理方法
GiantCorp.ErrorCodes = {};//错误处理方法
GiantCorp.PageHandler = {};//页面处理方法

私有成员

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
MyNamespace.Singleton = {};
MyNamespace.Singleton = function () {
return {};
}();
//或者
MyNamespace.Singleton = (function () {
return {};
})();

//匿名函数中添加私有成员,公有成员添加到单体中返回
MyNamespace.Singleton = (function () {
//private members
var privateAttribute1 = false;
var privateAttribute2 = 10;

function privateMethod1() {}

function privateMethod2() {}
return { //public members
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function () {},
publicMethod2: function () {}
}
})();
//上述单体有称为模块模式(module pattern)

惰性实例化

前面的单体对象都是在脚本加载时被创建出来,对于资源密集型或者配置开销甚大的单体,也许更合理的做法是将实例化推迟到需要使用它的时候,这种技术成为惰性加载(lazy loading),它最常用于那些必须加载大量数据的单体。而那些被用作命名空间,或者代码包装器等最好还是立即实例化。

这种惰性加载单体的特别之处在于,对于它们的访问必须借助于一个静态方法,该静态方法会检查单体是否已经被实例化,如果没有就创建,如果有就返回。

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
27
MyNamespace.Singleton = (function () {
var uniqueInstance; //私有属性,存储单体实例化对象
function constructor() {
//private members
var privateAttribute1 = false;
var privateAttribute2 = 10;

function privateMethod1() {}

function privateMethod2() {}
return { //public members
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function () {},
publicMethod2: function () {}
}
}

return {
getInstance: function () {
if (!uniqueInstance) {
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();

分支

分支(branching)是一种用来把浏览器间的差异封装到在运行期间进行动态设置方法的技术,如创建XHR对象。如果不使用分支,每次在调用方法时,都要进行浏览器嗅探代码,验证缺乏效率,更有效的做法是在脚本加载时一次性确定针对特定浏览器的方法。

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
27
28
29
30
31
32
33
34
35
36
37
38
var SimpleXHRFactory = (function () {
var standard = {
createXHRObject: function () {
return new XMLHttpRequest();
}
};

var activeNew = {
createXHRObject: function () {
return new ActiveXObject('Msxml2.XMLHTTP');
}
};

var activeOld = {
createXHRObject: function () {
return new ActiveXObject('Microsoft.XMLHTTP');
}
};

//TO assign the branch,try each method;return whatever doesn't fail.
var testObject;
try {
testObject = standard.createXHRObject();
return standard;
} catch (error) {
try {
testObject = activeNew.createXHRObject();
return activeNew;
} catch (error) {
try {
testObject = activeOld.createXHRObject();
return activeOld;
} catch (error) {
throw new Error('No XHR object found in this environment');
}
}
}
})();

使用分支技术,所有的那些特性嗅探代码都只会执行一次,而不是每生产一个对象都要执行一次。

这种技术,适用于任何只有在运行时才能确定具体实现的情况。

工厂模式

一个类或者对象中往往会包含别的对象,创建这些成员对象时,使用new关键字和类构造函数,会导致两个类之间的依赖,工厂模式就是消除类之间的依赖。

简单工厂

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var BicycleShop = function () {};
BicycleShop.prototype = {
createBicycle: function (model) {
var bicycle;
switch (model) {
case 'The Speedster':
bicycle = new Speedster();//Speedster:IBicycle
break;
case 'The Lowrider':
bicycle = new Lowrider();//Speedster:IBicycle
break;
default:
bicycle = new ComfortCruiser();//ComfortCruiser:IBicycle
break;
}
return bicycle;
}
};

var californiaCruisers = new BicycleShop();
var yourBike = californiaCruisers.createBicycle('The Speedster');

//如果需要增加一种新型自行车,你就需要修改BicycleShop代码,尽管这个类的实际功能没有改变
//最好的解决办法是将创建实例对象交给一个方法,封装起来就是简单工厂对象
var BicycleFactory = {
createBicycle: function (model) {
var bicycle;
switch (model) {
case 'The Speedster':
bicycle = new Speedster();//Speedster:IBicycle
break;
case 'The Lowrider':
bicycle = new Lowrider();//Lowrider:IBicycle
break;
default:
bicycle = new ComfortCruiser();//ComfortCruiser:IBicycle
break;
}
return bicycle;
}
};

var BicycleShop = function () {};
BicycleShop.prototype = {
sellBicycle: function (model) {
var bicycle = BicycleFactory.createBicycle(model);//简单工厂
return bicycle;
}
};

简单工厂就是把成员对象的创建工作转交给一个外部对象。

但是,如果需要新型实例,就需要修改这个外部对象,应该对修改关闭,新增开放。

工厂模式

工厂模式,对象的创建不是使用另外一个类,而是使用一个子类。

工厂是将其成员对象的实例化推迟到子类中进行的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var SpeedsterFactory = {
createBicycle: function () {
return new Speedster();//Speedster:IBicycle
}
}
var LowriderFactory = {
createBicycle: function () {
return new Lowrider();//Lowrider:IBicycle
}
}
var ComfortCruiserFactory = {
createBicycle: function () {
return new ComfortCruiser();//ComfortCruiser:IBicycle
}
}

var yourBike = SpeedsterFactory.createBicycle();
× 请我吃糖~
打赏二维码