js自学笔记--高级技巧

1.高级函数

1.安全的类型检测

instanceof操作符在存在多个全局作用域(一个页面包含多个框架)的情况下,也会出现问题

var isArray = value  instanceof  Array
如果要返回true,value必须是一个数组,必须与Array构造函数在同一个全局作用域(Array是window的属性),如果value定义在另一个框架中,就会返回false

由于JSON库中定义了一个全局JSON对象。很难确定页面中的JSON对象是不是原生的


解决:使用原生的toString()方法,返回[object NativeConstructorName] 格式的字符串。内部[[Class]]属性指定了字符串中的构造函数名。

由于原生数组的构造函数名与全局作用域无关,使用toString()能保证返回一致的值

	function isArray(value){
		return Object.prototype.toString.call(value) == "[object Array]";
	}
	function isFunction(value){
		return Object.prototype.toString.call(value) == "[object Function]";
	}
	var isNativeJSON = window.JSON&&Object.prototype.toString.call(JSON)=="[object JSON]";

2.作用域安全的构造函数

构造函数内使用this的问题:

当我们使用new关键字去调用构造函数时,this会指向新创建的对象。当不使用new操作符来调用构造函数时,this对象在运行时绑定,直接调用Person()构造函数,this就会映射到全局对象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 person = Person("zhangsan",12,"stu");
	console.log(window.name);
	console.log(person.name);

一旦实现这个模式后,就相当于锁定了可以调用构造函数的环境。如果使用构造函数窃取模式的继承,这个继承就会被破坏。

	function Person(name,age){
		if(this instanceof Person){
			this.name = name;
			this.age = age;
		}else{
			return new Person(name,age);
		}
	}

	function Child(name,age,color){
		Person.call(this,name,age); //会新创建一个对象,不会直接调用构造函数
		this.color =  color;
		this.sayHi = function(){
			console.log("hi");
		}
	}
	var child = new Child("zhangsan",13,"black");
	console.log(child.name);//undefined
使用原型链或寄生组合的方式去实现继承
	function Person(name,age){
		if(this instanceof Person){
			this.name = name;
			this.age = age;
		}else{
			return new Person(name,age);
		}
	}

	function Child(name,age,color){
		Person.call(this,name,age); //会新创建一个对象,不会直接调用构造函数
		this.color =  color;
		this.sayHi = function(){
			console.log("hi");
		}
	}
	Child.prototype = new Person();//原型指向Person,调用构造函数时,检查对象的类型就是Person
	var child = new Child("zhangsan",13,"black");
	console.log(child instanceof Person);//true
	console.log(child.name);// zhangsan

3.惰性载入函数

函数执行的分支仅会发生一次。有两种实现惰性载入的方式:

1.在函数被调用时再处理函数,第一次调用时,函数被覆盖为另一个适合执行的函数

	function sayHi(type){
		if(type == 'teacher'){
			sayHi = function(){
				return "teacher";
			}
		}else if(type == "child"){
			sayHi = function(){
				return "child";
			}
		}else{
			sayHi = function(){
				return "nobody";
			}
		}
		return sayHi();
	}
	console.log(sayHi("teacher"));  //teacher
	console.log(sayHi());//不会再判断执行分支 teacher
if每个语句都会给sayHi变量赋值,有效覆盖了原有的函数。下次执行时会直接调用被分配的函数。


2.在声明函数时指定适当的函数(代码首次加载时会损失一点性能,第一次调用函数时就不会损失性能了)

	var sayHi = (function(type){
		if(type == 'teacher'){
			return function(){
				return "teacher";
			}
		}else if(type == "child"){
			return  function(){
				return "child";
			}
		}else{
			return function(){
				return "nobody";
			}
		}
	})("child");
	console.log(sayHi());  //teacher
	console.log(sayHi());//不会再判断执行分支 teacher

惰性载入函数的优点是在执行分支代码时牺牲一点性能,从而避免执行不必要的代码。

4.函数绑定

函数绑定要创建一个函数,可以在特定的this环境中指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用 ,以便在将函数作为变量传递的同时保留代码执行环境

问题:

	var handler = {
		message:"event handler",
		handlerClick:function(){
			console.log(this);//指向button按钮
			console.log(this.message);
		}
	}
	var btn = document.getElementById("btn");
	btn.onclick = handler.handlerClick; //undefined
