码迷,mamicode.com
首页 > 编程语言 > 详细

JavaScript高级技巧

时间:2016-05-06 13:04:47      阅读:298      评论:0      收藏:0      [点我收藏+]

标签:

下述内容主要讲述了《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);
};

四、自定义事件

事件是一种叫做观察者的设计模式,是一种创建松散耦合代码的技术。
对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者订阅这些事件来观察该主体。
JavaScript设计模式–观察者模式

/* 管理事件的对象 */
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>

技术分享

JavaScript高级技巧

标签:

原文地址:http://blog.csdn.net/ligang2585116/article/details/51326365

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!