高级技巧

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-06-23

一、高级函数

作用域安全的构造函数

function Person(name, age, job){
  if (this instanceof Person){ // new的时候 this是指向实例的
    this.name = name;
    this.age = age;
    this.job = job;
  } else { 
    // 针对直接Person() 直接return一个new的对象
    return new Person(name, age, job);
  }
}

如果使用上述写法,当你借用构造函数来实现继承的时候,会被破坏。
Person.call(this,'jack', 15, 'student');,此时的this的指向子类,所以需要使用组合继承来完成。

惰性载入函数

function createXHR(){
  if (typeof XMLHttpRequest != "undefined"){
    createXHR = function(){
      return new XMLHttpRequest();
    };
  } else if (typeof ActiveXObject != "undefined"){
    createXHR = function(){
      return new ActiveXObject("Microsoft.XMLHTTP");
    }
  } else {
    createXHR = function(){
      throw new Error("No XHR object available.");
    };
  }
  return createXHR();
}

第一次调用的时候就已经确定(重写)了之后所有的createXHR()函数应该的定义,下一次调用createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行if 语句了。

第二种惰性载入函数是在声明函数时就指定适当的函数。创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。

var createXHR = (function (){
  if (typeof XMLHttpRequest != "undefined"){
    return function(){
      return new XMLHttpRequest();
    };
  } else if (typeof ActiveXObject != "undefined"){
    return function(){
      return new ActiveXObject("Microsoft.XMLHTTP");
    }
  } else {
    return function(){
      throw new Error("No XHR object available.");
    };
  }
})()

函数绑定

函数绑定要创建一个函数,可以在特定的this 环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。

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

