下述内容主要讲述了《JavaScript高级程序设计(第3版)》第22章关于“高级技巧”。
一、高级函数
函数是第一等公民,所有函数都是对象。
1. 安全的类型检测
JavaScript内置的类型检测机制并非完全可靠。
var isArray = value instanceof Array;
以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。(Array是window的属性)如果value是在另外一个iframe中定义的数组,上述代码则返回false。
注意:BOM的核心对象时window,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,它既是通过JavaScript访问浏览器窗口的一个接口,又是ECMAScript规定的global对象。
解决上述问题:
Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。
function isArray(value) {
return Array.isArray(value) || Object.prototype.toString.call(value) == "[object Array]";
}
function isType(type) {
return function(obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
}
var isObject = isType("Object")
var isArray = Array.isArray || isType("Array")
注意:Object.prototype.toString()本身可能被修改!
2. 作用域安全的构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例。
var p1 = new Person("lg", 26);
console.log(p1.name, p1.age); // lg 26
到不使用new调用时,由于this是在运行时绑定的,直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。
var p2 = Person("camile", 26);
console.log(p2.name, p2.age); // TypeError: Cannot read prototype ‘name‘
console.log(window.name, age); // camile 26
注意:由于window的name属性用于识别链接目标和iframe的,上述覆盖可能会导致严重的问题。
解决上述问题:作用域安全构造函数
function Person(name, age) {
if(this instanceof Person) {
this.name = name;
this.age = age;
} else {
return new Person(name, age);
}
}
var p3 = Person("camile", 26); // 这里没有使用new操作符
console.log(p3.name, p3.age); // camile 26
通过上述模式,意味着锁定了可以调用构造函数的环境。如果使用构造函数窃取模式继承且不使用原型链,会破坏整个继承。
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, 4);
this.width = width;
this.height = height;
this.getArea = function() {
return this.width * this.height;
}
}
var rect = new Rectangle(2, 4);
console.log(rect.getArea()); // 8
console.log(rect.sides); // undefined
Polygon.call(this, 4)用于Polygon构造函数是作用域安全的,this并非Polygon的实例,所以会创建并返回一个新的Polygon对象,并没有实际作用,this得不到增长,所以Rectangle实例中不会有sides属性。
解决上述问题:使用原型模式或者寄生模式
方式一:原型模式
Rectangle.prototype = new Polygon();
var rect = new Rectangle(2, 4);
console.log(rect.getArea()); // 8
console.log(rect.sides); // 4
方式二:寄生模式
function Rectangle(width, height) {
var r = new Polygon(4);
r.width = width;
r.height = height;
r.getArea = function() {
return r.width * r.height;
}
return r;
}
var rect = new Rectangle(2, 4);
console.log(rect.getArea()); // 8
console.log(rect.sides); // 4
注意:
构造函数内部创建的var r = new Polygon(4)与外部创建没有什么不同。不能依赖instanceof来确定对象类型。
rect instanceof Rectangle; // false
rect instanceof Polygon; // true
由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式。
3. 惰性载入函数
可以将任和代码分支推迟到第一次调用函数的时候。
因浏览器之间行为差异,多数JavaScript代码包含大量的if语句。
例如,创建兼容性的XMLHttpRequest对象【Ajax与Comet 】
/* 兼容IE早期版本 */
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){ // 适用于IE7之前的版本
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 { // XHR对象和ActiveX对象都不存在,则抛出错误
throw new Error("No XHR object available.");
}
}
如果浏览器中支持内置XHR对象,每次if判断测试就显得多余了!!!
通过惰性载入的技术可以很好的解决上述问题。
方式一:在函数被调用时再处理函数
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();
}
方式二:在声明函数时就指定适当的函数
// 自执行,在createXHR声明时,就为其指定了相关创建方法。
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.");
};
}
})();
补充:
1. 惰性单例
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
};
};
// 测试
function testSingle(){}
getSingle(testSingle)() === getSingle(testSingle)(); // true
2. 系统中提供的页面刷新【回调函数不支持参数】
var refreshPage = (function () {
var fun;
function register(callback) {
fun = callback;
}
return function (callback) {
callback && typeof callback === ‘function‘ ? register(callback) : fun();
}
})();
// 改写
var refreshPage = (function() {
var fn;
return function(callback) {
callback && typeof callback === ‘function‘ ? fn = callback : fn();
}
})();
4. 函数绑定
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。
var handler = {
message: "Event handled",
handleClick: function(event) {
console.log(this.message);
}
};
document.getElementById("btn").addEventListener("click", handler.handleClick); // undefined
问题在于没有保存handleClick()的环境,this被绑定到了当前DOM按钮上。
// 闭包修正
document.getElementById("btn").addEventListener("click", function() {
handler.handleClick(event);
});
// 自定义bind方法
function bind(fn, context) {
return function() {
return fn.apply(context, arguments);
};
}
document.getElementById("btn").addEventListener("click", bind(handler.handleClick, handler));
// ES5新增bind方法 Function.prototype.bind
document.getElementById("btn").addEventListener("click", handler.handleClick.bind(handler));
5. 函数柯里化
用于创建已经设置好了一个或多个参数的函数。
其基本方法和函数绑定是一样的:使用一个闭包返回一个函数。
二者区别在于:当函数被调用时,返回的函数还需设置一些传入的参数。
function curry(fn){
// 获取参数,并转化为数组(第一个参数为柯里化的函数)
var args = Array.prototype.slice.call(arguments, 1);
return function(){
// 获取参数,并转化为数组
var innerArgs = Array.prototype.slice.call(arguments),
// 所有参数集(最终,有效参数受限于fn)
finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
};
}
function add(num1, num2){
return num1 + num2;
}
var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); //8
var curriedAdd2 = curry(add, 5, 12);
console.log(curriedAdd2()); //17
var curriedAdd3 = curry(add, 5, 12);
console.log(curriedAdd3(1)); //17
/* 增强版bind */
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);
};
}
注意:和上述“函数绑定”中的“自定义bind方法”区分
function enhanchBind(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);
};
}
document.getElementById("btn").addEventListener("click", enhanchBind(handler.handleClick, handler, event, "函数柯里化"));
二、防篡改对象
一旦把对象定义为防篡改,就无法撤销了。
preventExtensions –> seal –> freeze
isExtensible –> isSealed –> isFrozen
1. 不可扩展对象
默认情况下,所有对象都是可扩展的。意味着,任何时候都可以向对象中添加属性和方法。
var person = {name: "lg"};
person.age = 26;
Object.preventExtensions(person);
person.address = "sd";
console.log(person); // Object {name: "lg", age: 26} address属性没有成功添加
console.log(Object.isExtensible(person)); // false,不可扩展
2. 密封的对象
密封对象不可扩展,而且已有成员的[[configurable]]特性将被设置为false。意味着,不能删除属性和方法。
var person = {name: "lg"};
Object.seal(person);
person.age = 26;
console.log(person); // Object {name: "lg"} age属性没有成功添加
console.log(Object.isExtensible(person)); // false,不可扩展
console.log(Object.isSealed(person)); // true,密封的
3. 冻结的对象
冻结对象不可扩展、又是密封的,而且已有成员的[[Writable]]特性将被设置为false。如果定义了[[Set]]函数,访问器属性仍可写。
var person = {
name: "lg",
_address: "sd" // 表示私有
};
Object.defineProperty(person, "address", { // 访问器属性
get: function() {
return this._address;
},
set: function(add) {
this._address = add;
}
})
Object.freeze(person);
person.age = 26;
console.log(person); // Object {name: "lg", address: "sd"} age属性没有成功添加
person.name = "ligang";
person.address = "shandong"; // ??????
console.log(Object.isExtensible(person)); // false,不可扩展
console.log(Object.isSealed(person)); // true,密封的
console.log(Object.isFrozen(person)); // true,冻结的
对于JavaScript库的作者而言,冻结对象是很有用的,其很好的防止了意外修改库中核心对象。
4. 总结
三、高级定时器
JavaScript运行于单线程的环境中,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机不能保证。
定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立即执行,而是能表示它会尽快执行。设定一个150ms后执行的定时器不代表了150ms代码就立刻执行,它表示代码会在150ms后被加入到队列中。如果,在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面看上去好像就在精确指定的时间点上执行了。其他情况下,代码可能明显等待更长时间才执行。
谨记:定时器指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。
1. 重复的定时器
setInterval(),JavaScript引擎“仅当没有该定时器的任何代码实例时“,才将定时器代码添加到队列。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
其会存在两个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会比预期的小。
假如,某个onclick事件处理程序使用setInterval()设置了一该处理是否个200ms间隔的重复定时器。如果事件处理程序花了300ms多一点的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。
btn.addEventListener("click", function(){
// 300ms时间执行
setInterval(function(){
// 执行需要300ms
}, 200);
});
解释:第一个定时器在205ms处添加到队列中,但是直到过了300ms处才能够执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在5ms处添加的定时器代码结束后,405ms处添加的定时器代码就立即执行。
解决上述问题:链式调用setTimeout()
setTimeout(function(){
// 处理中
setTimeout(arguments.callee, interval)
}, interval);
好处:在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,可以保证在下一次定时器代码执行之前,至少等待指定的间隔,避免了连续的运行。
2. Yielding Processes
运行在浏览器中的JavaScript都被分配了一个确定数量的资源,当页面中存在一个耗时较大的脚本时,会导致浏览器卡死。
此时需要考虑两个问题:(1)该处理是否必须同步完成?(2)数据是否必须按顺序完成?
如果都是“否”,则需要考虑“数组分块”技术。
/**
* @array 要处理的项目的数组
* @process 处理项目的函数
* @context 运行函数的环境
*/
function chunk(array, process, context) {
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if(array.length > 0) {
setTimeout(arguments.callee, 100);
}
}, 100);
}
var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];
function printValue(item){
var div = document.getElementById("myDiv");
div.innerHTML += item + "<br>";
}
chunk(data, printValue); // 函数在全局中,所以无需传入执行环境
console.log(data); // []
注意:传递给chunk()的数组在处理数据时,数组中的条目也在改变。如果想保持原数组保持不变,应该将数组进行克隆。
var data2 = [1, 2, 3];
chunk(data2.concat(), printValue);
console.log(data2); // [1, 2, 3]
3. 节流处理
在浏览器中,处理DOM交互需要更多的内存和CUP时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,甚至崩溃。
函数节流背后的基本思想是指:某些代码不可以在没有间断的情况连续重复执行。
目的:只有在执行函数的请求停止了一段时间之后才执行。
/**
* @param method 方法
* @param scope 当前函数执行作用域
*/
function throttle(method, scope) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(scope);
}, 100);
}
function resizeDiv(){
var div = document.getElementById("myDiv");
div.style.height = div.offsetWidth + "px";
}
// 节流在resize事件中最常用
window.onresize = function(){
throttle(resizeDiv);
};
四、自定义事件
事件是一种叫做观察者的设计模式,是一种创建松散耦合代码的技术。
对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者订阅这些事件来观察该主体。
/* 管理事件的对象 */
function EventTarget(){
this.handlers = {};
}
EventTarget.prototype = {
constructor: EventTarget,
/**
* 添加事件
* @param type 事件类型
* @param handler 事件处理程序
*/
addHandler: function(type, handler){
if (typeof this.handlers[type] == "undefined"){
this.handlers[type] = [];
}
this.handlers[type].push(handler);
},
/**
* 触发事件
* @param event 事件对象
*/
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);
}
}
},
/**
* 移除事件
* @param type 事件类型
* @param handler 事件处理程序
*/
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);
}
}
};
实例1:
// 事件处理程序
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!"});
实例2:
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.");
五、拖放
点击某个对象,并按住鼠标按钮不放,将鼠标移动到另一个区域,然后释放鼠标按钮将对象“放”在这里。
拖放的基本概念:创建一个绝对定位的元素,使其可以用鼠标移动。
<!DOCTYPE html>
<html>
<head>
<title>Drag and Drop Example</title>
</head>
<body>
<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>
<script type="text/javascript">
var DragDrop = function(){
// EventTarget为上述实例中对象
// 继承EventTarget,具有事件功能
var dragdrop = new EventTarget(),
dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
//get event and target
var target = event.target;
//determine the type of event
switch(event.type){
case "mousedown":
if (target.className.indexOf("draggable") > -1){
dragging = target;
// 保存x、y坐标上的差值
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";
// 发布自定义事件
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;
}
};
//全局接口
// 可以拖放
dragdrop.enable = function(){
document.addEventListener("mousedown", handleEvent);
document.addEventListener("mousemove", handleEvent);
document.addEventListener("mouseup", handleEvent);
};
// 禁止拖放
dragdrop.disable = function(){
document.removeEventListener("mousedown", handleEvent);
document.removeEventListener("mousemove", handleEvent);
document.removeEventListener("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 + ")";
});
</script>
</body>
</html>