js高程读书笔记 第22章 高级技巧

[TOC]

本章内容

  • 使用高级函数
  • 防篡改对象
  • Yielding Timers

    高级函数

    安全的类型检测

    在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function isArray(value) {
return Object.prototype.toString.call(value) == "[object Array]";
}

function isFunction(value) {
return Object.prototype.toString.call(value) == "[object Function]";
}

function isRegExp(value) {
return Object.prototype.toString.call(value) == "[object RegExp]";
}

function isNativeJSON(value) {
return Object.prototype.toString.call(value) == "[object JSON]";
}

作用域安全的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
alert(person1.name); //"Nicholas"
alert(person1.age); //29
alert(person1.job); //"Software Engineer"

var person2 = Person("Nicholas", 29, "Software Engineer");
alert(person2); //undefined
alert(window.name); //"Nicholas"
alert(window.age); //29
alert(window.job); //"Software Engineer"

问题出在如果没有使用new操作符来调用构造函数的情况下,会创建一个新的Person对象,同时会给它分配这些属性。直接调用Person,this会映射到全局对象windows上,导致错误对象属性的意外增加。

作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name, age, job){
if (this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
} else {
return new Person(name, age, job);
}
}

var person1 = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //""
alert(person1.name); //"Nicholas"

var person2 = new Person("Shelby", 34, "Ergonomist");
alert(person2.name); //"Shelby"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}

function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}

var rect = new Rectangle(5, 10);
alert(rect.sides); //undefined

这段代码中,Polygon构造函数是作用域安全的,然而Rectangle构造函数不是。新创建一个Rectangle实例之后,这个实例应该通过Polygon.call()来继承Polygon的sides属性。但是由于Polygon对象构造函数是作用域安全的,this对象并非Polygon的实例,所以会创建并返回一个新的Polygon对象。Rectangle构造函数中的this对象并没有得到增长,同时Polygon.call()返回的值也没有用到,所以Rectangle实例中就不会有sides属性。

使用原型链方式来解决这个问题。

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 Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}

function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}

Rectangle.prototype = new Polygon();

var rect = new Rectangle(5, 10);
alert(rect.sides); //2

惰性载入函数

上一章的createXHR()函数

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
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;

for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//skip
}
}
}

return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}

if语句不必每次执行,使用惰性载入技巧。

第一种方式就是在函数被调用时再处理函数。

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
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
createXHR = function(){
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined"){
createXHR = function(){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;

for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
} catch (ex){
//skip
}
}
}

return new ActiveXObject(arguments.callee.activeXString);
};
} else {
createXHR = function(){
throw new Error("No XHR object available.");
};
}

return createXHR();
}

var xhr1 = createXHR();
var xhr2 = createXHR();

在第一次调用的过程中,该函数会被覆盖为另一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。

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
var createXHR = (function(){
if (typeof XMLHttpRequest != "undefined"){
return function(){
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined"){
return function(){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;

for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//skip
}
}
}

return new ActiveXObject(arguments.callee.activeXString);
};
} else {
return function(){
throw new Error("No XHR object available.");
};
}
})();

var xhr1 = createXHR();
var xhr2 = createXHR();

这个例子中使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪个函数去实现。实际的逻辑都一样。不一样的地方就是第一行代码(使用var定义函数)、新增了自执行的匿名函数,另外每个分支都返回正确的函数定义,以便立即将其赋值给createXHR()。

惰性载入函数的优点是只在分支代码时牺牲一点儿性能。

函数绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
var handler = {
message: "Event handled",

handleClick: function(event) {
alert(this.message);
}
};

var btn = document.getElementById("my-btn");
btn.addEventListener("click", handler.handleClick);//这样会显示undefiend,没有保存handler.handClick()环境,所以this对象最后是指向DOM按钮而非handler。用闭包来解决这个问题
btn.addEventListener("click",function(event) {
handler.handleClick(event);
});//闭包

bind()函数可以将函数绑定到指定环境

1
2
3
4
5
function bind(fn, context) {
return function() {
return fn.apply(context, arguments);
};
}

如下方式使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bind(fn, context) {
return function() {
return fn.apply(context, arguments);
};
}
var handler = {
message: "Event handled",

handleClick: function(event) {
alert(this.message);
}
};