没有保存handler.handlerClick()的环境,this对象最后指向了DOM按钮,而不是handler

解决:使用闭包

	var handler = {
		message:"event handler",
		handlerClick:function(){
			console.log(this);//指向button按钮
			console.log(this.message);
		}
	}
	var btn = document.getElementById("btn");
	btn.onclick =  function(event){
		  handler.handlerClick(event);
	}; //event handler


bind():将函数绑定到指定环境的函数

自定义bind()函数,接收一个函数和一个环境

	function bind(fn,context){
		return function(){//接收event参数
			return fn.apply(context,arguments);//arguments是传给内部函数的参数
		}
	}

	var handler ={
		message:"event handled",
		handleClick:function(event){
			console.log(this.message);
			console.log(event.type);
		}
	}
	var btn = document.getElementById("btn");
	btn.onclick = bind(handler.handleClick,handler);//click事件触发内部函数,给内部函数传递event参数

用bind()函数创建了一个保持了执行环境的函数。

ECMAScript5为所有函数定义了原生的bind()方法。


btn.onclick =  handler.handleClick.bind(handler);


只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,热门需要更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。

5.函数柯里化

用于创建已经设置好了一个或多个参数的函数。创建方式:使用闭包返回一个函数。

	function add(num1,num2){
		return num1 + num2;
	}
	function curriedAdd(num2){
		return add(3,num2);
	}
	console.log(curriedAdd(2));  //2+3=5


函数柯里化:调用另一个函数并为它传入要柯里化的函数和必要参数。

	function curry(fn){
		var args = Array.prototype.slice.call(arguments,1);//2
		return function(){
			var innerArgs = Array.prototype.slice.call(arguments);//3
			var finalArgs = args.concat(innerArgs); //2,3
			return fn.apply(null,finalArgs);
		}
	}
	function add(num1,num2){
		return num1+num2;
	}
	var curriedAdd =  curry(add,2);
	console.log(curriedAdd(3));  //5


函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更复杂的bind()函数

	function bind(fn,context){
		//获取柯里化的参数
		var args = Array.prototype.slice.call(arguments,2);//"zhangsan",12
		return function(){
			//获取调用函数时传入的参数
			var innerArgs = Array.prototype.slice.call(arguments);//event
			var finalArgs = innerArgs.concat(args);//event,"zhangsan",12
			return fn.apply(context,finalArgs);
		}
	}
	handler = {
		message:"hehe",
		handleClick:function(event,name,age){
			console.log(this.message);
			console.log(event.type);
			console.log(name+":"+age);
		}
	}
	var btn = document.getElementById("btn");
	btn.onclick = bind(handler.handleClick,handler,"zhangsan",12);

ECMAScript5的bind()方法也实现了函数柯里化,只要在this的值后再传入另一个参数即可.

	handler = {
		message:"hehe",
		handleClick:function(name,age,event){//event对象作为最后一个参数传入
			console.log(this.message);
			console.log(event.type);
			console.log(name+":"+age);
		}
	}
	var btn = document.getElementById("btn");
	btn.onclick = handler.handleClick.bind(handler,"zhangsan",12);

JS中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。

2.防篡改对象

JavaScript共享的本质会导致很多问题,因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能以外修改别人的代码。
ECMAScript定义了防篡改对象(tamper-proof object)。 一旦将对象定义成防篡改,就无法撤销了

1.不可扩展对象

默认情况下,所有对象都是可以扩展的,任何时候都可以向对象中添加属性和方法。
Object.preventExtensions()方法可以禁止给对象添加属性和方法。
Object.istExtensible():检测对象是否可扩展
	var person ={
		name:"张三",
		age:"12"
	}
	Object.preventExtensions(person);
	person.job = "child";
	person.name = "lisi";
	console.log(Object.isExtensible(person));//false
	console.log(person.job);//undefined
	console.log(person.name);// lisi

2.密封的对象

ECMAScript5为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而且已有成员的[[Configurable]]特性为false。不能删除属性和方法,可以修改
	var person ={
		name:"张三",
		age:"12"
	}
	Object.seal(person);
	person.job = "child";
	person.name = "lisi";//可以修改
	delete person.name;//无法删除
	console.log(Object.isExtensible(person));//false
	console.log(Object.isSealed(person));//true
	console.log(person.job);//undefined
	console.log(person.name);// lisi 

