基础题
变量赋值
function a (b = c, c = 1) {console.log(b, c)}a()// 报错
函数多个参数设置默认值,跟按顺序定义变量一样,存在暂时性死区。前面定义的变量不能引用后面还未定义的变量。
let a = b = 10// 上句写法等同于 let a = 10; b = 10;;(function(){let a = b = 20; // 等同于 let a = 20; b = 20;})()console.log(a)console.log(b)// 10 20
var a = {n:1}var b = aa.x = a = {n:2} // . 操作符运算等级最高 a.x = {n:1,x:undefined},// 然后按照连等操作从右到左执行代码,a = {n: 2}// a指向新对象,之后a.x = a,由于a.x在最开始执行过// 此时等价于({n: 1, x: undefined}).x = b.x = a = {n: 2}console.log(a.x) //undefinedconsole.log(b.x) // {n:2}
变量提升和作用域
var scope = 'global scope'function a(){function b(){console.log(scope)}return bvar scope = 'local scope'; // var会存在变量提升,如果是let,则会报错。}a()() // 输出undefined
console.log(a)var a = 1var getNum = function() {a = 2}function getNum() {a = 3}console.log(a)getNum()console.log(a)// 答案 undefined 1 2
var i = 1function b() {console.log(i) //使用的是全局作用域下的值}function a() {var i = 2b()}a();// 结果输出 1
变量声明和函数声明同时存在
var a = 1function a(){}console.log(a)// 第一行的变量声明会被函数声明覆盖 FnA varA,执行到console.log时,又在之前进行了赋值a=1var bfunction b(){}console.log(b) // 打印出b函数function b(){}var bconsole.log(b) // 打印出b函数
输出结果: 1
[Function: b]
[Function: b]
function Foo() {getName = function () { console.log(1) }return this; //this表示的是window}Foo.getName = function () { console.log(2) }Foo.prototype.getName = function () { console.log(3) }var getName = function () { console.log(4) }function getName() { console.log(5) }//请写出以下输出结果:Foo.getName()getName()Foo().getName()getName()new Foo.getName()new Foo().getName()new new Foo().getName()
分析:这是一道综合性题目,首先getName函数声明会先提升,然后getName函数表达式提升,但是因为函数声明提升在线,所以忽略函数表达式的提升,然后开始执行代码,执行到var getName= …时,修改了getName的值,赋值成了打印4的新函数。
- 执行Foo函数的静态方法,打印出2。
- 执行getName,当前getName是打印出4的那个函数
- 执行Foo函数,修改了全局变量getName,赋值成了打印1的函数,然后返回this,因为是在全局环境下执行,所以this指向window,因为getName已经被修改了,所以打印出1。
- 因为getName没有被重新赋值,所以再执行仍然打印出1。
- new操作符是用来调用函数的,所以new Foo.getName()相当于new (Foo.getName)(),所以new的是Foo的静态方法getName,打印出2。
- 因为点运算符(.)的优先级和new是一样高的,所以从左往右执行,相当于(new Foo()).getName(),对Foo使用new调用会返回一个新创建的对象,然后执行该对象的getName方法,该对象本身并没有该方法,所以会从Foo的原型对象上查找,找到了,所以打印出3
- 点运算符(.)的优先级和new一样高,另外new是用来调用函数的,所以new new Foo().getName()相当于new ((new Foo()).getName)(),括号里面的就是上一题,所以最后找到的是Foo原型上的方法,无论是直接调用,还是通过new调用,都会执行该方法,所以打印出3
结果:2、4、1、1、2、3、3
Es6+新语法
Object.assign
console.log(Object.assign([1, 2, 3], [4, 5])); // [4,5,3]
const obj = {a: {a: 1}};const obj1 = {a: {b: 1}};// assign方法执行的是浅拷贝console.log(Object.assign(obj, obj1)) //{a: { b: 1}}
for … of
var arr = [0, 1, 2]arr[10] = 10// filter ES6之前的遍历方法都会跳过数组未赋值过的位置console.log(arr.filter(a=>a === undefined)); // []// for of为ES6新增方法,不会跳过undefined的位置for(let x of arr){console.log(arr[x], x);}
类型转换
console.log(1 + NaN)console.log("1" + 3)console.log(1 + undefined)console.log(1 + null)console.log(1 + {})console.log(1 + [])console.log([] + {})// console.log([].toString()) //为空// console.log({}.toString()) //[object Object]
答案:NaN、13、NaN、1、1[object Object]、1、[object Object]
- 如果有一个操作数是字符串,那么把另一个操作数转成字符串执行连接
- 如果有一个操作数是对象,那么调用对象的valueOf方法转成原始值,如果没有该方法或调用后仍是非原始值,则调用toString方法
- 其他情况下,两个操作数都会被转成数字执行加法操作
let a = {},b={key:'b'},c={key:'c'}a[b]=123a[c]=456console.log(a[b]) // 456
console.log(typeof undefined == typeof NULL)console.log(typeof function () {} == typeof class {})
this指向
var out = 25var inner = {out: 20,func: function () {var out = 30return this.out}};console.log((inner.func, inner.func)())console.log(inner.func())console.log((inner.func)())console.log((inner.func = inner.func)())
答案:25,20,20,25
- 逗号操作符会返回表达式中的最后一个值,这里为inner.func对应的函数,注意是函数本身,然后执行该函数,该函数并不是通过对象的方法调用,而是在全局环境下调用,所以this指向window,打印出来的当然是window下的out。
- 这个显然是以对象的方法调用,那么this指向该对象
- 加了个括号,看起来有点迷惑人,但实际上(inner.func)和inner.func是完全相等的,所以还是作为对象的方法调用
- 赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数
var obj = {name: 'abc',fn: () => {console.log(this.name)}};obj.name = 'bcd'obj.fn()// 输出undefined
++i和i++
var x=1switch(x++){case 0: ++xcase 1: ++xcase 2: ++x}console.log(x) // 4// 第一次 i++时,i为1,进入switch,执行case 1,先会让i++生效, i为2 然后++i,i为3;// 由于没break 执行后边case 2,然后++i,i为4;
综合题
事件循环【微任务和宏任务】
答案:2、3、5、4、1setTimeout(function() {console.log(1)}, 0)new Promise(function(resolve) {console.log(2)for( var i=0 ; i<10000 ; i++ ) {i == 9999 && resolve()}console.log(3)}).then(function() {console.log(4)})console.log(5)复制代码
分析:js是一门单线程的语言,但是为了执行一些异步任务时不阻塞代码,以及避免等待期间的资源浪费,js存在事件循环的机制,单线程指的是执行js的线程,称作主线程,其他还有一些比如网络请求的线程、定时器的线程,主线程在运行时会产生执行栈,栈中的代码如果调用了异步api的话则会把事件添加到事件队列里,只要该异步任务有了结果便会把对应的回调放到【任务队列】里,当执行栈中的代码执行完毕后会去读取任务队列里的任务,放到主线程执行,当执行栈空了又会去检查,如此往复,也就是所谓的事件循环。
异步任务又分为【宏任务】(比如setTimeout、setInterval)和【微任务】(比如promise),它们分别会进入不同的队列,执行栈为空完后会优先检查微任务队列,如果有微任务的话会一次性执行完所有的微任务,然后去宏任务队列里检查,如果有则取出一个任务到主线程执行,执行完后又会去检查微任务队列,如此循环。
回到这题,首先整体代码作为一个宏任务开始执行,遇到setTimeout,相应回调会进入宏任务队列,然后是promise,promise的回调是同步代码,所以会打印出2,for循环结束后调用了resolve,所以then的回调会被放入微任务队列,然后打印出3,最后打印出5,到这里当前的执行栈就空了,那么先检查微任务队列,发现有一个任务,那么取出来放到主线程执行,打印出4,最后检查宏任务队列,把定时器的回调放入主线程执行,打印出1。
console.log('1');setTimeout(function() {console.log('2');process.nextTick(function() {console.log('3');});new Promise(function(resolve) {console.log('4');resolve();}).then(function() {console.log('5');});});process.nextTick(function() {console.log('6');});new Promise(function(resolve) {console.log('7');resolve();}).then(function() {console.log('8');});setTimeout(function() {console.log('9');process.nextTick(function() {console.log('10');})new Promise(function(resolve) {console.log('11');resolve();}).then(function() {console.log('12')});})
答案:1、7、6、8、2、4、9、11、3、10、5、12
这道题和上一题差不多,但是出现了process.nextTick,所以显然是在node环境下,node也存在事件循环的概念,但是和浏览器的有点不一样,nodejs中的宏任务被分成了几种不同的阶段,两个定时器属于timers阶段,setImmediate属于check阶段,socket的关闭事件属于close callbacks阶段,其他所有的宏任务都属于poll阶段,除此之外,只要执行到前面说的某个阶段,那么会执行完该阶段所有的任务,这一点和浏览器不一样,浏览器是每次取一个宏任务出来执行,执行完后就跑去检查微任务队列了,但是nodejs是来都来了,一次全部执行完该阶段的任务好了,那么process.nextTick和微任务在什么阶段执行呢,在前面说的每个阶段的后面都会执行,但是process.nextTick会优先于微任务,一图胜千言:
理解了以后再来分析这道题就很简单了,首先执行整体代码,先打印出1,setTimeout回调扔进timers队列,nextTick的扔进nextTick的队列,promise的回调是同步代码,执行后打印出7,then回调扔进微任务队列,然后又是一个setTimeout回调扔进timers队列,到这里当前节点就结束了,检查nextTick和微任务队列,nextTick队列有任务,执行后打印出6,微任务队列也有,打印出8,接下来按顺序检查各个阶段,check队列、close callbacks队列都没有任务,到了timers阶段,发现有两个任务,先执行第一个,打印出2,然后nextTick的扔进nextTick的队列,执行promise打印出4,then回调扔进微任务队列,再执行第二个setTimeout的回调,打印出9,然后和刚才一样,nextTick的扔进nextTick的队列,执行promise打印出11,then回调扔进微任务队列,到这里timers阶段也结束了,执行nextTick队列的任务,发现又两个任务,依次执行,打印出3和10,然后检查微任务队列,也是两个任务,依次执行,打印出5和12,到这里是有队列都清空了。
