资讯专栏INFORMATION COLUMN

高程3总结#第22章高级技巧

CKJOKER / 3147人阅读

摘要:如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题惰性载入函数惰性载入表示函数执行的分支仅会发生一次。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。,用于注销某个事件类型的事件处理程序。

高级技巧 高级函数 安全的类型检测

typeof操作符在检测数据类型时,可能会得到不靠谱的结果

instanceof操作符在存在多个全局作用域,也就是页面包含多个iframe的情况下,也会出现问题

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

原生数组的构造函数名与全局作用域无关,因此使用toString()就能保证返回一致的值

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]";
}

作用域安全的构造函数

作用域安全的构造函数在进行任何更改之前,首先确认this对象是正确类型的实例,如果不是,那么会创建新的实例并返回

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

使用构造函数窃取模式的继承且不适用原型链,这个继承可能被破坏

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构造函数则不是。如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题

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

惰性载入函数

惰性载入表示函数执行的分支仅会发生一次。

第一种实现惰性载入的方法,在函数被调用时再处理函数。在第一次调用的过程中,该函数会覆盖为另一个按何时方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了

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

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

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.");
    };
  }
})();

函数绑定

一个简单的bind()函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去

function bind(fn, context){
  return function(){
    return fn.apply(context, arguments);
  };
}

当调用返回的函数时,它会在给定环境中执行被传入的函数并给出所有参数

var handler = {
  message: "Event handled",
  handleClick: function(event){
    alert(this.message);
  }
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler))

ECMAScript5为所有函数定义了一个原生的bind()方法,进一步简单了操作,不用再自己定义bind()函数了,而是可以直接在函数上调用这个方法

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

ECMAScript5的bind()方法也实现函数柯里化,只要在this的值之后再传入另一个参数即可

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

防篡改对象 不可扩展对象

默认情况下,所有对象都是可以扩展的,也就是说,任何时候都可以向对象中添加属性和方法

Object.preventExtensions()方法可以改变这个行为,不能再给对象添加属性和方法

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

Object.isExtensible()方法还可以确定对象是否可以扩展

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

密封的对象

封闭对象不可扩展,而且已有成员[Configurable]特性将被设置为false,这就意味着不能删除属性和方法,因为不能使用Object.defineProperty()把数据属性修改为访问器属性

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

冻结的对象

冻结的对象既不可扩展又是密封的,而且对象数据属性的[Writable]特性会被设置为false,如果定义[Set]函数,访问器属性仍然是可写的。ECMAScript5丁意思的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

高级定时器 重复的定时器

使用setInterval()创建的定时器确保了定时器代码规则地插入队列中

使用setInterval()时,仅当没有该定时器的任何其他代码实现时,才将定时器代码添加到队列中,这确保了定时器加入到队列中的最小时间间隔为指定间隔

重复定时器有两个问题:某些间隔会被跳过;多个定时器的代码执行之间的间隔可能会比预期小

为了避免setInterval()的重复定时器的两个问题,使用链式setTimeout()调用

setTimeout(function(){
  //处理中
  setTimeout(arguments.callee, interval);
}, interval);

这个模式主要用于重复定时器

setTimeout(function(){
  var div = document.getElementById("myDiv");
  left = parseInt(div.style.left) + 5;
  div.style.left = left + "px";
  if (left < 200){
    setTimeout(arguments.callee, 50);
  }
}, 50)

Yielding Processes

脚本长时间运行的问题通常是由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环

两个重要问题:该处理是否必须同步完成;数据是否必须按顺序完成

如果两个问题回答都是否,可以使用定时器分隔这个循环,即数组分块技术

小块小块地处理数组,通常每次一小块,基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器

setTimeout(function(){
  //取出下一个条目并处理
  var item = array.shift();
  process(item);
  //若还有条目,再设置另一个定时器
  if(array.length > 0){
    setTimeout(arguments.callee, 100);
  }
}, 100);

数组分块模式中,array变量本质上就是一个列表,包含了要处理的项目。使用shift()方法获取队列中要处理的项目,然后将其传递给某个函数。如果在队列中还有其他项目,则设置另一个定时器,并通过arguments.callee调用同一个匿名函数,要实现数组分块非常简单,可以使用下面的函数

function chunk(array, process, context){
  setTimeout(function(){
    var item = array.shift();
    process.call(context, item);
    if (array.length > 0){
      setTimeout(arguments.callee, 100);
    }
  }, 100);
}

chunk()方法接收三个参数:要处理的项目的数组,用于处理项目的函数,以及可选的运行该函数的环境

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 + "
"; } chunk(data, printValue);

函数节流

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

var processor = {
  timeoutId: null,
  //实际进行处理的方法
  performProcessing: function(){
    //实际执行的代码
  },
  //初始处理调用的方法
  process: function(){
    clearTimeout(this.timeoutId);
    var that = this;
    this.timeoutId = setTimeout(function(){
      that.performProcessing();
    }, 100);
  }
};
//尝试开始执行
processor.process();

自定义事件

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

自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听事件

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类型有一个多带带的属性handlers ,用于储存事件处理程序。

还有三个方法:

addHandler() ,用于注册给定类型事件的事件处理程序

addHandler() 方法接受两个参数:事件类型和用于处理该事件的函数。当调用该方法时,会进行一次检查,看看 handlers 属性中是否已经存在一个针对该事件类型的数组;如果没有,则创建一个新的。然后使用 push() 将该处理程序添加到数组的末尾。

fire() ,用于触发一个事件