3.冻结的对象

最严格的防篡改级别是冻结对象(frozen object)。不可扩展,又是密封的,而且对象的属性的[[Writable]]特性会被设置为false。无法直接修改属性,但是如果定义[[Set]]函数,访问器属性任然可以修改。
Object.freeze() : 冻结对象。
Object.isFrozen():检测对象是否冻结。
	var person ={
		name:"张三",
		age:"12"
	}
	Object.freeze(person);//冻结对象
	person.name = "lisi";//冻结后不能修改属性
	console.log(Object.isExtensible(person));//false
	console.log(Object.isSealed(person));//true
	console.log(Object.isFrozen());//true
	console.log(person.name);// 张三

3.高级定时器

JavaScript是 运行在单线程环境中的,定时器仅仅只是 计划代码在未来某个时间执行,但并不能保证。因为在页面生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完后的代码运行,事件处理程序,Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责排序,指派某段代码在某个时间点运行的优先级。

可以把JavaScript想象成在时间线上运行的。当页面载入时,首先执行的是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,有时候也包含一切初始数据的处理。

除了主JavaScript执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序加入队列。如:当按下某个按钮时,时间处理程序代码会被添加到队列,并在下一个可能的时间里执行,当接收到某个Ajax响应时,回调函数的代码会被添加到队列。

定时器工作方式: 特定时间过去后将代码插入队列(并不意味着立刻执行,如果这个时间点上,队列中没有其他东西,这段代码就会得到执行)

执行完一套代码后,JavaScript进程会返回一段很短的时间,这样页面上的其他处理就可以进行了。由于JavaScript进程会阻塞其他页面处理,所以必须有这些小间隔来防止用户界面被锁定(代码长时间运行中还有可能出现)。这样设置一个定时器,可以确保在定时器代码执行前至少有一个进程间隔。

1.重复的定时器

setInterval()创建定时器,确保了定时器代码规则地插入队列中。问题:定时器代码可能在代码再次被添加到队列之前还没执行完。导致定时器代码连续运行好几次,而之间的时间没有任何停顿。
面对这种情况,JavaScript引擎做了处理。setInterval()时,仅当没有该定时器代码时,才向队列中添加定时器代码。导致(1)某些时间间隔被跳过。(2)多

个定时器代码执行之间的时间间隔可能比预期小。


使用链式setTimeout()方式调用来避免上述问题。

	setTimeout(function(){
		var div = document.getElementById("editDiv");
		left = parseInt(div.style.left)+5;
		div.style.left = left+"px";
		console.log(div.style.left);
		if(left<300){
			setTimeout(arguments.callee,500);
		}
	},100);

2.Yielding Processes

在浏览器中的JS都被分配了一个确定数量的资源。需要的内存大小和处理器时间都被严格限制了,防止恶意的Web程序员把用户机器资源耗尽。

脚本长时间运行的问题通常由两个原因之一造成:过长的,过深嵌套的函数调用或者是进行大量处理的循环。

对于后者,可以使用定时器分割这个循环。这是一种叫做数组分块(array  chunking)的技术,小块小块地处理数组。

基本思路:为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。

	setTimeout(function(){
		//取出下一个条目进行该处理
		var item = array.shift();
		process(item);
		//如果还有条目,再设置另一个定时器
		if(array.length>0){
			setTimeout(arguments.callee,100);//使用arguments.callee调用同一个匿名函数
		}
	},100);

实现数组分块:

	function chunk(array,process,context){
		setTimeout(function(){
			var item = array.shift();//待处理事项
			process.call(context,item);
			if(array.length>0){//如果还有待办事项
				setTimeout(arguments.callee,100);
			}
		},100);
	}

	function printValue(value){
		console.log(value);
	}
	var data = [1,2,3,4,5,6,7,8];
	chunk(data.concat(),printValue);//传递数组的克隆,保持原数组不变
可将多个项目的处理在执行队列上分开,在每个项目处理后,给予其他的浏览器处理机会运行,可避免长时间运行脚本的错误。

一旦某个函数要花50ms以上的时间完成,最好将任务分割为一系列可以使用定时器的小任务。

3.函数节流

