JS红皮书读书笔记-22-高级技巧

提醒:本文发布于 1472 天前,文章内容可能 因技术时效性过期 或 被重新修改,请谨慎参考。

TOC
  1. 1. 高级函数
    1. 1.1. 安全类型的检测
    2. 1.2. 作用域安全的构造函数
    3. 1.3. 惰性载入函数
    4. 1.4. 函数绑定
    5. 1.5. 函数curry化
  2. 2. 防篡改对象
    1. 2.1. 不可扩展对象
    2. 2.2. 密封的对象
    3. 2.3. 冻结的对象
  3. 3. 高级定时器
    1. 3.1. 重复的定时器
    2. 3.2. yielding processes
    3. 3.3. 函数节流
  4. 4. 自定义事件
  5. 5. 拖放

高级函数

安全类型的检测

JavaScript 内置的类型检测机制并非完全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说 typeof 操作符吧,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。

var isArray = value instanceof Array;

这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同一个全局作用域中,如果value是另一个全局作用域(其他frame)中定义的数组,那这个表达式返回false。

检测某个对象是原生的还是开发人员自定义的对象时也会有问题。因为浏览器开始原生支持JSON了,而有些开发人员还是在用第三方库来实现JSON,这个库里会有全局的JSON对象,这样想确定JSON对象是不是原生的就麻烦了。
解决这些问题的办法就是使用Object的toString方法,这个方法会返回一个[object NativeConstructorName]格式的字符串。

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

不过要注意的是,对于在IE中任何以COM形式实现的函数,isFunction()都会返回false。
对于JSON是否为原生的问题可以这样:

var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";

作用域安全的构造函数

第六章的时候我们将了构造函数, 我们来回顾一下一个例子:

function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");

如果不使用new运算符, 那么name, age, job三个属性会被直接挂在到window对象上, 为了防止普通调用的过程中出现这种疏忽, 我们有必要做一道保险:

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"

加了这个判断之后,看起来更叫稳妥.

不过又产生了新的问题, 假如Person函数调用call/apply实现继承的话, 那么结果可能不是我们想要的:

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传的是Rectangle的实例
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
var rect = new Rectangle(5, 10);
alert(rect.sides); //undefined

解决方式:

Rectangle.prototype = new Polygon();	//原型链继承, 这样this就是Polygon的实例了
var rect = new Rectangle(5, 10);
alert(rect.sides); //2

惰性载入函数

由于浏览器差异,大量的判断浏览器能力的函数需要被使用(通常是大量的if),然而这些判断一般其实不必每次都执行,在执行一次后,浏览器的能力就确定了,以后就应该不用在判断了。比如:

function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
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){
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}

这里的创建XHR对象的函数,每次创建对象时都会判断一次浏览器能力,这是不必要的。

惰性载入有两种方式. 第一种就是在函数第一次被调用时,根据不同情况,用不同的新函数把这个函数覆盖掉,以后调用就不需要再判断而是直接执行该执行的操作。

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();
}
createXHR();//第一次调用的时候会执行if语句
createXHR();//第二次就不会执行if语句了

第二种方法就是在声明函数时候就指定适当的函数, 实际上原理和上面的类似:

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.");
};
}
})(); //这里是一个立即执行函数, 执行完毕后createXHR就可以直接调用, 无需再检测

函数绑定

函数绑定是为了解决this的指向问题:

var handler = {
message: "Event handled",
handleClick: function(event){
console.log(this);
console.log(this.message);
}
};
var btn = document.getElementById("myButton");

btn.addEventListener('click', handler.handleClick, false); //这里会输出 dom 和 undefined, 表面上handler.handleClick是挂载在handler上, 但是它里面的this指向会发生改变

// 为了解决上面的问题, 我们有如下两个方法:

// 方法1: 新增匿名函数
btn.addEventListener('click', function(evt){
handler.handleClick(evt) // 通过新增一个匿名函数可以实期待的输出
}, false);
// 方法2: 使用Es5 bind方法
btn.addEventListener('click', handler.handleClick.bind(handler), false);

// 如果浏览器不支持bind方法, 我们可以利用apply实现一个
if(!Function.prototype.bind){
Function.prototype.bind = function(fn,context){
return function(){
fn.apply(context,arguments)
}
}
}

函数curry化

函数curry化, 中文翻译柯里化, 个人觉得在大多数情况下不是很有必要.书上讲得也不好, 请直接观看 这篇文章讲解什么是curry化

