高级函数

安全类型的检测

JavaScript 内置的类型检测机制并非完全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说 typeof 操作符吧,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。

  1. var isArray = value instanceof Array;

这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同一个全局作用域中,如果value是另一个全局作用域(其他frame)中定义的数组,那这个表达式返回false。

检测某个对象是原生的还是开发人员自定义的对象时也会有问题。因为浏览器开始原生支持JSON了,而有些开发人员还是在用第三方库来实现JSON,这个库里会有全局的JSON对象,这样想确定JSON对象是不是原生的就麻烦了。
解决这些问题的办法就是使用Object的toString方法,这个方法会返回一个[object NativeConstructorName]格式的字符串。

  1. function isArray(value){
  2. return Object.prototype.toString.call(value) == "[object Array]";
  3. }
  4. function isFunction(value){
  5. return Object.prototype.toString.call(value) == "[object Function]";
  6. }
  7. function isRegExp(value){
  8. return Object.prototype.toString.call(value) == "[object RegExp]";
  9. }

不过要注意的是,对于在IE中任何以COM形式实现的函数,isFunction()都会返回false。
对于JSON是否为原生的问题可以这样:

  1. var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";

作用域安全的构造函数

第六章的时候我们将了构造函数, 我们来回顾一下一个例子:

  1. function Person(name, age, job){
  2. this.name = name;
  3. this.age = age;
  4. this.job = job;
  5. }
  6. var person = new Person("Nicholas", 29, "Software Engineer");

如果不使用new运算符, 那么name, age, job三个属性会被直接挂在到window对象上, 为了防止普通调用的过程中出现这种疏忽, 我们有必要做一道保险:

  1. function Person(name, age, job){
  2. if (this instanceof Person){
  3. this.name = name;
  4. this.age = age;
  5. this.job = job;
  6. } else {
  7. return new Person(name, age, job); //保险
  8. }
  9. }
  10. var person1 = Person("Nicholas", 29, "Software Engineer");
  11. alert(window.name); //""
  12. alert(person1.name); //"Nicholas"
  13. var person2 = new Person("Shelby", 34, "Ergonomist");
  14. alert(person2.name); //"Shelby"

加了这个判断之后,看起来更叫稳妥.

不过又产生了新的问题, 假如Person函数调用call/apply实现继承的话, 那么结果可能不是我们想要的:

  1. function Polygon(sides){
  2. if (this instanceof Polygon) {
  3. this.sides = sides;
  4. this.getArea = function(){
  5. return 0;
  6. };
  7. } else {
  8. return new Polygon(sides);
  9. }
  10. }
  11. function Rectangle(width, height){
  12. Polygon.call(this, 2); //这里的this传的是Rectangle的实例
  13. this.width = width;
  14. this.height = height;
  15. this.getArea = function(){
  16. return this.width * this.height;
  17. };
  18. }
  19. var rect = new Rectangle(5, 10);
  20. alert(rect.sides); //undefined

解决方式:

  1. Rectangle.prototype = new Polygon(); //原型链继承, 这样this就是Polygon的实例了
  2. var rect = new Rectangle(5, 10);
  3. alert(rect.sides); //2

惰性载入函数

由于浏览器差异,大量的判断浏览器能力的函数需要被使用(通常是大量的if),然而这些判断一般其实不必每次都执行,在执行一次后,浏览器的能力就确定了,以后就应该不用在判断了。比如:

  1. function createXHR(){
  2. if (typeof XMLHttpRequest != "undefined"){
  3. return new XMLHttpRequest();
  4. } else if (typeof ActiveXObject != "undefined"){
  5. if (typeof arguments.callee.activeXString != "string"){
  6. var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
  7. "MSXML2.XMLHttp"],
  8. i,len;
  9. for (i=0,len=versions.length; i < len; i++){
  10. try {
  11. new ActiveXObject(versions[i]);
  12. arguments.callee.activeXString = versions[i];
  13. break;
  14. } catch (ex){
  15. }
  16. }
  17. }
  18. return new ActiveXObject(arguments.callee.activeXString);
  19. } else {
  20. throw new Error("No XHR object available.");
  21. }
  22. }

这里的创建XHR对象的函数,每次创建对象时都会判断一次浏览器能力,这是不必要的。

惰性载入有两种方式. 第一种就是在函数第一次被调用时,根据不同情况,用不同的新函数把这个函数覆盖掉,以后调用就不需要再判断而是直接执行该执行的操作。

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){
                        //skip
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    } else {
        createXHR = function(){
            throw new Error("No XHR object available.");
        };
    }
    return createXHR();
}
createXHR();//第一次调用的时候会执行if语句
createXHR();//第二次就不会执行if语句了

第二种方法就是在声明函数时候就指定适当的函数, 实际上原理和上面的类似:

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.");
        };
    }
})();    //这里是一个立即执行函数, 执行完毕后createXHR就可以直接调用, 无需再检测

函数绑定

函数绑定是为了解决this的指向问题:

var handler = {
      message: "Event handled",
      handleClick: function(event){
        console.log(this);
        console.log(this.message);
      }
    };
    var btn = document.getElementById("myButton");

    btn.addEventListener('click', handler.handleClick, false);    //这里会输出 dom 和 undefined, 表面上handler.handleClick是挂载在handler上, 但是它里面的this指向会发生改变

