Skip to content

面向对象、闭包、高阶函数 #15

Description

@isNeilLin

Chapter1 - 面向对象的Javascript

Javascript通过原型委托的方式实现继承。

动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

鸭子类型(duck typing):“如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。”

varduck={duckSinging: function(){console.log('嘎嘎嘎');}};varchicken={duckSinging: function(){console.log('嘎嘎嘎');}};varchoir=[];// 合唱团varjoinChoir=function(animal){if(animal&&typeofanimal.duckSinging==='function'){choir.push(animal);console.log('恭喜加入合唱团');console.log('合唱团已有成员数量:'+choir.length);}};joinChoir(duck);// 恭喜加入合唱团joinChoir(chicken);// 恭喜加入合唱团

多态: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态的思想实际上是把“做什么”和“谁去做”分离开来。要实现这一点,归根结底要先消除类型之间的耦合关系。

“JavaScript对象的多态性是与生俱来的: “JavaScript的变量类型在运行期是可变的。一个JavaScript对象,既可以表示Duck类型的对象,又可以表示Chicken类型的对象。某一种动物能否发出叫声,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的类型耦合。”

varmakeSound=function(animal){animal.sound();}varDuck=function(){}Duck.prototype.sound=function(){console.log('嘎嘎嘎')}varChicken=function(){}Chicken.prototype.sound=function(){console.log('咯咯咯')}makeSound(newDuck());// 嘎嘎嘎makeSound(newChicken());// 咯咯咯

封装

封装的目的是将信息隐藏。Javascript只能依赖变量的作用域来实现封装特性。
封装包括:封装数据封装实现、封装类型、封装变化

原型模式和基于原型继承的JavaScript对象系统

在以类为中心的面向对象编程语言中,对象总是从类中创建而来。而在原型编程的思想中,一个对象是通过克隆另一个对象得到的。

原型模式的实现关键是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create方法可以用来克隆对象。

varPlane=function(){this.blood=50;this.attackLevel=1;this.defenseLevel=1;}varplane=newPlane();plane.blood=500;plane.attackLevel=10;plane.defenseLevel=7;varclonePlane=Object.create(plane);console.log(clonePlane)// 输出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}// 兼容IE8Object.create=Object.create||function(obj){varF=function(){};F.prototype=obj;returnnewF();}

原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

原型编程范型至少包括以下基本规则

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住原型
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

基于原型链的委托机制就是原型继承的本质

JavaScript中的原型继承

JavaScript中的根对象是Object.prototype对象,Object.prototype对象是一个空的对象。我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype对象就是它们的原型。

varobj1=newObject();varobj2={};console.log(Object.getPrototypeOf(obj1)===Object.prototype);// 输出:trueconsole.log(Object.getPrototypeOf(obj2)===Object.prototype);// 输出:true

“在JavaScript语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Object()或者var obj2 = {}。此时,引擎内部会从Object.prototype上面克隆一个对象出来,我们最终得到的就是这个对象。”

varPerson=function(name){this.name=name;}Person.prototype.getName=function(){returnthis.name;}varp=newPerson('sven');console.log(p.name)// svenconsole.log(p.getName)// svenconsole.log(Object.getPrototypeOf(p)===Person.prototype)// true

就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给它的构造器的原型。

Javascript给对象提供了一个__proto__的隐藏属性,某个对象的__proto__属性会指向它的构造器的原型对象。即{Constructor}.prototype

对象构造器的原型并不仅限于Object.prototype上,而是可以动态的指向其他对象。这样一来,当对象a需要借用对象b的能力时,可以有选择性地把对象a的构造器的原型指向对象b,从而达到继承的效果

varobj={name: 'sven'};varA=function(){}A.prototype=obj;vara=newA();console.log(a.name);// sven

当我们期望得到一个“类”继承自另外一个“类”的效果时,往往会用下面的代码来模拟实现

varA=function(){}A.prototype={name: 'sven'};varB=function(){}B.prototype=newA()varb=newB();console.log(b.name)// sven

