基础题
变量赋值
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 = a
a.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) //undefined
console.log(b.x) // {n:2}
变量提升和作用域
var scope = 'global scope'
function a(){
function b(){
console.log(scope)
}
return b
var scope = 'local scope'; // var会存在变量提升,如果是let,则会报错。
}
a()() // 输出undefined
console.log(a)
var a = 1
var getNum = function() {
a = 2
}
function getNum() {
a = 3
}
console.log(a)
getNum()
console.log(a)
// 答案 undefined 1 2
var i = 1
function b() {
console.log(i) //使用的是全局作用域下的值
}
function a() {
var i = 2
b()
}
a();
// 结果输出 1
变量声明和函数声明同时存在
var a = 1
function a(){}
console.log(a)
// 第一行的变量声明会被函数声明覆盖 FnA varA,执行到console.log时,又在之前进行了赋值a=1
var b
function b(){}
console.log(b) // 打印出b函数
function b(){}
var b
console.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]=123
a[c]=456
console.log(a[b]) // 456
console.log(typeof undefined == typeof NULL)
console.log(typeof function () {} == typeof class {})
this指向
var out = 25
var inner = {
out: 20,
func: function () {
var out = 30
return 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=1
switch(x++)
{
case 0: ++x
case 1: ++x
case 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,到这里是有队列都清空了。