js高级程序设计(第4版)-js 技巧

829 阅读4分钟

安全类型检查

  • typeof在某些浏览器返回的类型不一定准确,比如老版本的safari就认为正则是function, typeof null === 'object'
  • instanceof 存在作用域问题, 原型可以被修改 所以比较靠谱的方式是通过利用原生Object对象的toString()方法返回的字符串。当然前提是这个toString()方法没有被覆盖。
function isArray(value){
	return Object.prototype.toString.call(value) === '[object Array]'
}

作用域安全的构造函数

两种作用域会被污染

  • 1、构造函数不用new,内部的this很容易因为调用环境不同而被污染
function Person(name, age){
    this.name = name
    this.age = age
}
let p1 = new Person('xiaoming', 123) // 
let p2 = Person('xiaoming', 3) // this 是window  p2 undefined

修改:

function Person(name, age){
	if(this instanceof Person){
        this.name = name
        this.age = age
	} else {
		return new Person(name, age)
	}
}
let p1 = new Person('xiaoming', 123) //  ok
let p2 = Person('xiaoming', 3) // ok

这种操作会遇到另一个坑坑坑坑,因为Person的作用域安全的,调用Person.call的时候this对象并非是Person对象的实例,所以Person对象会返回一个new出来的新Person对象。Police中的this并没有被增长。即:

  • 2、父类已经好好写了,但是子类继承父类的时候不按常理出牌也需要重新指向一下原型为父类来避免
function Person(name, age){
	if(this instanceof Person) {
		this.name = name;
		this.age = age;	
	} else {
		return new Person(name, age);
	}
}
function Police(name, age, number){
	Person.call(this, name, age);
	this.number = number;
}
let p1 = new Police('xx', 12, 3) // 第一次this 是police  第二次是person ,导致p1 没继承到
// 加了这个,原型上可以 instanceof 是可以找到的。所以扩展了。
Police.prototype = new Person();
let p2 = new Police('xx', 12, 3) 

惰性载入函数

惰性载入函数的意思是有大量分支逻辑的函数,但是这些分支只需要选择一次,后续都不需要再选择了,常用语浏览器特性的差异判断。有2两种方式来实现惰性函数。

  • 在函数被调用时再处理函数。
  • 在函数声明时就指定适当的函数。损失加载性能,但是能提高第一次执行性能。

eg:

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

所以上面是会一直执行的。用方法一修改 (把createXHR 重置了,所以下次就不会再次判断)

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("No XHR object available.");
		}
	}
	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 {
		return function( ){
			throw new Error("No XHR object available.");
		};
	}
})();

函数柯里化(function currying)

实现方式跟函数绑定是一样的,都是用闭包返回一些函数。区别在于,在函数调用的时候需要处理一些参数。

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1);
    return function( ) {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments));
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs);
    }
}

curry 之后会有哪些好处:

  • 参数复用
// 普通操作  参数无法复用,不够优雅
function check(reg, txt){
	return reg.test(txt)
}
// use 
check(/\d+/g, 'test')
check(/[a-z]+/g, 'test') 
check(/[a-z]+/g, 'test2')
// 柯里化函数 ☆
function curryingCheck(reg){
    return function(txt){
    	return reg.text(txt)
    }
}
let hasNumber = curryingCheck(/\d+/g)
let hasLetter = curryingCheck(/[a-z]+/g)
// 这样参数即可复用了
  • 提前确认 即自执行
  • 延迟执行
Function.prototype.bind = function(context){
    let _this = this
    let _args = Array.prototype.slice.call(arguments, 1)
    return function(){
		return _this.apply(context, _args)
	}
}

通用型currying,如上初步封装 上述只能多扩展一个参数,currying(a)(b)(c) 这种的无法支持。看其他高手再次封装

// 支持多参数传递
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);
    }
}

curry 性能

  • 存取argument对象参数慢
  • 创建大量嵌套作用域和闭包函数,代理内存花销
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}
add(1)(2)(3)                // 6
// 每个对象的toString和valueOf方法都可以被改写,每个对象执行完毕,如果被用以操作JavaScript解析器就会自动调用对象的toString或者valueOf方法
// 利用toString隐式调用的特性,当最后执行时隐式调用,并计算最终的值返回