如果要触发一个事件,要调用 fire() 函数。该方法接受一个多带带的参数,是一个至少包含 type属性的对象。 fire() 方法先给 event 对象设置一个 target 属性,如果它尚未被指定的话。然后它就查找对应该事件类型的一组处理程序,调用各个函数,并给出 event 对象。

removeHandler() ,用于注销某个事件类型的事件处理程序。

removeHandler() 方法是 addHandler() 的辅助,它们接受的参数一样:事件的类型和事件处理程序。这个方法搜索事件处理程序的数组找到要删除的处理程序的位置。如果找到了,则使用 break操作符退出 for 循环。然后使用 splice() 方法将该项目从数组中删除。

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!"});
拖放

基本实现过程

var DragDrop = function(){
  var dragging = null;
  function handleEvent(event){
    //获取事件和目标
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    //确定事件类型
    switch(event.type){
      case "mousedown":
        if (target.className.indexOf("draggable") > -1){
          dragging = target;
        }
        break;
      case "mousemove":
        if (dragging !== null){
          //指定位置
          dragging.style.left = event.clientX + "px";
          dragging.style.top = event.clientY + "px";
        }
        break;
      case "mouseup":
        dragging = null;
        break;
    }
  };
  //公共接口
  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);
    }
  }
}();

修缮拖动功能

为防止出现上图情况。计算元素左上角和指针位置之间的差值。这个差值应该在 mousedown 事件发生的时候确定,并且一直保持,直到 mouseup 事件发生。通过将 event的 clientX 和 clientY 属性与该元素的 offsetLeft 和 offsetTop 属性进行比较,就可以算出水平方向和垂直方向上需要多少空间

var DragDrop = function(){
  var dragging = null;
  diffX = 0;
  diffY = 0;
  function handleEvent(event){
    //获取事件和目标
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(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){
          //指定位置
          dragging.style.left = (event.clientX - diffX) + "px";
          dragging.style.top = (event.clientY - diffY) + "px";
        }
        break;
      case "mouseup":
        dragging = null;
        break;
    }
  };
  //公共接口
  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);
    }
  }
}()

添加自定义事件
var DragDrop = function(){
var dragdrop = new EventTarget(),
    dragging = null,
    diffX = 0,
    diffY = 0;
function handleEvent(event){
  //获取事件和对象
  event = EventUtil.getEvent(event);
  var target = EventUtil.getTarget(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){
        //指定位置
        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(){
  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;
}();

这段代码定义了三个事件: dragstart 、 drag 和 dragend 。它们都将被拖动的元素设置为了 target ,并给出了 x 和 y 属性来表示当前的位置。它们触发于 dragdrop 对象上,之后在返回对象前给对象增加 enable() 和 disable() 方法。这些模块模式中的细小更改令 DragDrop 对象支持了事件

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 += "
Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")"; }); DragDrop.addHandler("dragend", function(event){ var status = document.getElementById("status"); status.innerHTML += "
Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")"; });

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98189.html

相关文章

  • 高程3总结#3基本概念

    摘要:基本概念语法区分大小写,中的一切变量函数名和操作符都区分大小写。要将一个值转换成对应的值,可以调用类型包括整数和浮点数值,基本数值字面量格式是十进制整数,除了十进制外还有八进制十六进制。八进制第一位必须是,十六进制第一位必须是。 基本概念 语法 区分大小写,ECMAScript中的一切(变量、函数名和操作符)都区分大小写。函数名不能使用typeof,因为它是一个关键字,但typeOf...

    Rindia 评论0 收藏0
  • 高程3总结#4变量、作用域和内存问题

    摘要:当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。这样,一直延续到全局执行环境全局执行环境的变量对象始终都是作用域链中的最后一个对象。 变量、作用域和内存问题 基本类型和引用类型的值 基本类型值指的是简单的数据段,而引用类型值值那些可能由多个值构成的对象。 定义基本类型值的引用和引用类型值的方法是类似的,创建...

    xumenger 评论0 收藏0
  • 高程3总结#9客户端检测

    摘要:能力检测无法精确地检测特定的浏览器和版本。用户代理检测通过检测用户代理字符串来识别浏览器。用户代理检测需要特殊的技巧,特别是要注意会隐瞒其用户代理字符串的情况。 客户端检测 能力检测 能力检测的目的不是识别特定的浏览器,而是识别浏览器的能力,采用这种方式不必顾忌特定的浏览器,只要确定浏览器支持的特定的能力,就能给出解决方案,检测基本模式 if(object.propertyInQu...

    BigNerdCoding 评论0 收藏0
  • 高程3总结#8BOM

    摘要:对象的核心对象是,它表示浏览器的一个实例。而和则表示该容器中页面视图区的大小。在中,与返回相同的值,即视口大小而非浏览器窗口大小。第三个参数是一个逗号分隔的设置字符串,表示在新窗口中都显示哪些特性。这应该是用户打开窗口后的第一个页面 BOM window对象 BOM的核心对象是window,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,它既是通过JavaScript访...

    MASAILA 评论0 收藏0
  • 高程3总结#7函数表达式

    摘要:匿名函数可以用来模仿块级作用域来避免这个问题这里是块级作用域代码定义并立即调用了一个匿名函数,将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,而紧随其后的另一对圆括号会立即调用这个函数。 函数表达式 递归 递归函数是在一个函数通过名字调用自身的情况下构成的 function factrial(num){ if(num

    sevi_stuo 评论0 收藏0

发表评论

0条评论

CKJOKER

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<