JS高級技巧學習

高級技巧

因爲全部的函數都是對象,因此使用函數的指針很是簡單javascript

安全類型檢測

  1. typeof 操做符有一些沒法預知的行爲,檢測數據類型時有時候會獲得不靠譜的結果
  2. instanceof 操做符在存在多個全局做用域的狀況下,也會有問題

場景:假設一個頁面有多個 iframejava

array 是 window 的屬性數組

/* value是一個數組的狀況下,且必須與Array在同一個全局做用域下才會返回true 若是value是在另外一個ifream下定義的數組,那麼返回false */

var isArray = value instanceof Array;
複製代碼
  1. 檢測原生 JSON 對象

任何值上面調用 object 的 toString()方法都會返回一個[object NativeConstructorName]格式的字符串,每一個類在內部都有一個[[class]]屬性,指定了上述字符串中構造函數名瀏覽器

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

//原生數組的構造函數與全局做用域無關,可使用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]";
}
複製代碼

這一技巧也被被普遍的應用於檢測原生 JSON 對象,Object 的 toString 方法不能檢測非原生構造函數的構造函數名,所以開發人員定義的任何的構造函數都將返回 [obje0ct object]安全

var isNativeJSON=window.JSON && Object.prototype.toString.call(JSON)==「[Onject JSON]」
複製代碼

做用域安全的構造函數

當沒有使用 new 操做符來建立的時候,this 對象是在運行時候綁定的,直接調用的話,this 會映射到 window 全局對象上構造函數當作普通函數去調用,這個問題是 this 晚綁定產生的,這裏的 this 解析成了 window 對象bash

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}
var p = Person("cc", 18, "coder");
console.log(window.name);
console.log(window.age);
console.log(window.job);
複製代碼

如下方式不管 Person 是否使用 new 操做符調用,都會返回一個新的實例對象,這就避免了在全局對象上意外的設置屬性閉包

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);
  }
}
複製代碼

使用這個模式能夠鎖定構造函數的做用域。若是使用了構造函數竊取模式的繼承且不使用原型鏈,那麼這個繼承有可能會被破壞app

//多邊形類 ,此構造函數的做用域是安全的
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並不是Polygon的實例
  this.width = width;
  this.height = height;
  this.getArea = function() {
    return this.width * this.height;
  };
}

var rect = new Rectangle(5, 10);
console.log(rect.sides); //undefined
console.log(rect.getArea()); //50
console.log(rect); //Rectangle {width: 5, height: 10, getArea: ƒ}
複製代碼

解決辦法是 結合使用原型鏈或者寄生組合

//多邊形類
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);
  //原型鏈,rect是Rectangle實例,也是Polygon的實例,call執行,sides被添加
  this.width = width;
  this.height = height;
  this.getArea = function() {
    return this.width * this.height;
  };
}
Rectangle.prototype = new Polygon();

var rect = new Rectangle(5, 10);
console.log(rect.sides); //undefined
console.log(rect.getArea()); //50
console.log(rect);
複製代碼

惰性載入函數

產生背景:大多數瀏覽器之間的行爲差別,致使多數 js 代碼包含了大量的 if 語句,將執行引導到正確的代碼中,因此若是 if 沒必要每次執行,那麼代碼能夠運行的更快一些。ide

function createXHR() {
  if (typeof XMLHttpRequest != "undefined") {
    //...
    return new XMLHttpRequest();
  } else if (typeof ActiveXObject != "undefined") {
    //...
    return new ActiveXObject(arguments.callee.activeXString);
  } else {
    throw new Error("error message");
  }
}
複製代碼

解決方案就稱爲惰性載入的技巧: 表示函數執行的分支只會發生一次函數

這兩種方式都能避免執行沒必要要的代碼,惰性載入函數的優勢只執行一次 if 分支,避免了函數每次執行時候都要執行 if 分支和沒必要要的代碼,所以提高了代碼性能,至於那種方式更合適,就要看您的需求而定了。

  1. 在函數被調用時再處理函數
function createXHR() {
  if (typeof XMLHttpRequest != "undefined") {
    createXHR = function() {
      //...
      return new XMLHttpRequest();
    };
  } else if (typeof ActiveXObject != "undefined") {
    createXHR = function() {
      //...
      return new ActiveXObject(arguments.callee.activeXString);
    };
  } else {
    createXHR = function() {
      throw new Error("error message");
    };
  }
  return createXHR();
}
複製代碼
  1. 在聲明函數時就指定適當的函數 這樣第一次調用函數時就不會損失性能,而在代碼首次加載的時候會損失一點性能,具體使用可根據本身的具體需求而定
var createXHR = (function() {
  if (typeof XMLHttpRequest != "undefined") {
    return function() {
      //...
      return new XMLHttpRequest();
    };
  } else if (typeof ActiveXObject != "undefined") {
    return function() {
      //...
      return new ActiveXObject(arguments.callee.activeXString);
    };
  } else {
    throw new Error("error message");
  }
})();
複製代碼

函數綁定

該技巧經常和回調函數和事件處理程序一塊兒使用,以便將函數做爲變量傳遞的同時保留代碼執行環境