ECMAScript 6带来了新的Class语法。这让JavaScript看起来像是一门基于类的语言,但其背后仍是通过原型机制来创建对象。

classAnimal{constructor(name){this.name=name;}getName(){returnthis.name;}}classDogextendsAnimal{constructor(name){super(name)}speak(){return"woof";}}vardog=newDog("Scan");console.log(dog.getName()+" says: "+dog.speak());

chapter2-this、call和apply

javascript的this总是指向一个对象。具体指向哪个对象是在运行时基于函数的执行环境动态绑定的。this的指向大致可以分为四种:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • Function.prototype.callFunction.prototype.apply调用

作为对象的方法调用

当函数作为对象的方法被调用时,this指向该对象。

varobj={a: 1,getA: function(){alert(this===obj);// 输出:truealert(this.a);// 输出: 1}}

作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this总是指向全局对象。

在ECMAScript 5的strict模式下,这种情况下的this已经被规定为不会指向全局对象,而是undefined

functionfunc(){"use strict"alert(this);// 输出:undefined}func();

构造器调用

构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象。

如果构造器显式地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的this。如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成这个问题。

varMyClass=function(){this.name='sven';return{name: 'anne'}}varobj=newMyClass();console.log(obj.name);// 'anne'

Function.prototype.callFunction.prototype.apply调用

Function.prototype.callFunction.prototype.apply可以动态地改变传入函数的this.

varobj1={name: 'sven',getName: function(){returnthis.name;}};varobj2={name: 'anne'};console.log(obj1.getName());// 输出: svenconsole.log(obj1.getName.call(obj2));// 输出:anne

call和apply

apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数

varfunc=function(a,b,c){alert([a,b,c]);// 输出 [ 1, 2, 3 ]};func.apply(null,[1,2,3]);

call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数

varfunc=function(a,b,c){alert([a,b,c]);// 输出 [ 1, 2, 3 ]};func.call(null,1,2,3);

当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象。但如果是在严格模式下,函数体内的this还是为null

使用callapply借用其他对象的方法

Math.max.apply(null,[6,4,8,1,2,7])// 8

call和apply的实际用途

改变this指向

varobj1={name: 'sven'};varobj2={name: 'anne'};window.name='window';vargetName=function(){alert(this.name);};getName();// 输出: windowgetName.call(obj1);// 输出: svengetName.call(obj2);// 输出: anne

Function.prototype.bind

模拟Function.prototype.bind的实现

Function.prototype.bind=function(){varself=this,context=[].shift.call(arguments),args=[].slice.call(arguments);returnfunction(){returnself.apply(context,[].concat.call(args,[].slice.call(arguments)))}}varobj={name: 'sven'}varfunc=function(a,b,c,d){console.log(this.name)// svenconsole.log([a,b,c,d])// [1,2,3,4]}.bind(obj,1,2);func(3,4)

借用其他对象的方法

(function(){Array.prototype.push.call(arguments,3);console.log(arguments);// 输出[1,2,3]})(1,2);

闭包

封装变量

varmult=(function(){varcache={};returnfunction(){varargs=[].join.call(arguments,',');if(cache[args]){returncache[args]}vara=1;for(vari=0,l=arguments.length;i<l;i++){a=a*arguments[i];}returncache[args]=a;}})()console.log(mult(1,2,3))// 6console.log(mult(1,2,3))// 6

提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数有一个良好的命名,它们本身也起到了注释的作用。如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来。

varmult=(function(){varcache={};varcalculate=function(){vara=1;for(vari=0,l=arguments.length;i<l;i++){a=a*arguments[i];}returna;}returnfunction(){varargs=[].join.call(arguments,',');if(cache[args]){returncache[args]}returncache[args]=calculate.apply(null,arguments);}})()

延续局部变量的寿命

使用img进行数据上报

varreport=(function(){varimgs=[];returnfunction(){varimg=newImage();imgs.push(img);img.src=src;}})()

面向对象实现命令模式

<html><body><buttonid="execute">execute</button><buttonid="undo">undo</button><script>varTv={open: function(){console.log('open tv');},close: function(){console.log('close tv');}}varOpenTvCommand=function(receiver){this.receiver=receiver;}OpenTvCommand.prototype.execute=function(){this.receiver.open();}OpenTvCommand.prototype.close=function(){this.receiver.close();}varsetCommand=function(command){document.getElementById('execute').onclick=function(){command.execute();}document.getElementById('undo').onclick=function(){command.close();}}setCommand(newOpenTvCommand(Tv))</script></body></html>

判断一个数据是否是数组,在以往的实现中,可以基于鸭子类型的概念来判断,比如判断这个数据有没有length属性,有没有sort方法或者slice方法等。但更好的方式是用Object.prototype.toString来计算。Object.prototype.toString.call( obj )返回一个字符串,比如Object.prototype.toString.call( [1,2,3] )总是返回"[object Array]",而Object.prototype.toString.call( "str")总是返回"[object String]"。

varisType=function(type){returnfunction(obj){returnObject.prototype.toString.call(obj)==='[object '+type+']';}}varisString=isType('String');varisNumber=isType('Number');varisArray=isType('Array');console.log(isArray([1,2,3,4]));// true

JavaScript实现AOP(面向切面编程)

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。

Function.prototype.before=function(beforeFn){var__self=this;returnfunction(){beforeFn.apply(this,arguments);return__self.apply(this,arguments);}}Function.prototype.after=function(afterFn){var__self=this;returnfunction(){varret=__self.apply(this,arguments);afterFn.apply(this,arguments);returnret;}}varfunc=function(){console.log(2);};func=func.before(function(){console.log(1);}).after(function(){console.log(3);});**JavaScript实现AOP(面向切面编程)**AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。```javascriptFunction.prototype.before=function(beforeFn){var__self=this;returnfunction(){beforeFn.apply(this,arguments);return__self.apply(this,arguments);}}Function.prototype.after=function(afterFn){var__self=this;returnfunction(){varret=__self.apply(this,arguments);afterFn.apply(this,arguments);returnret;}}varfunc=function(){console.log(2);};func();

函数柯里化

currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

varcurring=function(fn){varargs=[];returnfunction(){if(arguments.length===0){fn.apply(this,args)}else{**JavaScript实现AOP(面向切面编程)**AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。```javascriptFunction.prototype.before=function(beforeFn){var__self=this;returnfunction(){beforeFn.apply(this,arguments);return__self.apply(this,arguments);}}Function.prototype.after=function(afterFn){var__self=this;returnfunction(){varret=__self.apply(this,arguments);afterFn.apply(this,arguments);returnret;}}varfunc=function(){consol[].push.call(args,arguments);returnarguments.callee;}}}varcost=(function(){varmoney=0;returnfunction(){for(vari=0,l=arguments.length;i<l;i++){money+=arguments[i];}returnmoney;}})();varcost=currying(cost);cost(100);// 未真正求值cost(200);// 未真正求值cost(300);// 未真正求值console.log(cost());// 求值并输出:600

反柯里化(uncurrying)的实现

Function.prototype.uncurrying=function(){varself=this;// self就是Array.prototype.pushreturnfunction(){// Array.prototype.push当做Function.prototype.call的this传进去returnFunction.prototype.call.apply(self,arguments)// arguments是[obj,'first'],apply接受数组形式的参数// 接下来被直接传入Function.prototype.call,call接受以逗号分隔的N个参数的形式// 所以arguments中的第一个参数会被当做call里面的this,其他参数一次传入call// 整句代码相当于 Array.prototype.push.call(obj,'first');}}varpush=Array.prototype.push.uncurrying();varobj={length: 1,a: 'test'};push(obj,‘first’);

函数节流

varthrottle=function(fn,interval){varself=fn,timer,firstTime=true;returnfunction(){var_this=this,args=arguments;if(firstTime){self.apply(_this,args)returnfirstTime=false;}if(timer){returnfalse;}timer=setTimeout(function(){clearTimeout(timer);timer=null;self.apply(_this,args)},interval||500)}}

Metadata

Metadata

Assignees

No one assigned

    Labels

    JSJavaScript内容

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions