高级技巧
由于所有的函数都是对象,所以使用函数的指针非常简单
安全类型检测
- typeof 操作符有一些无法预知的行为,检测数据类型时有时候会得到不靠谱的结果
- instanceof 操作符在存在多个全局作用域的情况下,也会有问题
场景:假设一个页面有多个 iframe
array 是 window 的属性
/*
value是一个数组的情况下,且必须与Array在同一个全局作用域下才会返回true
如果value是在另一个ifream下定义的数组,那么返回false
*/
var isArray = value instanceof Array;
- 检测原生 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 分支和不必要的代码,因此提升了代码性能,至于那种方式更合适,就要看您的需求而定了。
- 在函数被调用时再处理函数
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();
}
- 在声明函数时就指定适当的函数 这样第一次调用函数时就不会损失性能,而在代码首次加载的时候会损失一点性能,具体使用可根据自己的具体需求而定
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);
};
}
柯里化好处:
- 参数复用
//将第一个参数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);
未完持续中......