btn.addEventListener("click", bind(handler.handleClick, handler));

ECMAScript5定义了原生bind()方法。

1
2
3
4
5
6
7
8
9
10
var handler = {
message: "Event handled",

handleClick: function(event){
alert(this.message + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));

函数柯里化 function currying

基本方法与函数绑定是一样的:使用一个闭包返回一个函数。区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

1
2
3
4
5
6
7
8
9
10
function add(num1, num2) {
return num1 + num2;
}

function curriedAdd(num2) {
return add(5, num2);
}

console.log(add(2,3));
console.log(curriedAdd(3));

柯里化函数动态创建步骤:

调用另一个函数并为它传入要柯里化的函数和必要参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
};
}

function add(num1, num2){
retrun num1 + num2;
}

var curriedAdd = curry(add, 5);
console.log(curriedAdd(3));//8

Array.prototype.slice.call(arguments)能将具有length属性的对象转成数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function bind(fn, context){
var args = Array.prototype.slice.call(arguments, 2);
return function(){
var innerArgs = Array.prototype.slice.call(arguments),
finalArgs = args.concat(innerArgs);
return fn.apply(context, finalArgs);
};
}

var handler = {
message: "Event handled",

handleClick: function(name, event){
alert(this.message + ":" + name + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));

1
2
3
4
5
6
7
8
9
10
var handler = {
message: "Event handled",

handleClick: function(name, event){
alert(this.message + ":" + name + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "my-btn"));

防篡改对象

不可拓展对象

preventExtensions(object)方法,不能给对象添加属性和方法。但是可以删除和修改已有成员。

Object.preventExtensions(person);
Object.isExtensible(person);

密封的对象

密封对象不可扩展,已有成员的[[Configurable]]特性将被设置为false。不能删除属性和方法。
Object.seal(person);
Object.isSeal(person);

冻结的对象

frozen对象不可拓展又是密封的,而且对象属性的[[Writable]]特性会被设置为false。如果定义[[Set]]函数,访问器属性仍是可写的。
Object.frozen(person);
Object.isFrozen(person);

高级定时器

关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时执行代码。

重复的定时器

使用setInterval()创建的定时器,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。

这会出现一个问题:如果上一个定时器在执行的过程中,下一个定时器启动。这时候就会跳过这个定时器。

为了避免setInterval()重复定时器的缺点,可以使用链式setTImeout()

1
2
3
4
setTimeout(function() {
//要处理的代码
setTimeout(arguments.callee, interval);
}, interval);

这段代码可以确保在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。

Yielding Processes

JavaScript的执行时是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也就越久。

如果处理循环必须同步完成且数据必须按数据完成,就可以用定时器分割这个循环。这种技术叫做数组分块(Array chunking)技术。

1
2
3
4
5
6
7
8
9
10
setTimeout(function() {
//取出下一条目并处理
var item = array.shift();
process(item);

//若还有条目,再设置另外一个定时器
if(array.length > 0) {
setTimeout(arguments.callee, 100);
}
}, 100);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];

function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);

if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}

function printValue(item){
var div = document.getElementById("myDiv");
div.innerHTML += item + "<br>";
}

chunk(data, printValue);

需要注意的地方是,传递给chunk()的数组是用作一个队列的,因此处理数据的同时,数组中的条目也在改变。如果想保持元素组不变,应该将数组的克隆传递给chunk()

1
chunk(data.concat(), printValue);

当不传递任何参数调用某个数组的concat()方法时,将返回和原来数组中项目一样的数组。

数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其它的浏览器处理机会运行,这样就可以避免长时间运行脚本的的错误。

函数节流

基本思想:某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过过了,这个操作就没有任何意义。如果前一个定时器尚未执行,其实就是将替换为一个新的定时器。

目的:只有在执行函数的请求停止了一段时间之后才执行。
以下代码是该模式的基本形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var processor = {
timeoutId: null,

//实际进行处理的方法
performProcessing: function() {
//实际执行的代码
},

//初始处理调用的方法
process: function() {
clearTimeout(this.timeoutId);

var thar = this;
this.timeoutId = setTimeout(function() {
that.performProcessing();
}, 100);
}
};


