刘小夕:
1.基本类型有哪几种?null是对象吗?基本数据类型和复杂数据类型存储有什么区别?
基本类型有6种,分别是undefined,null,bool,string,number,symbol(ES6新增)。
虽然typeofnull返回的值是object,但是null不是对象,而是基本数据类型的一种。
基本数据类型存储在栈内存,存储的是值。
复杂数据类型的值存储在堆内存,地址(指向堆中的值)存储在栈内存。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。
2.typeof是否正确判断类型?instanceof呢?instanceof的实现原理是什么?
首先typeof能够正确的判断基本数据类型,但是除了null,typeofnull输出的是对象。
但是对象来说,typeof不能正确的判断其类型,typeof一个函数可以输出'function',而除此之外,输出的全是object,这种情况下,我们无法准确的知道对象的类型。
instanceof可以准确的判断复杂数据类型,但是不能正确判断基本数据类型。
instanceof是通过原型链判断的,AinstanceofB,在A的原型链中层层查找,是否有原型等于,如果一直找到A的原型链的顶端(null;即Object.__proto__.__proto__),仍然不等于,那么返回false,否则返回true.
instanceof的实现代码:
//LinstanceofRfunctioninstance_of(L,R){//L表示左表达式,R表示右表达式varO=;//取R的显式原型L=L.__proto__;//取L的隐式原型while(true){if(L===null)//已经找到顶层returnfalse;if(O===L)//当O严格等于L时,返回truereturntrue;L=L.__proto__;//继续向上一层原型链查找}}3.forof,forin和forEach,map的区别。
forof循环:具有iterator接口,就可以用forof循环遍历它的成员(属性值)。forof循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象、Generator对象,以及字符串。forof循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。对于普通的对象,forof结构不能直接使用,会报错,必须部署了Iterator接口后才能使用。可以中断循环。
forin循环:遍历对象自身的和继承的可枚举的属性,不能直接获取属性值。可以中断循环。
forEach:只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
map:只能遍历数组,不能中断,返回值是修改后的数组。
PS:():返回给定对象所有可枚举属性的字符串数组。
关于forEach是否会改变原数组的问题,有些小伙伴提出了异议,为此我写了代码测试了下(注意数组项是复杂数据类型的情况)。除了forEach之外,map等API,也有同样的问题。
letarry=[1,2,3,4];((item)={item*=10;});(arry);//[1,2,3,4]((item)={arry[1]=10;//直接操作数组});(arry);//[1,10,3,4]letarry2=[{name:"Yve"},{age:20}];((item)={=10;});(arry2);//[{name:10},{age:20,name:10}]如还不了解iterator接口或forof,请先阅读ES6文档:Iterator和forof循环
更多细节请戳:/YvetteLau/B…
4.如何判断一个变量是不是数组?
使用判断,如果返回true,说明是数组
使用instanceofArray判断,如果返回true,说明是数组
使用判断,如果值是[objectArray],说明是数组
通过constructor来判断,如果是数组,那么===Array.(不准确,因为我们可以指定=Array)
functionfn(){((arguments));//false;因为arguments是类数组,但不是数组(([1,2,3,4]));//(argumentsinstanceofArray);//([1,2,3,4]instanceofArray);//((arguments));//[objectArguments](([1,2,3,4]));//[objectArray](===Array);//=Array;(===Array);//((arguments));//false}fn(1,2,3,4);5.类数组和数组的区别是什么?
1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理);
2)不具有数组所具有的方法;
类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组有:函数的参数arguments,DOM对象列表(比如通过得到的列表),jQuery对象(比如$("div")).
类数组可以转换为数组:
//第一种方法(arrayLike,start);//第二种方法[arrayLike];//第三种方法:(arrayLike);
PS:任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
方法用于将两类对象转为真正的数组:类似数组的对象(array-likeobject)和可遍历(iterable)的对象。
6.==和===有什么区别?
===不需要进行类型转换,只有类型相同并且值相等时,才返回true.
==如果两者类型不同,首先需要进行类型转换。具体流程如下:
首先判断两者类型是否相同,如果相等,判断值是否相等.
如果类型不同,进行类型转换
判断比较的是否是null或者是undefined,如果是,返回true.
判断两者类型是否为string和number,如果是,将字符串转换成number
判断其中一方是否为boolean,如果是,将boolean转为number再进行判断
判断其中一方是否为object且另一方为string、number或者symbol,如果是,将object转为原始类型再进行判断
letperson1={age:25}letperson2=person1;=20;(person1===person2);//true,注意复杂数据类型,比较的是引用地址思考:[]==![]
我们来分析一下:[]==![]是true还是false?
首先,我们需要知道!优先级是高于==(更多运算符优先级可查看:运算符优先级)
![]引用类型转换成布尔值都是true,因此![]的是false
根据上面的比较步骤中的第五条,其中一方是boolean,将boolean转为number再进行判断,false转换成number,对应的值是0.
根据上面比较步骤中的第六条,有一方是number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,那么转成number就是这个数字,其它情况,均为NaN)
0==0;为true
7.ES6中的class和ES5的类有什么区别?
ES6class内部所有定义的方法都是不可枚举的;
ES6class必须使用new调用;
ES6class不存在变量提升;
ES6class默认即是严格模式;
ES6class子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。
8.数组的哪些API会改变原数组?
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
slice/map/forEach/every/filter/reduce/entries/find
注:数组的每一项是简单数据类型,且未直接操作数组的情况下(稍后会对此题重新作答)。
9.let、const以及var的区别是什么?
let和const定义的变量不会出现变量提升,而var定义的变量会提升。
let和const是JS中的块级作用域
let和const不允许重复声明(会抛出错误)
let和const定义的变量在定义语句之前,如果使用会抛出错误(形成了暂时性死区),而var不会。
const声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
10.在JS中什么是变量提升?什么是暂时性死区?
变量提升就是变量在声明之前就可以使用,值为undefined。
在代码块内,使用let/const命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着typeof不再是一个百分百安全的操作。
typeofx;//ReferenceError(暂时性死区,抛错)letx;复制代码typeofy;//值是undefined,不会报错
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
11.如何正确的判断this?箭头函数的this是什么?
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.
函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象【前提是构造函数中没有返回对象或者是function,否则this指向返回的对象/function】。
函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是()
如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
箭头函数没有自己的this,它的this继承于上一层代码块的this。
测试下是否已经成功Get了此知识点(浏览器执行环境):
varnumber=5;varobj={number:3,fn1:(function(){varnumber;*=2;number=number*2;number=3;returnfunction(){varnum=;*=2;(num);number*=3;(number);}})()}varfn1=;(null);();();12.词法作用域和this的区别。
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
this是在调用时被绑定的,this指向什么,完全取决于函数的调用位置.
13.谈谈你对JS执行上下文栈和作用域链的理解。
执行上下文就是当前JavaScript代码被解析和执行时所在环境,JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。
JavaScript执行在单线程上,所有的代码都是排队执行。
浏览器的JS执行引擎总是访问栈顶的执行上下文。
全局上下文只有唯一的一个,它在浏览器关闭时出栈。
作用域链:无论是LHS还是RHS查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。
14.什么是闭包?闭包的作用是什么?闭包有哪些使用场景?
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。
闭包的作用有:
封装私有变量
模仿块级作用域(ES5中没有块级作用域)
实现JS的模块
15.call、apply有什么区别?call,aplly和bind的内部是如何实现的?
call和apply的功能相同,区别在于传参的方式不一样:
(obj,arg1,arg2,),调用一个函数,具有一个指定的this值和分别地提供的参数(参数的列表)。
(obj,[argsArray]),调用一个函数,具有一个指定的this值,以及作为一个数组(或类数组对象)提供的参数。
将函数设为传入参数的属性
指定this到函数并传入给定参数执行函数
如果不传入参数或者参数为null,默认指向为window/global
删除参数上的函数
=function(context){/**如果第一个参数传入的是null或者是undefined,那么指向this指向window/global*//**如果第一个参数传入的不是null或者是undefined,那么必须是一个对象*/if(!context){//context为null或者是undefinedcontext=typeofwindow==='undefined'?global:window;}=this;//this指向的是当前的函数(Function的实例)letrest=[arguments].slice(1);//获取除了this指向对象以外的参数,空数组slice后返回的仍然是空数组letresult=(rest);//隐式绑定,当前函数的this指向了;returnresult;}//测试代码varfoo={name:'Selina'}varname='Chirs';functionbar(job,age){();(job,age);}(foo,'programmer',20);//(null,'teacher',25);//浏览器环境:Chirsteacher25;node环境:undefinedteacher25apply的实现和call很类似,但是需要注意他们的参数是不一样的,apply的第二个参数是数组或类数组.
=function(context,rest){if(!context){//context为null或者是undefined时,设置默认值context=typeofwindow==='undefined'?global:window;}=this;letresult;if(rest===undefined||rest===null){//undefined或者是null不是Iterator对象,不能被result=(rest);}elseif(typeofrest==='object'){result=(rest);};returnresult;}varfoo={name:'Selina'}varname='Chirs';functionbar(job,age){();(job,age);}(foo,['programmer',20]);//(null,['teacher',25]);//浏览器环境:Chirsprogrammer20;node环境:undefinedteacher25bind和call/apply有一个很重要的区别,一个函数被call/apply的时候,会直接调用,但是bind会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数。
=function(context){if(typeofthis!=="function"){thrownewTypeError("notafunction");}letself=this;letargs=[arguments].slice(1);functionFn(){};=;letbound=function(){letres=[args,arguments];//bind传递的参数和函数调用时传递的参数拼接context=thisinstanceofFn?this:context||this;(context,res);}//原型链=newFn();returnbound;}varname='Jack';functionperson(age,job,ger){(,age,job,ger);}varYve={name:'Yvette'};letresult=(Yve,22,'enginner')('female');16.new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?
new:
创建一个新对象。
这个新对象会被执行[[原型]]连接。
属性和方法被加入到this引用的对象中。并执行了构造函数中的方法.
如果函数没有返回其他对象,那么this指向这个新对象,否则this指向构造函数中返回的对象。
functionnew(func){lettarget={};target.__proto__=;letres=(target);if(restypeof(res)=="object"||typeof(res)=="function"){returnres;}returntarget;}字面量创建对象,不会调用Object构造函数,简洁且性能更好;
newObject()方式创建对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。
通过对象字面量定义对象时,不会调用Object构造函数。
17.谈谈你对原型的理解?
在JavaScript中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
18.什么是原型链?【原型链解决的是什么问题?】
原型链解决的主要是继承问题。
每个对象拥有一个原型对象,通过proto(读音:dunderproto)指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null(__proto__指向的是null)。这种关系被称为原型链(prototypechain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。
构造函数Parent、和实例p的关系如下:(p.__proto__===)
19.prototype和__proto__区别是什么?
prototype是构造函数的属性。
__proto__是每个实例都有的属性,可以访问[[prototype]]属性。
实例的__proto__与其构造函数的prototype指向的是同一个对象。
functionStudent(name){=name;}=function(){=20;}letJack=newStudent('jack');(Jack.__proto__);//((Jack));;();(Jack.__proto__===);//true20.使用ES5实现一个继承?
functionSuperType(name){=name;=['red','blue','green'];}=function(){();}functionSubType(name,age){(this,name);=age;}=newSuperType();=SubType;=function(){();}其它继承方式实现,可以参考《JavaScript高级程序设计》
21.什么是深拷贝?深拷贝和浅拷贝有什么区别?
浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
实现一个深拷贝:
functiondeepClone(obj){//递归拷贝if(obj===null)returnnull;//null的情况if(objinstanceofRegExp)returnnewRegExp(obj);if(objinstanceofDate)returnnewDate(obj);if(typeofobj!=='object'){//如果不是复杂数据类型,直接返回returnobj;}/***如果obj是数组,那么是[Function:Array]*如果obj是对象,那么是[Function:Object]*/lett=();for(letkeyinobj){//如果obj[key]是复杂数据类型,递归t[key]=deepClone(obj[key]);}returnt;}22.防抖和节流的区别是什么?防抖和节流的实现。
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。
functiondebounce(func,wait,immediate=true){lettimer;//延迟执行函数constlater=(context,args)=setTimeout(()={timer=null;//倒计时结束if(!immediate){(context,args);//执行回调context=args=null;}},wait);letdebounced=function(params){letcontext=this;letargs=params;if(!timer){timer=later(context,args);if(immediate){//立即执行(context,args);}}else{clearTimeout(timer);//函数在每个等待时延的结束被调用timer=later(context,args);}}=function(){clearTimeout(timer);timer=null;};returndebounced;};防抖的应用场景:
每次resize/scroll触发统计事件
文本输入的验证(连续输入文字后发送AJAX请求进行验证,验证一次就好)
//(func,wait,options){vartimeout,context,args,result;varprevious=0;if(!options)options={};varlater=function(){previous====false?0:()||newDate().getTime();timeout=null;result=(context,args);if(!timeout)context=args=null;};varthrottled=function(){varnow=()||newDate().getTime();if(!===false)previous=now;varremaining=wait-(now-previous);context=this;args=arguments;if(remaining=0||remainingwait){if(timeout){clearTimeout(timeout);timeout=null;}previous=now;result=(context,args);if(!timeout)context=args=null;}elseif(!!==false){//判断是否设置了定时器和trailingtimeout=setTimeout(later,remaining);}returnresult;};=function(){clearTimeout(timeout);previous=0;timeout=context=args=null;};returnthrottled;};函数节流的应用场景有:
DOM元素的拖拽功能实现(mousemove)
射击游戏的mousedown/keydown事件(单位时间只能发射一颗子弹)
计算鼠标移动的距离(mousemove)
Canvas模拟画板功能(mousemove)
搜索联想(keyup)
监听滚动事件判断是否到页面底部自动加载更多:给scroll加了debounce后,只有用户停止滚动后,才会判断是否到了页面底部;如果是throttle的话,只要页面滚动就会间隔一段时间判断一次
23.取数组的最大值(ES5、ES6)
//ES5的写法(null,[14,3,77,30]);//ES6的写法([14,3,77,30]);//reduce[14,3,77,30].reduce((accumulator,currentValue)={returnaccumulator=accumulatorcurrentValue?accumulator:currentValue});24.ES6新的特性有哪些?
新增了块级作用域(let,const)
提供了定义类的语法糖(class)
新增了一种基本数据类型(Symbol)
新增了变量的解构赋值
函数参数允许设置默认值,引入了rest参数,新增了箭头函数
数组新增了一些API,如isArray/from/of方法;数组实例新增了entries(),keys()和values()等方法
对象和数组新增了扩展运算符
ES6新增了模块化(import/export)
ES6新增了Set和Map数据结构
ES6原生提供Proxy构造函数,用来生成Proxy实例
ES6新增了生成器(Generator)和遍历器(Iterator)
25.setTimeout倒计时为什么会出现误差?
setTimeout()只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在setTimeout()指定的时间执行。所以,setTimeout()的第二个参数表示的是最少时间,并非是确切时间。
HTML5标准规定了setTimeout()的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用requestAnimationFrame()的效果要好于setTimeout();
26.为什么0.1+0.2!=0.3?
0.1+0.2!=0.3是因为在进制转换和进阶运算的过程中出现精度损失。
下面是详细解释:
JavaScript使用Number类型表示数字(整数和浮点数),使用64位表示一个数字。
图片说明:
第0位:符号位,0表示正数,1表示负数(s)
第1位到第11位:储存指数部分(e)
第12位到第63位:储存小数部分(即有效数字)f
计算机无法直接对十进制的数字进行运算,需要先对照IEEE754规范转换成二进制,然后对阶运算。
0.1和0.2转换成二进制后会无限循环
0.1-0.0001100110011001(无限循环)0.2-0.0011001100110011(无限循环)
但是由于IEEE754尾数位数限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。
由于指数位数不相同,运算时需要对阶运算这部分也可能产生精度损失。
按照上面两步运算(包括两步的精度损失),最后的结果是
0.0100110011001100110011001100110011001100110011001100
结果转换成十进制之后就是0.30000000000000004。
27.promise有几种状态,Promise有什么优缺点?
promise有三种状态:fulfilled,rejected,ping.
一旦状态改变,就不会再变,任何时候都可以得到这个结果
可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
无法取消Promise
当处于ping状态时,无法得知目前进展到哪一个阶段
28.Promise构造函数是同步还是异步执行,then中的方法呢?promise如何实现then处理?
Promise的构造函数是同步执行的。then中的方法是异步执行的。
29.Promise和setTimeout的区别?
Promise是微任务,setTimeout是宏任务,同一个事件循环中,总是先于setTimeout执行。
30.如何实现?
要实现,首先我们需要知道的功能:
如果传入的参数是一个空的可迭代对象,那么此promise对象回调完成(resolve),只有此情况,是同步执行的,其它都是异步返回的。
如果传入的参数不包含任何promise,则返回一个异步完成.promises中所有的promise都“完成”时或参数中不包含promise时回调完成。
如果参数中有一个promise失败,那么返回的promise对象失败
在任何情况下,返回的promise的完成状态的结果都是一个数组
=function(promises){returnnewPromise((resolve,reject)={letindex=0;letresult=[];if(===0){resolve(result);}else{functionprocessValue(i,data){result[i]=data;if(++index===){resolve(result);}}for(leti=0;;i++){//promises[i]可能是普通值(promises[i]).then((data)={processValue(i,data);},(err)={reject(err);return;});}}});}31.如何实现?
不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then。并且会将值原封不动的传递给后面的then.
=function(callback){((value)={(callback()).then(()={returnvalue;});},(err)={(callback()).then(()={throwerr;});});}32.什么是函数柯里化?实现sum(1)(2)(3)返回结果是1,2,3之和。
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
functionsum(a){returnfunction(b){returnfunction(c){returna+b+c;}}}(sum(1)(2)(3));//6引申:实现一个curry函数,将普通函数进行柯里化:
functioncurry(fn,args=[]){returnfunction(){letrest=[args,arguments];if(){(this,fn,rest);}else{(this,rest);}}}//testfunctionsum(a,b,c){returna+b+c;}letsumFn=curry(sum);(sumFn(1)(2)(3));//6(sumFn(1)(2,3));//6





