系列文章 w字总结《JavaScript设计模式与开发实践》(设计模式)(上)
w字总结《JavaScript设计模式与开发实践》(设计模式)(下)
w字总结《JavaScript设计模式与开发实践》(设计原则和编程技巧)
前言 为什么我要开始写这类文章了,一切都得从EDG夺冠开始说起,在打完第三局后我发了一条朋友圈……如下图。
我也是没想到EDG韧性这么强,汉子哥这么热爱中国。 不管怎么说,恭喜EDG,自己立的flag,哭着也要把它实现。(其实蛮不错的,充实自己。) 现在每天和周末空闲时间就读一些,做做笔记,周末整合一下形成文章,往掘金这么一发。
this,call和apply this
在JavaScript中,this指向的对象是在运行时基于函数的执行环境动态绑定的,而非声明函数时的环境
this指向除去with
和eval
这两种会“破坏”我们对作用域理解的情况,this的指向大致分为以下几种。
作为对象的方法
作为普通函数
构造器调用
Function.prototype.call
或 Function.prototype.apply
调用
作为对象的方法调用时,this指向该对象
1 2 3 4 5 6 7 8 9 var obj = { name :'moe' , getName :function ( ) { console .log (this === obj) console .log (this .name ) } } obj.getName ()
当函数不作为对象属性而作为普通函数调用时,this指向全局对象,在浏览器的JavaScript中,全局对象为window
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 window .name = 'moe' var getName = function ( ) { return this .name }var person = { name :'kid' , getName :function ( ) { return this .name } }var globalGetName = person.getName console .log ( getName () ) console .log ( globalGetName () )
作为构造器调用
用new运算符调用时,该函数返回一个对象,通常情况下,构造器内this指向返回的这个对象。
1 2 3 4 5 6 var MyClass = function ( ) { this .name = 'moe' }var obj = new MyClass ()console .log (obj.name )
但如果构造器显式返回了一个object类型对象,则返回的为该对象,而不是上述的this。
1 2 3 4 5 6 7 8 var MyClass = function ( ) { this .name = 'moe' return { name : 'kid' } }var obj = new MyClass ()console .log (obj.name )
构造器不显式返回数据或返回非对象类型数据,则无上述问题。
Function.prototype.call或Function.prototype.apply调用可以动态改变传入函数的this
1 2 3 4 5 6 7 8 9 10 11 12 13 var obj1 = { name :'a' , getName :function ( ) { return this .name } }var obj2 = { name :'b' }console .log (obj1.getName ()) console .log (obj1.getName .call (obj2))
call和apply
call和apply都是用来修改this指向,并执行函数,唯一的区别是入参形式不同
apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为集合(数组或类数组),apply方法把这个集合中的元素作为参数传递给被调用的函数。
call本质上为apply的语法糖,它传入参数数量不定,第一个参数同apply一样,也是代表函数体内this指向,从第二个参数往后,每个参数被依次传入函数。
1 2 3 4 5 6 var foo = function (a, b, c ) { console .log ([a, b, c]) } foo.apply (null , [1 , 2 , 3 ]) foo.call (null , 1 , 2 , 3 )
上述代码中,我们传入的第一个参数为null,则函数体内的this会指向默认的宿主对象。
call和apply的用途
改变this指向
借用其他对象方法
利用apply或call,可以实现类似于继承的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var A = function (name ) { this .name = name }var B = function ( ) { A.apply (this ,arguments ) } B.prototype .getName = function ( ) { return this .name }var b = new B ('newBee' )console .log ( b.getName () )
函数的arguments是一个类数组对象,因为其不是真正的数组,所以无法像数组一样进行排序或向集合中添加删除元素之类的操作。这种情况我们可以借用Array.prototype
对象上的方法,比如push。
1 2 3 4 (function ( ) { Array .prototype .push .call (arguments , 3 ); console .log (arguments ); })(1 , 2 )
闭包和高阶函数 闭包 变量作用域 在函数中声明变量,如果该变量前没有关键字var,该变量就会成为全局变量,而在函数中用var关键字声明的变量为该函数的局部变量,只有在该函数内才能访问到该变量。
1 2 3 4 5 6 7 8 var foo = function ( ) { a = 1 var b = 2 console .log ('in foo:' , b) }foo ()console .log ( a ) console .log ( b )
函数可以用来创造函数作用域,此时函数像一层半透明玻璃,函数内可以看到外面的变量,而函数外无法看到函数内部的变量。这是因为在函数中搜索一个变量时,若函数内没有声明这个变量,搜索则会随着代码执行环境创建的作用域链向外逐层搜索,直到全局对象。
1 2 3 4 5 6 7 8 9 10 11 12 var a = 1 var bar = function ( ) { var b = 2 var foo = function ( ) { var c = 3 console .log ( b ) console .log ( a ) } foo () console .log ( c ) }bar ()
变量的生存周期
全局变量的生存周期为永久,除非主动销毁。
函数内的局部变量会随着函数调用的结束而被销毁。
而闭包的存在可以让我们延续函数内局部变量的生命周期
1 2 3 4 5 6 7 8 9 10 11 var func = function ( ) { var a = 1 return function ( ) { a++ console .log (a) } }var f = func ()f () f () f ()
类似的,我们一定遇到过这样的题目
1 2 3 4 5 6 7 8 9 10 for (var i = 0 ; i < 5 ; i++) { setTimeout (function ( ) { console .log ( i ); }, 1000 ); } console .log (new Date , i);
在这个函数里面的i其实引用的是最后一次i的值,为什么不是0,1,2,3,4…呢?因为在你for循环的时候,你并没有执行这个函数,你这个函数是过一秒才执行的,当执行这个函数的时候,它发现它自己没有这个变量i,于是向它的作用域链中查找这个变量i,因为这个时候已经for循环完了,所以储存在作用域链里面的i的值就是5,最后就打印出来5了。
利用闭包解决,通过自执行函数,将变量i保存到该函数的参数中,延长其生命周期。
1 2 3 4 5 6 7 8 for (var i = 0 ; i < 5 ; i++) { (function (j ) { setTimeout (function ( ) { console .log (j); }, 1000 ); })(i); }console .log (i);
闭包的更多作用
封装变量
如果一大块代码中中存在可独立的小代码块,我们通常将其封装在独立的小函数中,独立出来的小函数有助于复用,如果它们不需要在程序的其他地方使用,最好用闭包将它们封闭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var mult = (function ( ) { var cache = {} var calculate = function ( ) { var a = 1 for (var i = 0 , l = arguments .length ; i < l; i++ ) { a = a * arguments [i] } return a } return function ( ) { var args = Array .prototype .join .call (arguments , ',' ); if (args in cache) { return cache[args] } return cache[args] = calculate.apply (null , arguments ) } })
延续局部变量寿命
1 2 3 4 5 6 7 8 var report = (fucntion ( ) { var imgs = [] return function (src ) { var img = new Image () imgs.push (img) img.src = src } })
闭包和面向对象设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var extent = function ( ) { var value = 0 return { call :function ( ) { value++ console .log (value) } } }var extent = extent (); extent.call (); extent.call (); extent.call (); var extent2 = { value :0 , call :function ( ) { this .value ++ console .log (this .value ) } } extent2.call (); extent2.call (); extent2.call ();
用闭包实现命令模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <html > <body > <button id ="undo" > 点击我执行命令</button > <button id ="execute" > 点击我执行命令</button > <script > var Tv = { open : function ( ){ console .log ( '打开电视机' ); }, close : function ( ){ console .log ( '关上电视机' ); } }; var OpenTvCommand = function ( receiver ){ this .receiver = receiver; }; OpenTvCommand .prototype .execute = function ( ){ this .receiver .open (); }; OpenTvCommand .prototype .undo = function ( ){ this .receiver .close (); }; var setCommand = function ( command ){ document .getElementById ( 'execute' ).onclick = function ( ){ command.execute (); } document .getElementById ( 'undo' ).onclick = function ( ){ command.undo (); } }; setCommand ( new OpenTvCommand ( Tv ) ); </script > </body > </html >
高阶函数
即函数柯里化,指函数作为参数或函数作为返回值输出的函数。
函数作为参数传递
回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 var getUserInfo = function (userId, cb ) { $.ajax ('http://xxx.com/getUserInfo?' + userId , function (data ) { if (typeof cb === 'function' ) { cb (data) } }) }getUserInfo (10086 , (data )=> { console .log (data) })
1 2 3 4 5 6 7 8 9 10 11 12 13 var appendDiv = function ( callback ){ for ( var i = 0 ; i < 100 ; i++ ){ var div = document .createElement ( 'div' ); div.innerHTML = i; document .body .appendChild ( div ); if ( typeof callback === 'function' ){ callback ( div ); } } }; appendDiv (function ( node ){ node.style .display = 'none' ; });
Array.prototype.sort
Array.prototype.sort接受一个函数当作参数,函数内封装了数组元素的排序规则,我们只需要关注用什么规则排序,这是可变的,而对数组排序则是不变的。把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使之更加灵活。
1 2 3 4 5 6 7 8 [1 , 4 , 3 ].sort ((a, b ) => { return a - b }) [1 , 4 , 3 ].sort ((a, b )=> { return b - a })
函数作为返回值输出
判断数据类型
1 2 3 4 5 6 7 8 9 10 var isType = function ( type ){ return function ( obj ){ return Object .prototype .toString .call ( obj ) === '[object ' + type +']' ; } };var isString = isType ( 'String' ); var isArray = isType ( 'Array' ); var isNumber = isType ( 'Number' ); console .log ( isArray ( [ 1 , 2 , 3 ] ) );
getSingle
单例模式,后续设计模式章节会有详细介绍
1 2 3 4 5 6 var getSingle = function ( fn ) { var ret; return function ( ) { return ret || ( ret = fn.apply ( this , arguments ) ); }; };
这里getSingle是一个高阶函数,将函数作为参数传递,又让函数执行后返回另一个函数,看看效果。
1 2 3 4 5 6 var getScript = getSingle (function ( ){ return document .createElement ( 'script' ); }); var script1 = getScript (); var script2 = getScript (); console .log ( script1 === script2 );
高阶函数实现AOP AOP即面向切面编程,主要作用是将一些与核心业务逻辑无关的代码抽离,通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过“动态织入”的方式掺入业务逻辑模块,这样做的好处是可以保持业务逻辑的纯净与高内聚,且可以方便的复用抽离的功能模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Function .prototype .before = function (beforeFn ) { var _self = this return function ( ) { beforeFn.apply (this , arguments ) 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 foo = function ( ) { console .log ('函数执行' ) } foo = foo.before (function ( ) { console .log ('before' ) }).after (function ( ) { console .log ('after' ) })foo ()
结果:
高阶函数的其他应用
函数柯里化(currying)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ; } } }; var cost = (function ( ){ var money = 0 ; return function ( ){ for ( var i = 0 , l = arguments .length ; i < l; i++ ){ money += arguments [ i ]; } return money; } })(); var cost = currying ( cost ); cost ( 100 ); cost ( 200 ); cost ( 300 ); alert ( cost () );
uncurrying
简单来说,uncurrying函数是实现从别的对象中赋值方法,比如我们常常让类数组对象去借用Array.prototype上的方法,这是call和apply最常见的应用场景之一。
1 2 3 4 (function ( ){ Array .prototype .push .call ( arguments , 4 ); console .log ( arguments ); })( 1 , 2 , 3 );
uncurrying用来解决将泛化this的过程提取出来的问题。下面是uncurrying的实现方式之一。
1 2 3 4 5 6 7 Function .prototype .uncurrying = function ( ) { var self = this return function ( ) { var obj = Array .prototype .shift .call (arguments ); return self.apply (obj , arguments ) } }
先来看看它的作用是什么,在类数组对象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call转换为一个通用的push函数。
1 2 3 4 5 var push = Array .prototype .push .uncurrying (); (function ( ){ push ( arguments , 4 ); console .log ( arguments ); })( 1 , 2 , 3 );
通过uncurrying,将Array.prototype.push.call变成了一个通用函数,这样push的作用与Array.prototype.push相同,不仅仅局限于智能操作数组,而使用者对方法也更简洁和意图明了。现在通过push来看看调用uncurrying时发生了什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ); console .log ( obj );
函数节流(不展开说了,八股文系列)