JS高级技巧学习

519 阅读8分钟

高级技巧

由于所有的函数都是对象,所以使用函数的指针非常简单

安全类型检测

  1. typeof 操作符有一些无法预知的行为,检测数据类型时有时候会得到不靠谱的结果
  2. instanceof 操作符在存在多个全局作用域的情况下,也会有问题

场景:假设一个页面有多个 iframe

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 对象

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

使用这个模式可以锁定构造函数的作用域。如果使用了构造函数窃取模式的继承且不使用原型链,那么这个继承有可能会被破坏

//多边形类 ,此构造函数的作用域是安全的
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 不必每次执行,那么代码可以运行的更快一些。

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

未完持续中......