防篡改对象

不可扩展对象

JS共享的本质使任意对象都可被随意修改。这样有时很不方便。ES5增加了几个方法来设置对象的行为。一旦将对象设置为防篡改就不能撤销了。

var person = { name: "Nicholas" };
Object.preventExtensions(person); //ES5新增的Object.preventExtensions方法

person.age = 29;
alert(person.age); //undefined
alert(Object.isExtensible(person)); //false
person.name = "hahah"; //可以对现有属性进行修改
alert(person.name); //hahah

密封的对象

密封对象比不可扩展对象更加严格, 它不可以添加或删除属性,已有成员的[[Configurable]]特性被设置为false。

var person = { name: "Nicholas" };
Object.seal(person);

person.age = 29;
alert(person.age); //undefined

delete person.name; //不能删除
alert(person.name); //"Nicholas"

alert(Object.isExtensible(person)); //false ,不能扩展
alert(Object.isSealed(person)); //true

冻结的对象

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"

alert(Object.isExtensible(person));//false
alert(Object.isSealed(person));//true
alert(Object.isFrozen(person));//true

高级定时器

setTimeout()和setInterval()是很实用的功能,不过有些事情是要注意的。
JS是单线程的,这就意味着定时器实际上是很有可能被阻塞的。我们在这两个函数中所设置的定时,其实是代表将代码加入到执行队列的事件,如果在加入时恰巧JS是空闲的,那么这段代码会立即被执行,也就是说这个定时被准时的执行了。相反,如果这时JS并不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时执行。

记住: 在JS中, 没有任何代码是立即执行的, 只有一旦进程空闲就执行.

重复的定时器

使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样一种可能,在上次代码还没执行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有没有代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔因为JS的处理被跳过,代码之间的间隔比预期的小。
所以尽量使用setTimeout()模拟间隔调用。

setTimeout(function(){ 
setTimeout(arguments.callee, interval);
}, interval);

yielding processes

浏览器中的js被分配了一个确定数量的资源,所以会限制js脚本的运行时间,不能过长。

如果达到这个限制,会弹出一个浏览器错误的对话框,询问是否继续执行。定时器时绕开此限制的方法之一。

脚本长时间运行的原因有两个:

  • 过长的、过深嵌套的函数调用
  • 进行大量处理的循环

通常我们是处理第二个因素, 但是要记住, 如果你的循环不必同步,或者结果不必按顺序, 那么么就可以采用yielding processes思想.

我们看这例子:

function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
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 + "<br>";
}
chunk(data, printValue);

函数节流

举个例子 , 页面有一个长度为3的轮播图, 你鼠标放到(hover)对应轮播点的时候自动显示该张图, 如果你在非常短时间(比如10ms)内快速来回hover, 那图片自然也会也会快速闪烁, 这样会操作性能的浪费. 我们就可以利用setTimeout来限制用户的hover频率

自定义事件

事件是一种叫做观察者模的设计模式(也叫发布订阅模式), 这是一种创建松散耦合的代码技术.
观察者模式有两类对象组成: 主体和观察者, 主体发布时间, 同时观察者通过订阅这些事件来观察主体. 涉及到DOM上, DOM元素就是主体, 你的事件处理程序就是观察者.

我们来实现一个简单的观察者模式:

var Pubsub = function  (argument) {
this.hub = {};
}
Pubsub.prototype.on = function(type,fn){
if (!this.hub[type]) {
this.hub[type] = [];
}
this.hub[type].push(fn);
};
Pubsub.prototype.off = function(type){
this.hub[type] = [];
};
Pubsub.prototype.fire = function(type,fn){
var fns = this.hub[type]; //有可能存了多个事件
if (!fns.length) {
console.log('无'+type+'订阅')
}
for (var i = 0; i < fns.length; i++) {
fns[i]();
}
};

var user = new Pubsub;
function read(){
console.log("I'm reading");
}
function read2(){
console.log("I'm recording");
}
user.on('update',read);
user.on('update',read2);

user.fire('update');

user.off('update');
user.fire('update');

拖放

不太清除为何拖放这节内容会放在高级技巧中, 这里不再讲解.

JS实现拖放的思路就是对一个DOM元素设置绝对定位, 然后根据鼠标的位置, 配合mouseDown/mouseUp/mouseMove事件来动态设置DOM元素的top/left值. 代码略


本章完

访客评论