//尝试开始执行
processor.process();

这段代码中创建了一个叫做processor对象。这个对象还有两个方法:process()和performProcessing()。前者是初始化任何处理必须调用的,后者则实际进行应完成的处理。

这个模式可以使用throttle()函数来简化。

1
2
3
4
5
6
function throttle(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context);
}, 100);
}

自定义事件

事件是一种叫做观察者的设计模式。

观察者模式由两类对象组成:主体和观察者。

主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事件也就是说它可以独自存在并正常运作即使观察者不存在。

从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,事件处理程序代码便是观察者。

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
function EventTarget(){
this.handlers = {};
}

EventTarget.prototype = {
constructor: EventTarget,

addHandler: function(type, handler){
if (typeof this.handlers[type] == "undefined"){
this.handlers[type] = [];
}

this.handlers[type].push(handler);
},

fire: function(event){
if (!event.target){
event.target = this;
}
if (this.handlers[event.type] instanceof Array){
var handlers = this.handlers[event.type];
for (var i=0, len=handlers.length; i < len; i++){
handlers[i](event);
}
}
},

removeHandler: function(type, handler){
if (this.handlers[type] instanceof Array){
var handlers = this.handlers[type];
for (var i=0, len=handlers.length; i < len; i++){
if (handlers[i] === handler){
break;
}
}

handlers.splice(i, 1);
}
}
};

使用EventTarget类型的自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleMessage(event){
alert("Message received: " + event.message);
}

var target = new EventTarget();

target.addHandler("message", handleMessage);

target.fire({ type: "message", message: "Hello world!"});

target.removeHandler("message", handleMessage);

target.fire({ type: "message", message: "Hello world!"});

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
function object(o){
function F(){}
F.prototype = o;
return new F();
}

function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //create object
prototype.constructor = subType; //augment object
subType.prototype = prototype; //assign object
}

function Person(name, age){
EventTarget.call(this);
this.name = name;
this.age = age;
}

inheritPrototype(Person,EventTarget);

Person.prototype.say = function(message){
this.fire({type: "message", message: message});
};



function handleMessage(event){
alert(event.target.name + " says: " + event.message);
}

var person = new Person("Nicholas", 29);

person.addHandler("message", handleMessage);

person.say("Hi there.");

拖放

1
2
<div id="myDiv1" class="draggable" style="background:red;width:100px;height:100px;position:absolute"></div>
<div id="myDiv2" class="draggable" style="background:blue;width:100px;height:100px;position:absolute;left:100px"></div>
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
var DragDrop = function(){

var dragging = null;

function handleEvent(event){

//get event and target
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

//determine the type of event
switch(event.type){
case "mousedown":
if (target.className.indexOf("draggable") > -1){
dragging = target;
}
break;

case "mousemove":
if (dragging !== null){

//assign location
dragging.style.left = event.clientX + "px";
dragging.style.top = event.clientY + "px";
}
break;

case "mouseup":
dragging = null;
break;
}
};

//public interface
return {
enable: function(){
EventUtil.addHandler(document, "mousedown", handleEvent);
EventUtil.addHandler(document, "mousemove", handleEvent);
EventUtil.addHandler(document, "mouseup", handleEvent);
},

disable: function(){
EventUtil.removeHandler(document, "mousedown", handleEvent);
EventUtil.removeHandler(document, "mousemove", handleEvent);
EventUtil.removeHandler(document, "mouseup", handleEvent);
}
}
}();

DragDrop.enable();

元素的位置
element.offsetTop
element.offsetLeft

鼠标的位置
event.clientY
event.clientX

原先的代码会让被拖动的元素的左上角在鼠标下方。

现在对鼠标相对元素的位置记录后再次计算元素的绝对位置。

1
2
3
<div id="myDiv1" class="draggable" style="background:red;width:100px;height:100px;position:absolute"></div>
<div id="myDiv2" class="draggable" style="background:blue;width:100px;height:100px;position:absolute;left:100px"></div>
<script type="text/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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

