第 22 章 高级技巧

76 阅读6分钟

22.1 高级函数

函数是 JavaScript 中最有趣的部分之一。它们本质上是十分简单和过程化的,但也可以是非常复杂 和动态的。

22.1.1 安全的类型检测

JavaScript 内置的类型检测机制并非完全可靠。

typeof 操作符检测null时,返回Object;

instanceof 操作符在存在多个全局作用域(像一个页面包含多个 frame)的情况下,也是问题多多。

var isArray = value instanceof Array;

以上代码要返回 true,value 必须是一个数组,而且还必须与 Array 构造函数在同个全局作用域 中。

在任何值上调用 Object 原生的 toString()方法,都会返回一个[objectNativeConstructorName]格式的字符串。

alert(Object.prototype.toString.call(value)); //"[object Array]"

最终,我们推荐使用Object.prototype.toString.call()来检测类型。

22.1.2 作用域安全的构造函数

构造函数其实就是一个使用 new 操作符调用的函数。当使用 new 调用时,构造函数内用到的 this 对象会指向新创建的对象实例

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");

Person 构造函数使用 this 对象给三个属性赋值:name、age 和 job。当和 new操作符连用时,则会创建一个新的 Person 对象,同时会给它分配这些属性。

在当没有使用 new操作符来调用该构造函数的情况上。由于该 this 对象是在运行时绑定的,所以直接调用 Person(),this 会映射到全局对象 window 上,导致错误对象属性的意外增加。

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

22.1.3 惰性载入函数

因为浏览器之间行为的差异,多数 JavaScript 代码包含了大量的 if 语句,将执行引导到正确的代 码中。惰性载入表示函数执行的分支仅会发生一次。

常规createXHR()函数

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){
                //跳过
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}

函数被调用时再处理函数

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];
                    break;
                    } catch (ex){
                    //跳过
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        }
    } else {
        createXHR = function(){
            throw new Error("No XHR object available.");
        };
    }
}

在这个惰性载入的 createXHR()中,if 语句的每一个分支都会为 createXHR 变量赋值,有效覆 盖了原有的函数。最后一步便是调用新赋的函数。下一次调用 createXHR()的时候,就会直接调用被 分配的函数,这样就不用再次执行 if 语句了。

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

 var createXHR = (function(){
    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){
                //跳过
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
})()

这个例子中使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。

22.1.4 函数绑定

另一个日益流行的高级技巧叫做函数绑定。函数绑定要创建一个函数,可以在特定的 this 环境中 以指定参数调用另一个函数。也就是Function.prototype.bind,该方法用于修改函数调用是的this,并返回这个函数。

var handler = {
    message: "Event handled",
    handleClick: function(event){
        alert(this.message + ":" + event.type);
    }
};

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

22.1.5 函数柯里化

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。

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);
    };
}

22.2 防篡改对象

ECMAScript 5 致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)。

22.2.1 不可扩展对象

Object.preventExtensions()方法,让你不能再给对象添加属性和方法。仍然还可以修改和删除已有的成员。

var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29;
person.name = "abc";
alert(person.age); //undefined
alert(person.name); // "abc"

使用 Object.istExtensible()方法还可以确定对象是否可以扩展。

var person = { name: "Nicholas" };
alert(Object.isExtensible(person)); //true
Object.preventExtensions(person);
alert(Object.isExtensible(person)); //false

22.2.2 密封的对象

ECMAScript 5 为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而 且已有成员的[[Configurable]]特性将被设置为 false,这就意味着不能删除属性和方法。

要密封对象,可以使用 Object.seal()方法

var person = { name: "Nicholas" };
Object.seal(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"

使用 Object.isSealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用 Object.isExtensible()检测密封的对象也会返回 false。

var person = { name: "Nicholas" };
alert(Object.isExtensible(person)); //true
alert(Object.isSealed(person)); //false
Object.seal(person);
alert(Object.isExtensible(person)); //false
alert(Object.isSealed(person)); //true

22.2.3 冻结的对象

最严格的防篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,又是密封的,而且对象 数据属性的[[Writable]]特性会被设置为 false。如果定义[[Set]]函数,访问器属性仍然是可写的。

ECMAScript 5 定义的 Object.freeze()方法可以用来冻结对象。

var person = { name: "Nicholas" };
Object.freeze(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"

Object.isFrozen()方法用于检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以用 Object.isExtensible()和 Object.isSealed()检测冻结对象将分别返回 false和 true。

var person = { name: "Nicholas" };
alert(Object.isExtensible(person)); //true
alert(Object.isSealed(person)); //false
alert(Object.isFrozen(person)); //false
Object.freeze(person);
alert(Object.isExtensible(person)); //false
alert(Object.isSealed(person)); //true
alert(Object.isFrozen(person)); //true

按保护级别由低到高依次为:Object.preventExtensions 、Object.seal() 、Object.freeze()

22.3 高级定时器

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

22.3.1 重复的定时器

使用 setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个方式的问题在于, 定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次, 而之间没有任何停顿。,JavaScript 为了避免这个问题,当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。

22.3.2 Yielding Processes

运行在浏览器中的 JavaScript 都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制 他们要的内存大小和处理器时间,JavaScript 被严格限制了,以防止恶意的 Web 程序员把用户的计算机 搞挂了。

其中一个限制是长时间运行脚本的制约,如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。

22.3.3 函数节流

函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。

throttle()函数

function throttle(method, context) {
    clearTimeout(method.tId);
    method.tId= setTimeout(function(){
        method.call(context);
    }, 100);
}

22.4 自定义事件

事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。

观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来 观察该主体。