function bind(fn, context){
  var args = Array.prototype.slice.call(arguments, 2);
  return function(){
    var innerArgs = Array.prototype.slice.call(arguments);
    var finalArgs = args.concat(innerArgs);
    return fn.apply(context, finalArgs);
  ;
}

函数柯里化

函数柯里化(function currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

具体请查看:函数柯里化

二、防篡改对象

不可扩展对象

使用Object.preventExtensions()方法可以让你不能再给对象添加属性和方法。

var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29; // 在严格模式会报错
alert(person.age); //undefined

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

密封的对象

Object.seal()方法用来密封对象,密封对象不可扩展,而且已有成员的[[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"
person.name = "jack"
alert(person.name); // "jack"

alert(Object.isExtensible(person)); //false 不可扩展
alert(Object.isSealed(person)); //true 密封对象

使用Object.isSealed()方法可以确定对象是否被密封了,被密封的对象不可扩展,因此使用Object.istExtensible()方法返回false

冻结的对象

最严格的防篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为false。但是如果定义[[Set]]函数,访问器属性仍然是可写的。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" 没有setter 不能修改

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

三、高级定时器

除了主JavaScript执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加入队列。例如,当某个按钮被按下时,它的事件处理程序代码就会被添加到队列中,并在下一个可能的时间里执行。当接收到某个Ajax 响应时,回调函数的代码会被添加到队列。JavaScript中没有任何代码是立刻执行的,但一旦进程空闲则尽快执行。

设定一个150ms 后执行的定时器不代表到了150ms 代码就立刻执行,它表示代码会在150ms 后被加入到队列中。如果在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,如果有,代码可能明显地等待更长时间(等待事件循环中的同步代码执行完之后)才执行。

重复定时器

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

示例截图.png

可以使用链式调用setTimeout()来避免,这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。

var interval = 1000;

setTimeout(function(){
  // do something
  console.log(1);

  //处理中 arguments.callee 即该匿名函数的引用
  setTimeout(arguments.callee, interval);
}, interval);

Yielding Processes

运行在浏览器中的JavaScript 都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制他们要的内存大小和处理器时间,JavaScript被严格限制了,以防止恶意的Web 程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的制约,如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是允许其继续执行还是停止它。

当你发现某个循环占用了大量时间,同时对于是否必须同步,是否必须按顺序完成,你的回答都是“否”,那么你就可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术,小块小块地处理数组,通常每次一小块。

基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。

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

定时器的时间间隔设置为了100ms,使得JavaScript进程有时间在处理项目的事件之间转入空闲。你可以根据你的需要更改这个间隔大小,不过100ms在大多数情况下效果不错。

函数防抖

防抖:在任务高频率触发时,只有触发间隔超过制定间隔的任务才会执行。即一个动作连续触发则只执行最后一次。防抖的原理则是不管你在一段时间内如何不停的触发事件,只要设置了防抖,则只在触发n秒后才执行。如果我们在一个事件触发的n秒内又触发了相同的事件,那我们便以新的事件时间为标准,n秒之后再执行。

function debounce(method,delay) {
  let timeId = null;
  return function(){
    let context = this;
    clearTimeout(timeId);
    timeId= setTimeout(function(){
      method.call(context);
    },delay);
  }   
}
// 触发了多次 都只会在最后一次调用 设置的间隔之后执行一次

函数节流

节流:在制定间隔内任务只执行1次。节流的原理是不管你在一段时间内如何不停地触发事件,只要设置了节流,就会每隔一段时间执行一次。


function throttle(method,delay) {
  var timeId = null;
  return function(){
    let context = this;
    if (!timeId) {   // 定时器不存在时才执行
      timeId = setTimeout(() => {
        method.call(context);
        clearTimeout(timeId); // 本次的定时器跑完之后 才清楚定时器让其为null
      }, delay);
    }
  }   
}

function throttle2(method,delay) {
  let initTime = Date.now()
  return function(){
    let context = this;
    let doTime = Date.now();
    if(doTime - initTime >= delay){
      method.call(context);
      initTime = Date.now()
    }
  }   
}

四、观察者模式

观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM 上时,DOM元素便是主体,你的事件处理代码便是观察者。

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(e){ // 触发 e { type: string ; message: string } 调用者应有此类型限制
    if (!e.target){
      e.target = this;
    }
    if (this.handlers[e.type] instanceof Array){
      var handlers = this.handlers[e.type];
      for (var i=0, len=handlers.length; i < len; i++){
        handlers[i](e);
      }
    }
  },

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


function handleMessage(event){ // 此event对象上 应还有type、target属性
  alert("Message received: " + event.message);
}
//创建一个新对象
var target = new EventTarget();
//添加一个事件处理程序
target.addHandler("message", handleMessage);
//触发事件
target.fire({ type: "message", message: "Hello world!"});
//删除事件处理程序
target.removeHandler("message", handleMessage);

五、自定义拖动事件

var DragDrop = function(){ // 自执行函数
  var dragdrop = new EventTarget(), // 观察者对象实例
      dragging = null;
      diffX = 0;
      diffY = 0;

  function handleEvent(event){
    //获取事件和目标
    var target = event.target;
    //确定事件类型
    switch(event.type){
      case "mousedown": // 鼠标按下时 获取鼠标处于目标的哪个位置 记录位置
        if (target.className.indexOf("draggable") > -1){
          dragging = target;
          diffX = event.clientX - target.offsetLeft;
          diffY = event.clientY - target.offsetTop;
          // 鼠标移动时触发 注册的dragStart事件
          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";
          // 鼠标移动时 触发自定义的drag事件
          dragdrop.fire({
            type: "drag",
            target: dragging,
            x: event.clientX, 
            y: event.clientY,
          });
        }
        break;

      case "mouseup": // 鼠标放开 注销事件
        // 鼠标放开 触发自定义的dragend事件
        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.addHandler("dragstart", function(event){
  console.log("Started dragging ")
});

DragDrop.addHandler("drag", function(event){
  console.log( "Draging ")
});

DragDrop.addHandler("dragend", function(event){
  console.log(" dragend")
});

// 此时的DragDrop拥有自定义拖动相关事件了 且能够可以被拖动的元素跟随鼠标移动
DragDrop.enable(); // 启用拖动
DragDrop.disable(); // 禁用拖动