var DragDrop = function(){

var dragging = null,
diffX = 0,
diffY = 0;

function handleEvent(event){

//get event and target
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

//determine the type of event
switch(event.type){
case "mousedown":
if (target.className.indexOf("draggable") > -1){
dragging = target;
diffX = event.clientX - target.offsetLeft;
diffY = event.clientY - target.offsetTop;
}
break;

case "mousemove":
if (dragging !== null){

//assign location
dragging.style.left = (event.clientX - diffX) + "px";
dragging.style.top = (event.clientY - diffY) + "px";
}
break;

case "mouseup":
dragging = null;
break;
}
};

//public interface
return {
enable: function(){
EventUtil.addHandler(document, "mousedown", handleEvent);
EventUtil.addHandler(document, "mousemove", handleEvent);
EventUtil.addHandler(document, "mouseup", handleEvent);
},

disable: function(){
EventUtil.removeHandler(document, "mousedown", handleEvent);
EventUtil.removeHandler(document, "mousemove", handleEvent);
EventUtil.removeHandler(document, "mouseup", handleEvent);
}
}
}();

DragDrop.enable();

添加自定义事件

1
2
3
<div id="status"></div>
<div id="myDiv1" class="draggable" style="top:100px;left:0px;background:red;width:100px;height:100px;position:absolute"></div>
<div id="myDiv2" class="draggable" style="background:blue;width:100px;height:100px;position:absolute;top:100px;left:100px"></div>
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
var DragDrop = function(){

var dragdrop = new EventTarget(),
dragging = null,
diffX = 0,
diffY = 0;

function handleEvent(event){

//get event and target
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

//determine the type of event
switch(event.type){
case "mousedown":
if (target.className.indexOf("draggable") > -1){
dragging = target;
diffX = event.clientX - target.offsetLeft;
diffY = event.clientY - target.offsetTop;
dragdrop.fire({type:"dragstart", target: dragging, x: event.clientX, y: event.clientY});
}
break;

case "mousemove":
if (dragging !== null){

//assign location
dragging.style.left = (event.clientX - diffX) + "px";
dragging.style.top = (event.clientY - diffY) + "px";

//fire custom event
dragdrop.fire({type:"drag", target: dragging, x: event.clientX, y: event.clientY});
}
break;

case "mouseup":
dragdrop.fire({type:"dragend", target: dragging, x: event.clientX, y: event.clientY});
dragging = null;
break;
}
};

//public interface
dragdrop.enable = function(){
EventUtil.addHandler(document, "mousedown", handleEvent);
EventUtil.addHandler(document, "mousemove", handleEvent);
EventUtil.addHandler(document, "mouseup", handleEvent);
};

dragdrop.disable = function(){
EventUtil.removeHandler(document, "mousedown", handleEvent);
EventUtil.removeHandler(document, "mousemove", handleEvent);
EventUtil.removeHandler(document, "mouseup", handleEvent);
};

return dragdrop;
}();

DragDrop.enable();

DragDrop.addHandler("dragstart", function(event){
var status = document.getElementById("status");
status.innerHTML = "Started dragging " + event.target.id;
});

DragDrop.addHandler("drag", function(event){
var status = document.getElementById("status");
status.innerHTML += "<br>Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")";
});

DragDrop.addHandler("dragend", function(event){
var status = document.getElementById("status");
status.innerHTML += "<br>Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")";
});

总结

  • 使用惰性载入函数,将任何代码分支推迟到第一次调用函数的时候。
  • 函数绑定可以创建始终在指定环境中运行的函数,同时函数柯里化可以创建已经填了某些参数的函数。
  • 将绑定和柯里化组合起来,就可以获得在任意环境中以任意参数执行任意函数的方法。

  • 不可拓展的对象,不允许给对象添加新的属性或方法

  • 密封的对象,也是不可拓展的对象,不允许删除已有的属性和方法
  • 冻结的独享,也是密封的对象,不允许重写对象的成员

  • 定时器代码是放在一个等待区域,知道时间间隔到了之后,将代码添加到JavaScript的处理队列中,等待下一次JavaScript进程空闲的时候被执行。

  • 每次一段代码执行结束之后,都会有一小段空闲时间进行其他浏览器处理
  • 这种行为意味着,可以使用定时器将长时间运行的脚本切分为一小块一小块可以在以后运行的代码段。这种做法有助于Web应用对用户交互有更积极的响应。