收藏 javascript-questions 这个仓库很久了,趁着周末来锻炼下自己的 JS 基础水平
因为逐渐也在承担一些面试工作,顺便摘录一些个人觉得比较适合面试的题目和方向
事件流(捕获、冒泡)
以下代码点击结果是啥?
<div onclick="console.log('div')"> <p onclick="console.log('p')"> Click here! </p> </div>
答案
-> 依次打印 p
和 div
这道题考查事件流,在事件传播中,有三个阶段,捕获->目标->冒泡,按照标签来看,就是 div->p->div,但是默认只在冒泡阶段处理事件
如果需要在捕获阶段处理事件,可用 addEventLister
并且传入第三个参数为 true
(默认是 false
,即默认冒泡阶段处理)
<div id="div"> <p id="p"> Click here! </p> </div> <script> document.getElementById('div').addEventListener('click', () => console.log('div'), true) document.getElementById('p').addEventListener('click', () => console.log('p'), true) </script>
call、apply、bind
以下代码输出结果是啥?
const person = { name: 'Lydia' }; function sayHi(age) { return `${this.name} is ${age}`; } console.log(sayHi.call(person, 21)); console.log(sayHi.bind(person, 21));
答案
-> Lydia is 21
[Function: bound sayHi]
这道题主要考查 call 和 bind,当然我们面试的时候一般会把 apply 拿出来一起考察下
call、apply 以及 bind 都是为了改变 this 指向而生,而 call 和 apply 用法比较像,都会在函数调用时改变 this 指向,唯一的区别是 apply 的第二个参数是数组,而 call 从第二个参数开始都是实际传入该函数中的值。bind 与他们不一样,它返回的还是函数
new
以下代码输出结果是啥?
function Car() { this.make = 'Lamborghini'; return { make: 'Maserati' }; } const myCar = new Car(); console.log(myCar.make);
答案
-> Maserati
这题考查 new。如果对构造函数以及 new 比较了解的就会知道,构造函数里,如果返回一个非 null 的对象(包括数组),则将该对象值赋值给新建的对象
了解更多可以参考 一道有意思的笔试题引发的对于 new 操作符的思考
JSON.stringify
以下代码输出结果是啥?
const settings = { username: 'lydiahallie', level: 19, health: 90, }; const data = JSON.stringify(settings, ['level', 'health']); console.log(data);
答案
-> {"level":19,"health":90}
这道题主要考查 JSON.stringify
的第二个参数,当然它还能传入第三个参数
语法:JSON.stringify(value[, replacer [, space]])
。详见 JSON.stringify
replacer
replacer 参数可以是一个函数或者一个数组。作为函数,它有两个参数,键(key)和值(value),它们都会被序列化
在开始时,replacer 函数会被传入一个空字符串作为 key 值,代表着要被 stringify 的这个对象。随后每个对象或数组上的属性会被依次传入
const settings = { username: 'lydiahallie', level: 19, health: 90, }; const data = JSON.stringify(settings, (k, v) => { console.log(k, v); return v }); console.log(data); /* // 这里其实最开始打印了个空字符串。在开始时,replacer 函数会被传入一个空字符串作为 key 值,代表着要被 stringify 的这个对象 { username: 'lydiahallie', level: 19, health: 90 } username lydiahallie level 19 health 90 {"username":"lydiahallie","level":19,"health":90} */
const settings = { username: 'lydiahallie', level: 19, health: 90, }; const data = JSON.stringify(settings, (k, v) => { if (k === 'level') return v * 2 return v }); console.log(data); // {"username":"lydiahallie","level":38,"health":90}
当传入数组的时候,相对比较简单,就是需要 picked 的 key(但是没法深度 pick,只能 pick 第一层)
space
用来格式化,可以传入数字或者字符串
const settings = { username: 'lydiahallie', level: 19, health: 90, }; const data = JSON.stringify(settings, null, 2); // 这时候和传入 ' '(length 2)效果一样 console.log(data); /* { "username": "lydiahallie", "level": 19, "health": 90 } */
const settings = { username: 'lydiahallie', level: 19, health: 90, }; const data = JSON.stringify(settings, null, 'hello'); console.log(data); /* { hello"username": "lydiahallie", hello"level": 19, hello"health": 90 } */
函数参数解构
以下代码输出结果是啥?
const myFunc = ({ x, y, z }) => { console.log(x, y, z); }; myFunc(1, 2, 3);
答案
-> undefined
undefined
undefined
这道题比较简单,myFunc 函数需要的是一个对象参数,并且有 key x
y
和 z
,但是传入的并不是对象,所以都取了他们的默认值 undefined
如果传入对象,但是并没有对应的 key,也同样是 undefined
const myFunc = ({ x, y, z }) => { console.log(x, y, z); }; myFunc({ x: 1 }); // 1 undefined undefined
函数中的剩余参数
以下代码输出结果是啥?
function getItems(fruitList, ...args, favoriteFruit) { return [...fruitList, ...args, favoriteFruit] } getItems(["banana", "apple"], "pear", "orange")
答案
-> SyntaxError
这道题就比较脑经急转弯了,剩余参数只能放在最后,其实我觉得像上面代码那样其实也有使用场景,不知道未来会不会支持这样的写法
展开运算符
以下代码输出结果是啥?
const person = { name: 'Lydia', age: 21, }; const changeAge = (x = { ...person }) => (x.age += 1); const changeAgeAndName = (x = { ...person }) => { x.age += 1; x.name = 'Sarah'; }; changeAge(person); changeAgeAndName(); console.log(person);
答案
-> { name: 'Lydia', age: 22 }
这道题的考点是展开运算符在对象中的使用。一般我们会用展开运算符来复制一个对象
当调用 changeAge(person)
时,参数传入了 person,所以 (x.age += 1)
实际操作的和 person 是一个对象引用。而调用 changeAgeAndName()
时,因为没有传入参数,所以 x 其实是 { ...person }
,而这个其实是 person 的一个浅拷贝
可以考查下将 (x = { ...person })
改成 (x = person)
||
以下代码输出结果是啥?
const one = false || {} || null; const two = null || false || ''; const three = [] || 0 || true; console.log(one, two, three);
答案
-> {}
""
true
这道题的考点是 ||
和假值。 ||
运算符会返回第一个真值,如果都为假值,则返回最后一个值
这里可以顺便考查 &&
。&&
操作符,一旦遇到了假值,便会停止往后
console.log(true && 0); // 0 console.log(true && 0 && true); // 0 console.log(1 && 2); // 2 console.log(1 && false); // false console.log('' && false); // ''
falsy
以下哪些值是假值?
0; new Number(0); (''); (' '); new Boolean(false); undefined;
答案
-> 0
''
undefined
JS 中的假值有以下几种:
false
''
NaN
undefined
null
0
/-0
0n
(BigInt(0))
除此之外,new Number
、new Boolean
创建的其实都是对象,都是真值
对象中 key 重复
以下代码输出结果是啥?
const obj = { a: 'one', b: 'two', a: 'three' }; console.log(obj);
答案
-> { a: 'three', b: 'two' }
这其实更像是一个规范,当对象中 key 重复的时候,value 会被后面的替换,但是 key 的位置会是第一次出现的位置
一般是对象合并的时候会用到:
const oldObj = { name: 'fish', age: 30 } const coverdObj = { name: 'lessfish' } const newObj = { ...oldObj, ...coverdObj } console.log(newObj); // { name: 'lessfish', age: 30 }
Array Operators
有好几道和数组操作有关的题
这题 主要考查 push 操作返回 push 后的数组长度,这题 也是类似
这题 主要考查哪些数组操作会改变原来的数组(splice)
这题 主要考查 reduce 的使用
[1, 2, 3, 4].reduce((x, y) => console.log(x, y));
这题还是比较有意思的,reduce 函数调用中如果有第二个参数,则会被当作第一次迭代中的 previous 值(也就是第一个参数 x
),如果没有第二个参数,则数组第一个元素会被当作第一次迭代的 previous
所以上面的代码,如果 reduce 有第二个参数,会被迭代 4 次,如果没有,则是迭代三次
再看代码,因为没有第二个参数,所以第一次迭代参数是 1
和 2
,reduce 迭代中的返回会被当作下次迭代的 previous,但是这里没返回,所以就是 undefined
,而第二个参数 y
就是数组元素
Object Operators
这题 主要考查用 Object.defineProperty
定义的对象中的属性默认不可枚举,不能用 Object.keys
拿到
这题 主要考查 Object.freeze
,顾名思义它能冰冻住对象,使得对象不能增、删、修改键值对(但是注意,它只是 freeze 了第一层,可以参考 这题)
这题 考查 Object.seal
,它能阻止对象新增、删除属性,但是对于已有的属性依然可以修改其值(注意和 freeze 一样同样只是第一层)
delete
以下代码输出结果是啥?
const name = 'Lydia'; age = 21; console.log(delete name); console.log(delete age);
答案
-> false
true
这题考查 delete
操作
首先 delete
返回一个布尔值,代表是否删除成功。然后 var
const
let
定义的变量,都不能被删除,只有全局变量才能被删除
这里要注意下 name
,因为浏览器中默认挂了个全局变量 name
,所以 delete name
是 ok 的
暂时性死区
这两题都是暂时性死区相关,注意下 var
和 let
const
是有差异的,var
的话会变量声明提升(但是是 undefined),但是 let
和 const
并不会初始化
Object toString
以下代码输出结果是啥?
const animals = {}; let dog = { emoji: '🐶' } let cat = { emoji: '🐈' } animals[dog] = { ...dog, name: "Mara" } animals[cat] = { ...cat, name: "Sara" } console.log(animals[dog])
答案
-> { emoji: "🐈", name: "Sara" }
当对象的 key 也是对象的时候,会自动调用 toString
转为 [object Object]
const animals = {}; let dog = { emoji: '🐶' } let cat = { emoji: '🐈' } animals[dog] = { ...dog, name: "Mara" } animals[cat] = { ...cat, name: "Sara" } console.log(animals[dog] === animals[cat]) // true
这题 大同小异
对象引用
以下代码输出结果是啥?
let person = { name: 'Lydia' }; const members = [person]; person = null; console.log(members);
标签函数
以下代码输出结果是啥?
function getPersonInfo(one, two, three) { console.log(one); console.log(two); console.log(three); } const person = 'Lydia'; const age = 21; getPersonInfo`${person} is ${age} years old`;
答案
-> ["", " is ", " years old"]
Lydia
21
这个叫做标签函数,函数调用后跟一个模版字符串,函数中第一个参数是数组,返回模版字符串中根据 ${xxx}
分割的字符元素,其余参数就是 ${xxx}
的值
暂时没想到有什么用,据说 styled-components
内部就是使用的它实现。可以参考下 这篇文章
String.raw
以下代码输出结果是啥?
console.log(String.raw`Hellonworld`);
答案
-> Hellonworld
顾名思义,会展示 raw string
Number.isNaN & isNaN
以下代码输出结果是啥?
const name = 'Lydia Hallie'; const age = 21; console.log(Number.isNaN(name)); console.log(Number.isNaN(age)); console.log(isNaN(name)); console.log(isNaN(age));
答案
可以参考 这里
简单说,Number.isNaN
只有传入 NaN
的时候,才返回 true。而 isNaN
会强制先将参数转为 Number 类型(调用 Number(xxx)
)
比如 isNaN('Lydia Hallie')
,其实就是 isNaN(Number('Lydia Hallie'))
,而 Number('Lydia Hallie')
返回 NaN
ES6 Module
以下代码输出结果是啥?
// index.js console.log('running index.js'); import { sum } from './sum.js'; console.log(sum(1, 2)); // sum.js console.log('running sum.js'); export const sum = (a, b) => a + b;
答案
-> running sum.js
running index.js
3
这题主要考查 ES6 模块是编译时输出接口,而依赖的代码会被先执行(CommonJS 模块是运行时加载,所以可以在任何地方 require
)
以下代码输出结果是啥?
// counter.js let counter = 10; export default counter;
// index.js import myCounter from './counter'; myCounter += 1; console.log(myCounter);
答案
-> Error
ES6 导出的内容是只读的,不能被修改,只能在所导出模块中修改
关于 js modules 更多可参考 这里
宏任务、微任务
以下代码输出结果是啥?
const myPromise = Promise.resolve(Promise.resolve('Promise')); function funcOne() { setTimeout(() => console.log('Timeout 1!'), 0); myPromise.then(res => res).then(res => console.log(`${res} 1!`)); console.log('Last line 1!'); } async function funcTwo() { const res = await myPromise; console.log(`${res} 2!`) setTimeout(() => console.log('Timeout 2!'), 0); console.log('Last line 2!'); } funcOne(); funcTwo();
答案
简单点说,await
后的代码其实可以理解成 Promise.then
后的代码
先理解下以下代码输出:
const myPromise = Promise.resolve(Promise.resolve('Promise')); function funcOne() { myPromise.then(res => console.log(`${res} 1!`)); } async function funcTwo() { const res = await myPromise; console.log(`${res} 2!`) } funcOne(); funcTwo();
console.log(`${res} 1!`)
和 console.log(`${res} 2!`)
可以理解为微任务队列的内容,依次执行
稍作修改:
const myPromise = Promise.resolve(Promise.resolve('Promise')); function funcOne() { myPromise.then(res => res).then(res => console.log(`${res} 1!`)); } async function funcTwo() { const res = await myPromise; console.log(`${res} 2!`) } funcOne(); funcTwo();
这时微任务队列依次是 res => res
和 console.log(`${res} 2!`)
,然后前者执行后,将 console.log(`${res} 1!`)
加入微任务队列中,所以其实是 console.log(`${res} 2!`)
先执行
更多可参考 我以前的总结
箭头函数
先明确下箭头函数和普通函数的几个区别:(参考 箭头函数)
其中最重要的特性是,箭头函数没有自己的 this
对象,它的 this
是定义时上层作用域中的 this
以下代码输出结果是啥?
const shape = { radius: 10, diameter() { return this.radius * 2; }, perimeter: () => 2 * Math.PI * this.radius, }; console.log(shape.diameter()); console.log(shape.perimeter());
答案
根据 它的 this
是定义时上层作用域中的 this
,原题可以改写成这样:
var that = this const shape = { radius: 10, diameter() { return this.radius * 2; }, perimeter: () => 2 * Math.PI * that.radius, }; console.log(shape.diameter()); console.log(shape.perimeter());
这时候答案就比较清晰了
我们可以更进一步出题:
const shape = { radius: 10, diameter() { return this.radius * 2; }, perimeter: () => { return () => 2 * Math.PI * this.radius } }; console.log(shape.diameter()); console.log(shape.perimeter()());
这个时候答案其实没区别,毕竟第二个箭头函数 this 来自上层作用域,而上层作用域来自顶层作用域,层层传递
但是如果改写成这样:
const shape = { radius: 10, diameter() { return this.radius * 2; }, perimeter: function () { return () => 2 * Math.PI * this.radius } }; console.log(shape.diameter()); console.log(shape.perimeter()());
这就有意思了,因为上层并不是箭头函数,它的 this 指向 shape,所以箭头函数的 this 也指向 shape
这道题的考点可以是这个改写,需要对箭头函数中的 this
有着深入了解
92 这题的考点是箭头函数不能作为构造函数,所以它并没有 prototype
属性
98 这题比较脑经急转弯,箭头函数如果返回一个对象,需要用 ()
包下
151 还是 this 相关,和第一题类似