详看如下: www.jianshu.com/p/2975c25e4… zhuanlan.zhihu.com/p/79672381

防篡改对象 Object.freeze(obj)

浅冻结只能冻结当前对象、数组等内部第一层,如果要深冻结,就需要通过递归去处理了。

/* * 深度冻结 */
function deepFreeze(o) {
	var prop, propKey
	Object.freeze(o) // 首先冻结第一层对象
	for (propKey in o) {
		prop = o[propKey];
		if (!o.hasOwnProperty(propKey) || !(typeof prop === "object") || Object.isFrozen(prop)) {
			continue;// 跳过原型链上的属性、基本类型和已冻结的对象.
		}
		deepFreeze(prop);//递归调用.
	}
}

高级定时器

1、重复的定时器 JavaScript是只有一个单进程执行代码。所以不管是setTimeout(),还是setInterval()都是不能保证准时执行的。JS中除了执行主进程,还有一个待执行队列的概念,任何代码的执行都是被加入到这个待执行队列中,等待主进程空闲的时候再去执行的。所以定时器的概念不是定义何时执行,而是定义代码何时被加入到这个待执行队列中。

setInterval()创建的重复定时器,虽然比较聪明的帮我们解决了,如果加入一个新定时器实例的时候已经存在一个定时器就不在加入,会跳过。但是这里还存在另外一个问题,如果定时器内部逻辑代码的执行时间比较长,每次执行完都有一个新的定时器实例在等着执行下一次,就会导致定时器代码持续不断的在执行。

所以比较好的方式,是结合setTimeout()和setInterval()的思想:

setTimeout(function( ){
	// 这里先执行定时处理的业务代码
	// interval就是你具体想要间隔的时间值
    // callee是arguments的一个属性,指的是对函数对象本身的引用。
	setTimeout(arguments.callee, interval);
}, interval);

Yielding Processes 脚本长时间运行会导致浏览器弹窗询问是否继续执行。

长时间运行的原因可能有2种:

  • 过长、过深的嵌套函数调用

  • 大量处理的循环 对于大量处理的循环问题,先问两个问题:

  • 该处理是否必须是同步完成的

  • 数据是否必须按顺序完成 如果答案是否,那么就可以用定时器分割这个循环。这个技术叫做数组分块(array chucking):

function chuck(array, process, context){
	setTimeout(function( ){
		//取出并允许一条数据
		var item = array.shift(); //array的本质就是一个待办事项列表
		process(item);
		//如果还有待处理的数据,则设置一个新定时器处理
		if(array.length > 0){
			setTimeout(arguments.callee, 100);
		}		
	}, 100);
}

eg2

         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,12,1234,453,436,23,23,5,4123,45,346,3563,2234,345,342];
 
         function printValue(item) {
             var div = document.getElementById('myDiv');
             div.innerHTML += item + "</br>";
         };
         chunk(data.concat(), printValue);

如何让定时器更加准确?

理想情况,应该每次输入0的,或者很接近0,当遇到有阻塞的情况,这里的延迟就好很大,需要对下一次的间隔时间进行校正 。

let start = new Date().getTime()
let count = 0
setInterval(function(){
	count++
    console.log(new Date().getTime() - (start + count * 1000))
}, 1000)

eg:

//继续线程占用
setInterval(function(){ 
     var j = 0; 
     while(j++ < 100000000); 
}, 0); 

//倒计时
var  interval = 1000,
       ms = 50000,  //从服务器和活动开始时间计算出的时间差,这里测试用50000ms
       count = 0,
       startTime = new Date().getTime();
  if( ms >= 0){
         var timeCounter = setTimeout(countDownStart,interval);                  
  }
function countDownStart(){
       count++;
       var offset = new Date().getTime() - (startTime + count * interval);
       var nextTime = interval - offset;
       var daytohour = 0; 
       if (nextTime < 0) { nextTime = 0 };
       ms -= interval;
       console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
       if(ms < 0){
              clearTimeout(timeCounter);
       }else{
              timeCounter = setTimeout(countDownStart,nextTime);
       }
 }

结论:由于线程阻塞延迟问题,做了setTimeout执行时间的误差修正,保证setTimeout执行时间一致。若冻结时间特别长的,还要做特殊处理,比如数组分块啥的。 参考文章: www.cnblogs.com/xinxingyu/p…