本文适合有必定JS开发基础的读者,文章涉及开发中常常遇到的一些使人疑惑的问题,理解这些问题有助于咱们快速提高对JS这门语言的理解和应用能力。文章只讲述具体问题中的关键问题,不涵盖全面的知识点。如想了解具体的知识,能够参考笔者博客的相关文章。javascript
在实际应用中, this的指向大体分为如下四种:
(1)做为对象方法的调用
(2)做为普通函数调用
(3)构造器调用
(4)Function.prototype.call或Function.prototype.applyhtml
1-1阅读下面代码:java
//1.做为对象方法的调用this老是指向那个对象 window.name = 'globalName'; var getName = function(){ return this.name; }; console.log( getName() ); // 输出:globalName //2.做为普通函数的调用:非严格模式下this老是指向window,严格模式下 undefined window.name = 'globalName'; var myObject = { name: 'sven', getName: function(){ return this.name; } }; var getName = myObject.getName;//关键:这里保留了一个普通函数的引用 console.log( getName() ); // globalName
经过以上两个对比,理解使用方法不一样,this指向不一样node
1-2阅读下面的代码:web
var getId = function( id ){ return document.getElementById( id ); }; getId( 'div1' ); //咱们也许思考过为何不能用下面这种更简单的方式: var getId = document.getElementById; getId( 'div1' );
document.getElementById方法须要用到this。这个this原本被指望指向document,当getElementById被看成 document的属性被调用时,方法内部的this确实是指向document.
可是当使用getId来引用document.getElementById以后,在调用getId,此时就变成了普通函数调用,内部的this就指向了window。
利用call或者apply更正this指向:
//咱们能够尝试利用apply 把document 看成this 传入getId 函数,帮助“修正”this:编程
document.getElementById = (function( func ){ return function(){ return func.apply( document, arguments ); } })( document.getElementById ); var getId = document.getElementById; var div = getId( 'div1' ); alert (div.id); // 输出: div1
2-1:bind方法的兼容写法浏览器
var bind = Function.prototype.bind || function( context ){ var self = this; // 保存原函数 return function(){ // 返回一个新的函数 return self.apply( context, arguments ); // 执行新的函数的时候,会把以前传入的context看成新函数体内的this } };
3-1.如今来看看下面这段代码:安全
var func = function(){ var a = 1; return function(){ a++; alert ( a ); } }; var f = func(); f(); // 输出:2 f(); // 输出:3 f(); // 输出:4 f(); // 输出:5
当执行f = func()时,f返回了一个匿名函数的引用,它能够访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里。这个变量就有了不被销毁的理由,这里就产生了一个闭包结构。
3-2常见的闭包的问题:闭包
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div>1</div> <div>2</div> <div>3</div> <div>4</div> <div>5</div> <script type="text/javascript"> var node = document.getElementsByTagName('div'); for(var i=0;len=node.length;i<len;i++){ nodes[i].onclick=function(){ alert(i); } } //不管点击哪一个结点,都返回5 //这是由于onclick事件是异步的,当事件触发的时候,for循环早已经结束 //保存了函数的引用,顺着做用域链从内到外查找i时,查到的值老是5 //解决方法就是在闭包的帮助下,把每次循环的i都封闭起来。 /* for(var i=0;len=node.length;i<len;i++){ nodes[i].onclick=(function(i){ alert(i); })(i); }*/ </script> </body> </html>
3-3.利用闭包延续局部变量的寿命app
//img 对象常常用于进行数据上报,以下所示: var report = function( src ){ var img = new Image(); img.src = src; }; report( 'http://xxx.com/getUserInfo' ); //丢失数据的缘由是img是report函数中的局部变量,当函数调用以后局部变量就销毁了,而此时或许还没来得及发起http请求 //如今咱们把img 变量用闭包封闭起来,便能解决请求丢失的问题: var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; } })();
闭包与内存管理
闭包会使一些数据没法被及时的销毁,若是未来须要回收这些变量,咱们能够手动把这些变量设置为null。
跟闭包和内存泄漏有关系的地方是,使用闭包的同时容易造成循环引用,若是闭包的做用域链中保存着一些DOM结点,这时候就有可能形成内存泄漏。
(1)函数能够做为参数被传递
(2)函数能够做为返回值输出
4-1.函数做为参数传递
Array.prototype.sort方法:
var array = ['10','5','12','3']; array.sort(); //array:['10','12','3','5'] //如代码那样,排序的结果并非咱们想要的,这与sort函数的比较规则有关系 array.sort(function(a,b){return a-b;}); //array:['3','5','10','12'] 传入一个比较的函数,就能够按照数字大小的规则进行正确的比较了。
4-2.函数做为返回值输出
var getSingle = function ( fn ) { var ret; return function () { return ret || ( ret = fn.apply( this, arguments ) ); }; };
4-3.函数做为参数被传递而且返回另外一个函数
var getScript = getSingle(function(){ return document.createElement( 'script' ); }); var script1 = getScript(); var script2 = getScript(); alert ( script1 === script2 ); // 输出:true
4-4.高阶函数应用
(1)高阶函数实现AOP
AOP(面向切面编程)的主要做用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些业务逻辑无关的功能包括日志统计、控制安全、异常处理等。把这些功能抽离出来以后,再经过“动态织入”的方式掺入业务逻辑模块中。
下面代码经过扩展Function.prototype来实现把一个函数“动态织入”
Function.prototype.before = function( beforefn ){ var __self = this; // 保存原函数的引用 return function(){ // 返回包含了原函数和新函数的"代理"函数 beforefn.apply( this, arguments ); // 执行新函数,修正this return __self.apply( this, arguments ); // 执行原函数 } }; Function.prototype.after = function( afterfn ){ var __self = this; return function(){ var ret = __self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } }; var func = function(){ console.log( 2 ); }; func = func.before(function(){ console.log( 1 ); }).after(function(){ console.log( 3 ); }); func();
(2)柯里化
一个currying函数首先会接受一些参数,接受了这些参数以后,该函数不会当即求值,而是继续返回另一个函数,刚才传入的参数在函数造成的闭包中被保存了下来。待到函数真正须要求值的时候,以前传入的全部参数都会一次性用于求值。
一个经典的柯里化:
function curry(fn){ var arr1 = Array.prototype.slice.call(arguments,1); return function(){ var arg2 = Array.prototype.slice.call(arguments); var array = arr1.concat(arr2); return fn.apply(null,array); } }
不断累积的柯里化:
var currying = function( fn ){ var args = [];//外层函数变量:用来累积 return function(){ if ( arguments.length === 0 ){ return fn.apply( this, args ); }else{ [].push.apply( args, arguments ); return arguments.callee; } } };
(3)uncurrying
在javascript中,当咱们调用对象的某个方法时,其实不用关心对象本来是否被设计为拥有这个方法,这是动态类型语言的特色,也就是常说的鸭子类型思想。
同理,一个对象也未必只能使用它本身的方法,其实能够借用本来不属于他的方法: call apply
Function.prototype.uncurrying = function () { var self = this; return function() { var obj = Array.prototype.shift.call( arguments ); return self.apply( obj, arguments ); }; }; var push = Array.prototype.push.uncurrying(); var obj = { "length": 1, "0": 1 }; push( obj, 2 );//将2使用push的方法做用到obj上 console.log( obj ); // 输出:{0: 1, 1: 2, length: 2}
函数节流也用到了高阶函数的知识,由于比较重要,因此单开了一个标题。
javascript中的函数在大多数状况下都是由用户主动调用触发的,除非是函数自己的实现不合理。可是在一些少数状况下,函数可能被很频繁的调用,而形成大的性能问题。
(1)函数被频繁调用的场景
1.window.onresize事件 2.mousemove事件 3.上传进度
(2)函数节流的原理
解决函数触发频率过高的问题,须要咱们按照时间段来忽略一些事件请求。
(3)函数节流的代码实现
详情能够参考
Underscore.js#throttle
Underscore.js#debounce
简单实现:
将即将被执行的函数用steTimeout延时一段时间执行。若是该次延时执行尚未完成,就忽略掉接下来调用该函数的请求。
var throttle = function ( fn, interval ) { var __self = fn, // 保存须要被延迟执行的函数引用 timer, // 定时器 firstTime = true; // 是不是第一次调用 return function () { var args = arguments, __me = this; if ( firstTime ) { // 若是是第一次调用,不需延迟执行 __self.apply(__me, args); return firstTime = false; } if ( timer ) { // 若是定时器还在,说明前一次延迟执行尚未完成 return false; timer = setTimeout(function () { // 延迟一段时间执行 clearTimeout(timer); timer = null; __self.apply(__me, args); }, interval || 500 ); }; }; window.onresize = throttle(function(){ console.log( 1 ); }, 500 );
另外一种实现函数节流的方法-分时函数
某些函数确实是用户主动调用的,可是由于一些客观的缘由,这些函数会严重的影响页面的性能。
一个例子就是建立QQ好友列表。若是一个好友列表用一个节点表示,当咱们在页面中渲染这个列表的时候,可能要一次性的网页面中建立成百上千个节点。
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假设ary 装载了1000 个好友的数据 }; var renderFriendList = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); } }; renderFriendList( ary );
在短期内网页面中大量添加DOM节点显然也会让浏览器吃不消。
这个问题的解决方案之一是下面的timeChunk函数:让建立节点的工做分批进行
//第一个参数是建立节点时须要的数据,第二个参数封装了建立节点逻辑的函数,第三个参数表示每一批建立节点的数量。 var timeChunk = function( ary, fn, count ){ var obj, t; var len = ary.length; var start = function(){ for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){ var obj = ary.shift(); fn( obj ); } }; return function(){ t = setInterval(function(){ if ( ary.length === 0 ){ // 若是所有节点都已经被建立好 return clearInterval( t ); } start(); }, 200 ); // 分批执行的时间间隔,也能够用参数的形式传入 }; }; var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); }; var renderFriendList = timeChunk( ary, function( n ){ var div = document.createElement( 'div' ); div.innerHTML = n; document.body.appendChild( div ); }, 8 ); renderFriendList();
在web开发中,由于浏览器之间的实现差别,一些嗅探工做老是不可避免。
var addEvent = function( elem, type, handler ){ if ( window.addEventListener ){ return elem.addEventListener( type, handler, false ); } if ( window.attachEvent ){ return elem.attachEvent( 'on' + type, handler ); } };
这个函数的缺点是,当它每次被调用的时候都会执行里面的if条件分支。
下面这个函数虽然仍然有一些分支判断,可是在第一次进入条件分支以后,在函数内部就会重写这个函数,重写以后的函数就是咱们但愿的addEvent函数。
var addEvent = function(ele,type,handler){ if(window.addEventListener){ addEvent = function(ele,type,handler){ elem.addEventListener( type, handler, false ); } } if(window.attachEvent){ addEvent = function(ele,type,handler){ elem.attachEvent( 'on' + type, handler ); } } addEvent(ele,type,handler); }
文章介绍的都是JS须要掌握的重点又是难点的知识,须要多动手实践才能理解。有关相关知识的详细讲解,能够参考笔者的相关文章。固然 ,最好的方式是去谷歌而后本身动手实践。