// 为了解决上面的问题, 我们有如下两个方法:

// 方法1: 新增匿名函数
    btn.addEventListener('click', function(evt){
      handler.handleClick(evt)    // 通过新增一个匿名函数可以实期待的输出
    }, false); 
// 方法2: 使用Es5 bind方法
btn.addEventListener('click', handler.handleClick.bind(handler), false);

// 如果浏览器不支持bind方法, 我们可以利用apply实现一个
if(!Function.prototype.bind){
  Function.prototype.bind = function(fn,context){
    return function(){
      fn.apply(context,arguments)
    }
  }
}

函数curry化

函数curry化, 中文翻译柯里化, 个人觉得在大多数情况下不是很有必要.书上讲得也不好, 请直接观看 这篇文章讲解什么是curry化

防篡改对象

不可扩展对象

JS共享的本质使任意对象都可被随意修改。这样有时很不方便。ES5增加了几个方法来设置对象的行为。一旦将对象设置为防篡改就不能撤销了。

var person = { name: "Nicholas" };
Object.preventExtensions(person);        //ES5新增的Object.preventExtensions方法

person.age = 29;
alert(person.age); //undefined
alert(Object.isExtensible(person)); //false
person.name = "hahah";    //可以对现有属性进行修改
alert(person.name); //hahah

密封的对象

密封对象比不可扩展对象更加严格, 它不可以添加或删除属性,已有成员的[[Configurable]]特性被设置为false。

var person = { name: "Nicholas" };
Object.seal(person);

person.age = 29; 
alert(person.age); //undefined

delete person.name;     //不能删除
alert(person.name); //"Nicholas"

alert(Object.isExtensible(person)); //false ,不能扩展
alert(Object.isSealed(person));     //true

冻结的对象

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"

alert(Object.isExtensible(person));//false
alert(Object.isSealed(person));//true
alert(Object.isFrozen(person));//true

高级定时器

setTimeout()和setInterval()是很实用的功能,不过有些事情是要注意的。
JS是单线程的,这就意味着定时器实际上是很有可能被阻塞的。我们在这两个函数中所设置的定时,其实是代表将代码加入到执行队列的事件,如果在加入时恰巧JS是空闲的,那么这段代码会立即被执行,也就是说这个定时被准时的执行了。相反,如果这时JS并不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时执行。

记住: 在JS中, 没有任何代码是立即执行的, 只有一旦进程空闲就执行.

重复的定时器

使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样一种可能,在上次代码还没执行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有没有代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔因为JS的处理被跳过,代码之间的间隔比预期的小。
所以尽量使用setTimeout()模拟间隔调用。

setTimeout(function(){ 
    setTimeout(arguments.callee, interval);
}, interval);

yielding processes

浏览器中的js被分配了一个确定数量的资源,所以会限制js脚本的运行时间,不能过长。

如果达到这个限制,会弹出一个浏览器错误的对话框,询问是否继续执行。定时器时绕开此限制的方法之一。

脚本长时间运行的原因有两个:

  • 过长的、过深嵌套的函数调用
  • 进行大量处理的循环

通常我们是处理第二个因素, 但是要记住, 如果你的循环不必同步,或者结果不必按顺序, 那么么就可以采用yielding processes思想.

我们看这例子:

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

函数节流

举个例子 , 页面有一个长度为3的轮播图, 你鼠标放到(hover)对应轮播点的时候自动显示该张图, 如果你在非常短时间(比如10ms)内快速来回hover, 那图片自然也会也会快速闪烁, 这样会操作性能的浪费. 我们就可以利用setTimeout来限制用户的hover频率

自定义事件

事件是一种叫做观察者模的设计模式(也叫发布订阅模式), 这是一种创建松散耦合的代码技术.
观察者模式有两类对象组成: 主体和观察者, 主体发布时间, 同时观察者通过订阅这些事件来观察主体. 涉及到DOM上, DOM元素就是主体, 你的事件处理程序就是观察者.

我们来实现一个简单的观察者模式:

var Pubsub = function  (argument) {
      this.hub = {};
    }
    Pubsub.prototype.on = function(type,fn){
      if (!this.hub[type]) {
        this.hub[type] = [];
      }
      this.hub[type].push(fn);
    };
    Pubsub.prototype.off = function(type){
      this.hub[type] = [];
    };
    Pubsub.prototype.fire = function(type,fn){
      var fns  = this.hub[type];  //有可能存了多个事件
      if (!fns.length) {
        console.log('无'+type+'订阅')
      }
      for (var i = 0; i < fns.length; i++) {
        fns[i]();
      }
    };

    var user = new Pubsub;
    function read(){
      console.log("I'm reading");
    }
    function read2(){
      console.log("I'm recording");
    }
    user.on('update',read);
    user.on('update',read2);

    user.fire('update');

    user.off('update');
    user.fire('update');

拖放

不太清楚为何拖放这节内容会放在高级技巧中, 这里不再讲解.

JS实现拖放的思路就是对一个DOM元素设置绝对定位, 然后根据鼠标的位置, 配合mouseDown/mouseUp/mouseMove事件来动态设置DOM元素的top/left值. 代码略


本章完