浏览器中某些计算和处理要比其他的昂贵很多。如,DOM操作比非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在IE中使用onresize事件处理程序时。为了绕开这个问题,可以使用定时器对该函数进行节流。

基本思想:指某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。如果前一个定时器未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。


基本形式:

	var processor ={
		timeoutId:null,
		//实际处理的程序
		performProcessing:function(){
			//实际执行的代码
		}
		//初始处理调用的方法
		process:function(){
			//清除前一个定时器
			clearTimeout(this.timeoutId);
			//保留this当前执行环境
			var processContext = this;
			//设置新的处理程序定时器
			this.timeoutId = setTimeout(function(){
				processContext.performProcessing();//调用实际处理程序
			},100);
		}
	} 
	//开始执行
	processor.process();
由于setTimeout(0中用到的函数环境总是window,所以需要保存this的引用以便以后使用。
process()之后至少100ms后才会调用performProcessing()。如果100ms内调用了多次process(),performProcessing()只调用一次。


使用throttle()函数来简化,该函数可自动进行定时器的设置和清除。

	function throttle(method,context){
		clearTimeout(method.tId);
		method.tId = setTimeout(function(){
			method.call(context);//如果没有给定执行环境,就会在全局作用域内执行
		},1000);
	}

	function resizeDiv(){
		var div = document.getElementById("editDiv");
		div.style.height = div.offsetWidth + "px";
	}
	window.onresize = function(){
		//resizeDiv();
		throttle(resizeDiv);
	}
只要代码是周期性执行的,都应该使用节流。

4.自定义事件

事件是JavaScript与浏览器交互的主要途径。事件是一种叫做观察者的设计模式,是一种创建松散耦合代码的技术。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体不知道观察者的任何事情,也就是说它可以独立只存在并正常运作,即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。

DOM元素便是主体,事件处理代码便是观察者。

事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中--通过实现自定义事件。创建一个管理事件的对象,让其他对象监听那些事件。
基本模式:
	function EventTarget(){
		this.handlers = {};//存储事件处理程序
	}
	EventTarget.prototype = {
		constructor:EventTarget,
		//type事件类型,handler处理该事件的函数
		addHandler:function(type,handler){//注册给定类型事件的事件处理程序
			if(typeof this.handlers[type] == 'undefined'){
				this.handlers[type] = [];
			}
			this.handlers[type].push(handler);
		},
		//event,至少包含type属性的对象
		fire:function(event){//触发一个事件
			if(!event.target){//给event对象初始化设置一个target属性。
				event.target = this;
			}
			if(this.handlers[event.type] instanceof Array){//查找该事件类型的一组处理程序
				var handlers = this.handlers[event.type];
				for(var i=0;len=handlers.length;i<len;i++){//遍历调用所有的事件处理程序
					handlers[i](event);
				}
			}
		},
		removeHandler:function(type,handler){
			if(this.handlers[type] instanceof Array){
				var handlers = this.handlers[type];
				for(var i=0;len=handlers.length;i<len;i++){
					if(handlers[i] === handler){
						break;
					}
				}
				handlers.splice(i,1);//从数组中删除该处理程序
			}
		}
	}
使用自定义的事件:
	function handlerMessage(event){
		console.log("消息处理:"+event.message);
	}
	//创建一个新的对象
	var target = new EventTarget();
	//添加事件处理程序,类型是message
	target.addHandler("message",handlerMessage);
	//触发message类型的事件,并自定义event对象的message属性
	target.fire({type:"message",message:"消息事件处理"});
	//删除事件处理程序
	target.removeHandler("message",handlerMessage);
	//再次触发事件,已删除
	target.fire({type:"message",message:"再次触发事件,已删除"});

因为这种功能是封装在一种自定义类型中的,其他对象可以继承EventTarget并获得这个行为。
	function Person(name,age){
 		EventTarget.call(this);
 		this.name = name;
 		this.age = age;
 	}
 	Person.prototype = new EventTarget();
 	Person.prototype.constructor = Person;

 	//自定义方法
 	Person.prototype.say = function(msg){
 		//触发自定义的message事件
 		person.fire({type:"message",message:msg});
 	}
 	var person = new Person("zhangsan",23);
 	//添加事件处理程序
 	person.addHandler("message",function(event){
 		console.log(event.message + event.target.name );
 	});

 	person.say("hi,i'm ");// hi,i'm zhangsan
当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值