前端js总结
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种:null
,undefined
,boolean
,number
,string
,symbol
。
其中 JS 的数字类型是浮点类型的,没有整型。并且浮点类型基于 IEEE 754标准实现,在使用中会遇到某些Bug。NaN
也属于number
类型,并且NaN
不等于自身。
对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型
leta=111//这只是字面量,不是number类型a.toString()//使用时候才会转换为对象类型
对象(Object)是引用类型,在使用过程中会遇到浅拷贝和深拷贝的问题。
leta={name:'FE'}letb=ab.name='EF'console.log(a.name)//EF#Typeof
typeof
对于基本类型,除了null
都可以显示正确的类型
typeof1//'number'typeof'1'//'string'typeofundefined//'undefined'typeoftrue//'boolean'typeofSymbol()//'symbol'typeofb//b没有声明,但是还会显示undefined
typeof
对于对象,除了函数都会显示object
typeof[]//'object'typeof{}//'object'typeofconsole.log//'function'
对于null
来说,虽然它是基本类型,但是会显示object
,这是一个存在很久了的 Bug
typeofnull//'object'
PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000
开头代表是对象,然而null
表示为全零,所以将它错误的判断为object
。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
如果我们想获得一个变量的正确类型,可以通过Object.prototype.toString.call(xx)
。这样我们就可以获得类似[Object Type]
的字符串。
leta//我们也可以这样判断undefineda===undefined//但是undefined保留字,能够在低版本浏览器被赋值letundefined=1//这样判断就会出错//所以可以用下面的方式来判断,并且代码量更少//因为void后面随便跟上一个组成表达式//返回就是undefineda===void0#类型转换#转Boolean
除了undefined
,null
,false
,NaN
,''
,0
,-0
,其他所有值都转为true
,包括所有对象。
对象在转换基本类型时,首先会调用valueOf
然后调用toString
。并且这两个方法你是可以重写的。
leta={valueOf(){return0}}#四则运算符
只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。
1+'1'//'11'2*'2'//4[1,2]+[2,1]//'1,22,1'//[1,2].toString()->'1,2'//[2,1].toString()->'2,1'//'1,2'+'2,1'='1,22,1'
对于加号需要注意这个表达式'a' + + 'b'
'a'++'b'//->"aNaN"//因为+'b'->NaN//你也许在一些代码中看到过+'1'->1#==
操作符
上图中的toPrimitive
就是对象转基本类型。
一般推荐使用===
判断两个值,但是你如果想知道一个值是不是null
,你可以通过xx == null
来比较。
这里来解析一道题目[] == ![] // -> true
,下面是这个表达式为何为true
的步骤
//[]转成true,然后取反变成false[]==false//根据第8条得出[]==ToNumber(false)[]==0//根据第10条得出ToPrimitive([])==0//[].toString()->''''==0//根据第6条得出0==0//->true#比较运算符
如果是对象,就通过toPrimitive
转换对象
如果是字符串,就通过unicode
字符索引来比较
每个函数都有prototype
属性,除了Function.prototype.bind()
,该属性指向原型。
每个对象都有__proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了[[prototype]]
,但是[[prototype]]
是内部属性,我们并不能访问到,所以使用_proto_
来访问。
对象可以通过__proto__
来寻找不属于该对象的属性,__proto__
将对象连接起来组成了原型链。
如果你想更进一步的了解原型,可以仔细阅读深度解析原型中的各个难点。
#new新生成了一个对象
链接到原型
绑定 this
返回新对象
在调用new
的过程中会发生以上四件事情,我们也可以试着来自己实现一个new
functioncreate(){//创建一个空的对象letobj=newObject()//获得构造函数letCon=[].shift.call(arguments)//链接到原型obj.__proto__=Con.prototype//绑定this,执行构造函数letresult=Con.apply(obj,arguments)//确保new出来的是个对象returntypeofresult==='object'?result:obj}
对于实例对象来说,都是通过new
产生的,无论是function Foo()
还是let a = { b : 1 }
。
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用new Object()
的方式创建对象需要通过作用域链一层层找到Object
,但是你使用字面量的方式就没这个问题。
functionFoo(){}//function就是个语法糖//内部等同于newFunction()leta={b:1}//这个字面量内部也是使用了newObject()
对于new
来说,还需要注意下运算符优先级。
functionFoo(){returnthis;}Foo.getName=function(){console.log('1');};Foo.prototype.getName=function(){console.log('2');};newFoo.getName();//->1newFoo().getName();//->2
从上图可以看出,new Foo()
的优先级大于new Foo
,所以对于上述代码来说可以这样划分执行顺序
new(Foo.getName());(newFoo()).getName();
对于第一个函数来说,先执行了Foo.getName()
,所以结果为 1;对于后者来说,先执行new Foo()
产生了一个实例,然后通过原型链找到了Foo
上的getName
函数,所以结果为 2。
instanceof
可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype
。
我们也可以试着实现一下instanceof
functioninstanceof(left,right){//获得类型的原型letprototype=right.prototype//获得对象的原型left=left.__proto__//判断对象的类型是否等于类型的原型while(true){if(left===null)returnfalseif(prototype===left)returntrueleft=left.__proto__}}#this
this
是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。
functionfoo(){console.log(this.a)}vara=2foo()varobj={a:2,foo:foo}obj.foo()//以上两者情况`this`只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况//以下情况是优先级最高的,`this`只会绑定在`c`上,不会被任何方式修改`this`指向varc=newfoo()c.a=3console.log(c.a)//还有种就是利用call,apply,bind改变this,这个优先级仅次于new
以上几种情况明白了,很多代码中的this
应该就没什么问题了,下面让我们看看箭头函数中的this
functiona(){return()=>{return()=>{console.log(this)}}}console.log(a()()())
箭头函数其实是没有this
的,这个函数中的this
只取决于他外面的第一个不是箭头函数的函数的this
。在这个例子中,因为调用a
符合前面代码中的第一个情况,所以this
是window
。并且this
一旦绑定了上下文,就不会被任何代码改变。
当执行 JS 代码时,会产生三种执行上下文
全局执行上下文
函数执行上下文
eval 执行上下文
每个执行上下文中都有三个重要的属性
变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
this
vara=10functionfoo(i){varb=20}foo()
对于上述代码,执行栈中有两个上下文:全局上下文和函数foo
上下文。
stack=[globalContext,fooContext]
对于全局上下文来说,VO 大概是这样的
globalContext.VO===globeglobalContext.VO={a:undefined,foo:<Function>,}
对于函数foo
来说,VO 不能访问,只能访问到活动对象(AO)
fooContext.VO===foo.AOfooContext.AO{i:undefined,b:undefined,arguments:<>}//arguments是函数独有的对象(箭头函数没有)//该对象是一个伪数组,有`length`属性且可以通过下标访问元素//该对象中的`callee`属性代表函数本身//`caller`属性代表函数的调用者
对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过[[Scope]]
属性查找上级变量
fooContext.[[Scope]]=[globalContext.VO]fooContext.Scope=fooContext.[[Scope]]+fooContext.VOfooContext.Scope=[fooContext.VO,globalContext.VO]
接下来让我们看一个老生常谈的例子,var
b()//callbconsole.log(a)//undefinedvara='Helloworld'functionb(){console.log('callb')}
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
b()//callbsecondfunctionb(){console.log('callbfist')}functionb(){console.log('callbsecond')}varb='Helloworld'
var
会产生很多错误,所以在 ES6中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。
对于非匿名的立即执行函数需要注意以下一点
varfoo=1(functionfoo(){foo=10console.log(foo)}())//->ƒfoo(){foo=10;console.log(foo)}
因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到foo
,但是这又个值是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。
#闭包specialObject={};Scope=specialObject+Scope;foo=newFunctionExpression;foo.[[Scope]]=Scope;specialObject.foo=foo;//{DontDelete},{ReadOnly}deleteScope[0];//removespecialObjectfromthefrontofscopechain
闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
functionA(){leta=1functionB(){console.log(a)}returnB}
你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。
经典面试题,循环中使用闭包解决var
定义函数的问题
for(vari=1;i<=5;i++){setTimeout(functiontimer(){console.log(i);},i*1000);}˝
首先因为setTimeout
是个异步函数,所有会先把循环全部执行完毕,这时候i
就是 6 了,所以会输出一堆 6。
解决办法两种,第一种使用闭包
for(vari=1;i<=5;i++){(function(j){setTimeout(functiontimer(){console.log(j);},j*1000);})(i);}
第二种就是使用setTimeout
的第三个参数
for(vari=1;i<=5;i++){setTimeout(functiontimer(j){console.log(j);},i*1000,i);}
因为对于let
来说
第三种就是使用let
定义i
了
for(leti=1;i<=5;i++){setTimeout(functiontimer(){console.log(i);},i*1000);}
因为对于let
来说,他会创建一个块级作用域,相当于
{//形成块级作用域leti=0{letii=isetTimeout(functiontimer(){console.log(i);},i*1000);}i++{letii=i}i++{letii=i}...}#深浅拷贝
leta={age:1}letb=aa.age=2console.log(b.age)//2
从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。
#浅拷贝首先可以通过Object.assign
来解决这个问题。
leta={age:1}letb=Object.assign({},a)a.age=2console.log(b.age)//1
当然我们也可以通过展开运算符(…)来解决
leta={age:1}letb={...a}a.age=2console.log(b.age)//1
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了
leta={age:1,jobs:{first:'FE'}}letb={...a}a.jobs.first='native'console.log(b.jobs.first)//native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。
#深拷贝这个问题通常可以通过JSON.parse(JSON.stringify(object))
来解决。
leta={age:1,jobs:{first:'FE'}}letb=JSON.parse(JSON.stringify(a))a.jobs.first='native'console.log(b.jobs.first)//FE
但是该方法也是有局限性的:
会忽略undefined
不能序列化函数
不能解决循环引用的对象
letobj={a:1,b:{c:2,d:3,},}obj.c=obj.bobj.e=obj.aobj.b.c=obj.cobj.b.d=obj.bobj.b.e=obj.b.cletnewObj=JSON.parse(JSON.stringify(obj))console.log(newObj)
如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数或者undefined
的时候,该对象也不能正常的序列化
leta={age:undefined,jobs:function(){},name:'yck'}letb=JSON.parse(JSON.stringify(a))console.log(b)//{name:"yck"}
你会发现在上述情况中,该方法会忽略掉函数和undefined
。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用loadash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用MessageChannel
functionstructuralClone(obj){returnnewPromise(resolve=>{const{port1,port2}=newMessageChannel();port2.onmessage=ev=>resolve(ev.data);port1.postMessage(obj);});}varobj={a:1,b:{c:b}}//注意该方法是异步的//可以处理undefined和循环引用对象constclone=awaitstructuralClone(obj);#模块化
在有 Babel 的情况下,我们可以直接使用 ES6 的模块化
//filea.jsexportfunctiona(){}exportfunctionb(){}//fileb.jsexportdefaultfunction(){}import{a,b}from'./a.js'importXXXfrom'./b.js'#CommonJS
CommonJs
是 Node 独有的规范,浏览器中使用就需要用到Browserify
解析了。
//a.jsmodule.exports={a:1}//orexports.a=1//b.jsvarmodule=require('./a.js')module.a//->log1
在上述代码中,module.exports
和exports
很容易混淆,让我们来看看大致内部实现
varmodule=require('./a.js')module.a//这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,//重要的是module这里,module是Node独有的一个变量module.exports={a:1}//基本实现varmodule={exports:{}//exports就是个空对象}//这个是为什么exports和module.exports用法相似的原因varexports=module.exportsvarload=function(module){//导出的东西vara=1module.exports=areturnmodule.exports};
再来说说module.exports
和exports
,用法其实是相似的,但是不能对exports
直接赋值,不会有任何效果。
对于CommonJS
和 ES6 中的模块化的两者区别是:
前者支持动态导入,也就是require(${path}/xx.js)
,后者目前不支持,但是已有提案
前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用导入会对渲染有很大影响
前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
后者会编译成require/exports
来执行的
AMD 是由RequireJS
提出的
//AMDdefine(['./a','./b'],function(a,b){a.do()b.do()})define(function(require,exports,module){vara=require('./a')a.doSomething()varb=require('./b')b.doSomething()})#防抖
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。因为防抖动的轮子很多,这里也不重新自己造个轮子了,直接使用 underscore 的源码来解释防抖动。
/***underscore防抖函数,返回函数连续调用时,空闲时间必须大于或等于wait,func才会执行**@param{function}func回调函数*@param{number}wait表示时间窗口的间隔*@param{boolean}immediate设置为ture时,是否立即调用函数*@return{function}返回客户调用函数*/_.debounce=function(func,wait,immediate){vartimeout,args,context,timestamp,result;varlater=function(){//现在和上一次时间戳比较varlast=_.now()-timestamp;//如果当前间隔时间少于设定时间且大于0就重新设置定时器if(last<wait&&last>=0){timeout=setTimeout(later,wait-last);}else{//否则的话就是时间到了执行回调函数timeout=null;if(!immediate){result=func.apply(context,args);if(!timeout)context=args=null;}}};returnfunction(){context=this;args=arguments;//获得时间戳timestamp=_.now();//如果定时器不存在且立即执行函数varcallNow=immediate&&!timeout;//如果定时器不存在就创建一个if(!timeout)timeout=setTimeout(later,wait);if(callNow){//如果需要立即执行函数的话通过apply执行result=func.apply(context,args);context=args=null;}returnresult;};};
整体函数实现的不难,总结一下。
对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为null
,就可以再次点击了。
对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。
#节流防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
/***underscore节流函数,返回函数连续调用时,func执行频率限定为次/wait**@param{function}func回调函数*@param{number}wait表示时间窗口的间隔*@param{object}options如果想忽略开始函数的的调用,传入{leading:false}。*如果想忽略结尾函数的调用,传入{trailing:false}*两者不能共存,否则函数不能执行*@return{function}返回客户调用函数*/_.throttle=function(func,wait,options){varcontext,args,result;vartimeout=null;//之前的时间戳varprevious=0;//如果options没传则设为空对象if(!options)options={};//定时器回调函数varlater=function(){//如果设置了leading,就将previous设为0//用于下面函数的第一个if判断previous=options.leading===false?0:_.now();//置空一是为了防止内存泄漏,二是为了下面的定时器判断timeout=null;result=func.apply(context,args);if(!timeout)context=args=null;};returnfunction(){//获得当前时间戳varnow=_.now();//首次进入前者肯定为true//如果需要第一次不执行函数//就将上次时间戳设为当前的//这样在接下来计算remaining的值时会大于0if(!previous&&options.leading===false)previous=now;//计算剩余时间varremaining=wait-(now-previous);context=this;args=arguments;//如果当前调用已经大于上次调用时间+wait//或者用户手动调了时间//如果设置了trailing,只会进入这个条件//如果没有设置leading,那么第一次会进入这个条件//还有一点,你可能会觉得开启了定时器那么应该不会进入这个if条件了//其实还是会进入的,因为定时器的延时//并不是准确的时间,很可能你设置了2秒//但是他需要2.2秒才触发,这时候就会进入这个条件if(remaining<=0||remaining>wait){//如果存在定时器就清理掉否则会调用二次回调if(timeout){clearTimeout(timeout);timeout=null;}previous=now;result=func.apply(context,args);if(!timeout)context=args=null;}elseif(!timeout&&options.trailing!==false){//判断是否设置了定时器和trailing//没有的话就开启一个定时器//并且不能不能同时设置leading和trailingtimeout=setTimeout(later,remaining);}returnresult;};};#继承
在 ES5 中,我们可以使用如下方式解决继承的问题
functionSuper(){}Super.prototype.getNumber=function(){return1}functionSub(){}lets=newSub()Sub.prototype=Object.create(Super.prototype,{constructor:{value:Sub,enumerable:false,writable:true,configurable:true}})
以上继承实现思路就是将子类的原型设置为父类的原型
在 ES6 中,我们可以通过class
语法轻松解决这个问题
classMyDateextendsDate{test(){returnthis.getTime()}}letmyDate=newMyDate()myDate.test()
但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。
如果你使用编译过得代码调用myDate.test()
你会惊奇地发现出现了报错
因为在 JS 底层有限制,如果不是由Date
构造出来的实例的话,是不能调用Date
里的函数的。所以这也侧面的说明了:ES6 中的class
继承与 ES5 中的一般继承写法是不同的。
既然底层限制了实例必须由Date
构造出来,那么我们可以改变下思路实现继承
functionMyData(){}MyData.prototype.test=function(){returnthis.getTime()}letd=newDate()Object.setPrototypeOf(d,MyData.prototype)Object.setPrototypeOf(MyData.prototype,Date.prototype)
以上继承实现思路:先创建父类实例=> 改变实例原先的_proto__
转而连接到子类的prototype
=> 子类的prototype
的__proto__
改为父类的prototype
。
通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。
#call, apply, bind 区别首先说下前两者的区别。
call
和apply
都是为了解决改变this
的指向。作用都是相同的,只是传参的方式不同。
除了第一个参数外,call
可以接收一个参数列表,apply
只接受一个参数数组。
leta={value:1}functiongetValue(name,age){console.log(name)console.log(age)console.log(this.value)}getValue.call(a,'yck','24')getValue.apply(a,['yck','24'])#模拟实现 call 和 apply
可以从以下几点来考虑如何实现
不传入第一个参数,那么默认为window
改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
Function.prototype.myCall=function(context){varcontext=context||window//给context添加一个属性//getValue.call(a,'yck','24')=>a.fn=getValuecontext.fn=this//将context后面的参数取出来varargs=[...arguments].slice(1)//getValue.call(a,'yck','24')=>a.fn('yck','24')varresult=context.fn(...args)//删除fndeletecontext.fnreturnresult}
以上就是call
的思路,apply
的实现也类似
Function.prototype.myApply=function(context){varcontext=context||windowcontext.fn=thisvarresult//需要判断是否存储第二个参数//如果存在,就将第二个参数展开if(arguments[1]){result=context.fn(...arguments[1])}else{result=context.fn()}deletecontext.fnreturnresult}
bind
和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过bind
实现柯里化。
同样的,也来模拟实现下bind
Function.prototype.myBind=function(context){if(typeofthis!=='function'){thrownewTypeError('Error')}var_this=thisvarargs=[...arguments].slice(1)//返回一个函数returnfunctionF(){//因为返回了一个函数,我们可以newF(),所以需要判断if(thisinstanceofF){returnnew_this(args,...arguments)}return_this.apply(context,args.concat(arguments))}}#Promise 实现
Promise 是 ES6 新增的语法,解决了回调地狱的问题。
可以把 Promise 看成一个状态机。初始是pending
状态,可以通过函数resolve
和reject
,将状态转变为resolved
或者rejected
状态,状态一旦改变就不能再次变化。
then
函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了pending
状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个then
调用就失去意义了。
对于then
来说,本质上可以把它看成是flatMap
//三种状态constPENDING="pending";constRESOLVED="resolved";constREJECTED="rejected";//promise接收一个函数参数,该函数会立即执行functionMyPromise(fn){let_this=this;_this.currentState=PENDING;_this.value=undefined;//用于保存then中的回调,只有当promise//状态为pending时才会缓存,并且每个实例至多缓存一个_this.resolvedCallbacks=[];_this.rejectedCallbacks=[];_this.resolve=function(value){if(valueinstanceofMyPromise){//如果value是个Promise,递归执行returnvalue.then(_this.resolve,_this.reject)}setTimeout(()=>{//异步执行,保证执行顺序if(_this.currentState===PENDING){_this.currentState=RESOLVED;_this.value=value;_this.resolvedCallbacks.forEach(cb=>cb());}})};_this.reject=function(reason){setTimeout(()=>{//异步执行,保证执行顺序if(_this.currentState===PENDING){_this.currentState=REJECTED;_this.value=reason;_this.rejectedCallbacks.forEach(cb=>cb());}})}//用于解决以下问题//newPromise(()=>throwError('error))try{fn(_this.resolve,_this.reject);}catch(e){_this.reject(e);}}MyPromise.prototype.then=function(onResolved,onRejected){varself=this;//规范2.2.7,then必须返回一个新的promisevarpromise2;//规范2.2.onResolved和onRejected都为可选参数//如果类型不是函数需要忽略,同时也实现了透传//Promise.resolve(4).then().then((value)=>console.log(value))onResolved=typeofonResolved==='function'?onResolved:v=>v;onRejected=typeofonRejected==='function'?onRejected:r=>throwr;if(self.currentState===RESOLVED){return(promise2=newMyPromise(function(resolve,reject){//规范2.2.4,保证onFulfilled,onRjected异步执行//所以用了setTimeout包裹下setTimeout(function(){try{varx=onResolved(self.value);resolutionProcedure(promise2,x,resolve,reject);}catch(reason){reject(reason);}});}));}if(self.currentState===REJECTED){return(promise2=newMyPromise(function(resolve,reject){setTimeout(function(){//异步执行onRejectedtry{varx=onRejected(self.value);resolutionProcedure(promise2,x,resolve,reject);}catch(reason){reject(reason);}});}));}if(self.currentState===PENDING){return(promise2=newMyPromise(function(resolve,reject){self.resolvedCallbacks.push(function(){//考虑到可能会有报错,所以使用try/catch包裹try{varx=onResolved(self.value);resolutionProcedure(promise2,x,resolve,reject);}catch(r){reject(r);}});self.rejectedCallbacks.push(function(){try{varx=onRejected(self.value);resolutionProcedure(promise2,x,resolve,reject);}catch(r){reject(r);}});}));}};//规范2.3functionresolutionProcedure(promise2,x,resolve,reject){//规范2.3.1,x不能和promise2相同,避免循环引用if(promise2===x){returnreject(newTypeError("Error"));}//规范2.3.2//如果x为Promise,状态为pending需要继续等待否则执行if(xinstanceofMyPromise){if(x.currentState===PENDING){x.then(function(value){//再次调用该函数是为了确认xresolve的//参数是什么类型,如果是基本类型就再次resolve//把值传给下个thenresolutionProcedure(promise2,value,resolve,reject);},reject);}else{x.then(resolve,reject);}return;}//规范2.3.3.3.3//reject或者resolve其中一个执行过得话,忽略其他的letcalled=false;//规范2.3.3,判断x是否为对象或者函数if(x!==null&&(typeofx==="object"||typeofx==="function")){//规范2.3.3.2,如果不能取出then,就rejecttry{//规范2.3.3.1letthen=x.then;//如果then是函数,调用x.thenif(typeofthen==="function"){//规范2.3.3.3then.call(x,y=>{if(called)return;called=true;//规范2.3.3.3.1resolutionProcedure(promise2,y,resolve,reject);},e=>{if(called)return;called=true;reject(e);});}else{//规范2.3.3.4resolve(x);}}catch(e){if(called)return;called=true;reject(e);}}else{//规范2.3.4,x为基本类型resolve(x);}}
以上就是根据 Promise / A+ 规范来实现的代码,可以通过promises-aplus-tests
的完整测试
Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程
//使用*表示这是一个Generator函数//内部可以通过yield暂停代码//通过调用next恢复执行function*test(){leta=1+2;yield2;yield3;}letb=test();console.log(b.next());//>{value:2,done:false}console.log(b.next());//>{value:3,done:false}console.log(b.next());//>{value:undefined,done:true}
从以上代码可以发现,加上*
的函数执行后拥有了next
函数,也就是说函数执行后返回了一个对象。每次调用next
函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现
//cb也就是编译过的test函数functiongenerator(cb){return(function(){varobject={next:0,stop:function(){}};return{next:function(){varret=cb(object);if(ret===undefined)return{value:undefined,done:true};return{value:ret,done:false};}};})();}//如果你使用babel编译后可以发现test函数变成了这样functiontest(){vara;returngenerator(function(_context){while(1){switch((_context.prev=_context.next)){//可以发现通过yield将代码分割成几块//每次执行next函数就执行一块代码//并且表明下次需要执行哪块代码case0:a=1+2;_context.next=4;return2;case4:_context.next=6;return3;//执行完毕case6:case"end":return_context.stop();}}});}#Map、FlapMap 和 Reduce
Map
作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后append
到新的数组中。
[1,2,3].map((v)=>v+1)//->[2,3,4]
Map
有三个参数,分别是当前索引元素,索引,原数组
['1','2','3'].map(parseInt)//parseInt('1',0)->1//parseInt('2',1)->NaN//parseInt('3',2)->NaN
FlapMap
和map
的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将FlapMap
看成是map
+flatten
,目前该函数在浏览器中还不支持。
[1,[2],3].flatMap((v)=>v+1)//->[2,3,4]
如果想将一个多维数组彻底的降维,可以这样实现
constflattenDeep=(arr)=>Array.isArray(arr)?arr.reduce((a,b)=>[...flattenDeep(a),...flattenDeep(b)],[]):[arr]flattenDeep([1,[[2],[3,[4]],5]])
Reduce
作用是数组中的值组合起来,最终得到一个值
functiona(){console.log(1);}functionb(){console.log(2);}[a,b].reduce((a,b)=>a(b()))//->21#async 和 await
一个函数如果加上async
,那么该函数就会返回一个Promise
asyncfunctiontest(){return"1";}console.log(sync());//->Promise{<resolved>:"1"}
可以把async
看成将函数返回值使用Promise.resolve()
包裹了下。
await
只能在async
函数中使用
functionsleep(){returnnewPromise(resolve=>{setTimeout(()=>{console.log('finish')resolve("sleep");},2000);});}asyncfunctiontest(){letvalue=awaitsleep();console.log("object");}test()
上面代码会先打印finish
然后再打印object
。因为await
会等待sleep
函数resolve
,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。
async 和 await
相比直接使用Promise
来说,优势在于处理then
的调用链,能够更清晰准确的写出代码。缺点在于滥用await
可能会导致性能问题,因为await
会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
下面来看一个使用await
的代码。
vara=0varb=async()=>{a=a+await10console.log('2',a)//->'2'10a=(await10)+aconsole.log('3',a)//->'3'20}b()a++console.log('1',a)//->'1'1
对于以上代码你可能会有疑惑,这里说明下原理
首先函数b
先执行,在执行到await 10
之前变量a
还是 0,因为在await
内部实现了generators
,generators
会保留堆栈中东西,所以这时候a = 0
被保存了下来
因为await
是异步操作,所以会先执行console.log('1', a)
这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候a = 10
然后后面就是常规执行代码了
#ProxyProxy 是 ES6 中新增的功能,可以用来自定义对象中的操作
letp=newProxy(target,handler);//`target`代表需要添加代理的对象//`handler`用来自定义对象中的操作
可以很方便的使用 Proxy 来实现一个数据绑定和监听
letonWatch=(obj,setBind,getLogger)=>{lethandler={get(target,property,receiver){getLogger(target,property)returnReflect.get(target,property,receiver);},set(target,property,value,receiver){setBind(value);returnReflect.set(target,property,value);}};returnnewProxy(obj,handler);};letobj={a:1}letvalueletp=onWatch(obj,(v)=>{value=v},(target,property)=>{console.log(`Get'${property}'=${target[property]}`);})p.a=2//bind`value`to`2`p.a//->Get'a'=2#为什么 0.1 + 0.2 != 0.3
因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。
我们都知道计算机表示十进制是采用二进制表示的,所以0.1
在二进制表示为
//(0011)表示循环0.1=2^-4*1.10011(0011)
那么如何得到这个二进制的呢,我们可以来演算下
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出0.1 = 2^-4 * 1.10011(0011)
,那么0.2
的演算也基本如上所示,只需要去掉第一步乘法,所以得出0.2 = 2^-3 * 1.10011(0011)
。
回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为0.1
和0.2
都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。
所以2^-4 * 1.10011...001
进位后就变成了2^-4 * 1.10011(0011 * 12次)010
。那么把这两个二进制加起来会得出2^-2 * 1.0011(0011 * 11次)0100
, 这个值算成十进制就是0.30000000000000004
下面说一下原生解决办法,如下代码所示
parseFloat((0.1+0.2).toFixed(10))#正则表达式#元字符元字符 作用 .匹配任意字符除了换行符[]匹配方括号内的任意字符。比如 [0-9] 就可以用来匹配任意数字^^9,这样使用代表匹配以 9 开头。[^
9],这样使用代表不匹配方括号内除了 9 的字符{1, 2}匹配 1 到 2 位字符(yck)只匹配和 yck 相同字符串|匹配 | 前后任意字符\转义*只匹配出现 -1 次以上 * 前的字符+只匹配出现 0 次以上 + 前的字符?? 之前字符可选#修饰语修饰语 作用 i忽略大小写g全局搜索m多行#字符简写简写 作用 \w匹配字母数字或下划线或汉字\W和上面相反\s匹配任意的空白符\S和上面相反\d匹配数字\D和上面相反\b匹配单词的开始或结束\B和上面相反
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。