//事件兼容封裝
var EventUtil = {
  addHandler: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  },
  //獲取事件對象
  getEvent: function(event) {
    return event ? event : window.event;
  },
  //獲取目標元素
  getTarget: function(event) {
    return event.target || event.srcElement;
  },
  //阻止事件默認行爲
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  },
  //解除監聽
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  },
  //阻止冒泡
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation;
    } else {
      event.cancelBubble = true;
    }
  }
};
複製代碼

如下時間處理程序,在點擊 button 後會彈出 undefined,這個問題在於沒有保存 handler.handleClcik()的環境,因此 this 指向了 DOM 按鈕而非 handler,咱們能夠採用閉包來修正這個問題

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

var btn = document.getElementById("btn");
EventUtil.addHandler(btn, "click", handler.handleClick);

//閉包修正
EventUtil.addHandler(btn, "click", function(event) {
  handler.handleClick();
});
複製代碼

可是大多數使用可能致使代碼的難以調試和理解。因此下面咱們使用 bind 解決,大多數的 js 庫實現了一個能夠將函數綁定到指定環境的函數,通常叫作 bind()

// 自定義bind函數接受一個函數和一個環境
function bind(fn, context) {
  return function() {
    return fn.apply(context, arguments);
  };
}

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

var btn = document.getElementById("btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));
複製代碼

ES5 爲全部函數提供了一個原生 bind 方法進一步簡化了操做,可是被綁定函數比普通函數相比有更多的開銷,須要更多的內存

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

var btn = document.getElementById("btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
複製代碼

函數柯里化

用於建立已設置好了一個或多個參數的函數,函數柯里化的基本方法和函數綁定是同樣的:使用一個閉包返回一個函數

如下 curriedAdd 函數雖然並非柯里化的函數,可是很好的展示了其概念

function add(x, y) {
  return x + y;
}
function curriedAdd(y) {
  return add(5, y);
}
console.log(add(1, 2)); //3
console.log(curriedAdd(5)); //10
複製代碼

柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。

咱們改造下,實際上就是把 add 函數的 x,y 兩個參數變成了先用一個函數接收 x 而後返回一個函數去處理 y 參數。如今思路應該就比較清晰了,就是只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

// 普通的add函數
function add(x, y) {
  return x + y;
}

// Currying後
function curriedAdd(x) {
  return function(y) {
    return x + y;
  };
}

add(1, 2); // 3
curriedAdd(1)(2); // 3
複製代碼

柯里化函數一般的動態建立方式

/* arguments是一個關鍵字,表明當前參數,在javascript中雖然arguments表面上以數組形式來表示,但實際上沒有原生數組slice的功能,這裏使用call方法算是對arguments對象不完整數組功能的修正。 slice返回一個數組,該方法只有一個參數的狀況下表示除去數組內的第一個元素。就本上下文而言,原數組的第一個參數是「事件名稱」,具體像「click」,"render"般的字符串,其後的元素纔是處理函數所接納的參數列表。 */
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);
  };
}
function add(x, y) {
  return x + y;
}
var curryAdd = curry(add, 5);
console.log(curryAdd(1)); //6

var curryAdd2 = curry(add, 1, 5); //兩個參數都提供了,就無需在傳遞了
console.log(curryAdd2()); //6

// ------支持多參數傳遞---------
function progressCurrying(fn, args) {
  var _this = this;
  var len = fn.length;
  var args = args || [];

  return function() {
    var _args = Array.prototype.slice.call(arguments);
    Array.prototype.push.apply(args, _args);

    // 若是參數個數小於最初的fn.length,則遞歸調用,繼續收集參數
    if (_args.length < len) {
      return progressCurrying.call(_this, fn, _args);
    }

    // 參數收集完畢,則執行fn
    return fn.apply(this, _args);
  };
}
複製代碼

用於函數綁定的一部分構造更復雜的 bind 函數

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);
  };
}
複製代碼

柯里化好處:

  1. 參數複用
//將第一個參數reg進行復用,這樣別的地方就可以直接調用hasNumber,hasLetter等函數,讓參數可以複用,調用起來也更方便
// 正常正則驗證字符串 reg.test(txt)

// 函數封裝後
function check(reg, txt) {
  return reg.test(txt);
}

check(/\d+/g, "test"); //false
check(/[a-z]+/g, "test"); //true

// Currying後
function curryingCheck(reg) {
  return function(txt) {
    return reg.test(txt);
  };
}

var hasNumber = curryingCheck(/\d+/g);
var hasLetter = curryingCheck(/[a-z]+/g);

hasNumber("test1"); // true
hasNumber("testtest"); // false
hasLetter("21212"); // false
複製代碼

缺點:

存取arguments對象一般要比存取命名參數要慢一點
一些老版本的瀏覽器在arguments.length的實現上是至關慢的
使用fn.apply( … ) 和 fn.call( … )一般比直接調用fn( … ) 稍微慢點
建立大量嵌套做用域和閉包函數會帶來花銷,不管是在內存仍是速度上
複製代碼

防篡改對象

開發人員可能意外修改別人的代碼,因此 ES5 提供了防篡改對象定義, 一旦把對象定義爲防篡改。就沒法撤銷了

var person = { name: "cc" };

object.preventExtensions(person);
person.name = "bb";
console.log(person.name);
複製代碼

未完持續中......

相關文章
相關標籤/搜索