【面试题系列】必须掌握的JS面试题
1、原型和原型链
当我们找实例对象的属性时,如果找不到,就会查找与对象关联的原型中去找,如果还找不到,就去找原型的原型,直到最顶层。
function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);
答案:1 undefined 2
解析:
//声明一个构造函数A
function A() {}
//声明一个构造函数B,传人参数a,自有属性a的值取决于传入的参数
function B(a) {
this.a = a;
}
//声明一个构造函数B,如果有参数,则添加自有属性a,属性a的值为传入的参数值,
//如果没有传入参数,则构造函数C没有自有属性
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
//构造函数function A(){},是没有自有属性的,然后就会顺着原型链查找,我们找到构造函数A的原型对象A.prototype上有属性a且等于1
console.log(new A().a);//1
console.log(new B().a);//undefined
console.log(new C(2).a);//2
2、异步队列
微任务:Promise.then、async/await、MutaionObserver
宏任务:script(整体)、setTimeout、setInterval、Ajax、DOM事件、postMessage
执行顺序如下:
- 第一题
async function async1() {
console.log("a");
const res = await async2();
console.log("b");
}
async function async2() {
console.log("c");
return 2;
}
console.log("d");
setTimeout(() => {
console.log("e");
}, 0);
async1().then(res => {
console.log("f")
})
new Promise((resolve) => {
console.log("g");
resolve();
}).then(() => {
console.log("h");
});
console.log("i");
答案:d、a、c、g、i、b、h、f、e
解析:
//打印d
//遇到setTimeout,放到宏任务队列
//遇到async1().then(),先执行async1(),then函数注册到微任务队列(但不是立即注册,而是等async1()执行完之后才会注册)
//在执行async1()时候
async1().then(res => {
console.log("f")
})
//打印a
//执行async1()时,当执行到 const res = await async2()时,await后先让后面的表达式先执行,也就是async2()
//打印c
//然后将其后面的代码放到微任务队列中,然后跳出async函数,执行后面的代码,console.log("c")被放到了微任务队列中
//执行new Promise((resolve) => { console.log("g"); resolve(); }).then(),由于new Promise()是同步任务
//打印g
//new Promise后的then函数放到微任务队列
//打印i
//此时微任务队列[console.log("b"),console.log("h")][console.log("f")]
//宏任务队列[console.log("e")]
//先执行微任务,再执行宏任务
//打印g、f、e
- 第二题
setTimeout(function(){
console.log(1)
},0)
new Promise(function(resolve,reject){
console.log(2)
for(var i=0;i<10000;i++) {
if(i===10){
console.log(10);
}
i==9999 && resolve()
}
console.log(3);
}).then(function(){
console.log(4);
})
console.log(5);
答案:2 10 3 5 4 1
解析
//setTimeout放到宏任务队列
//执行 new Promise()同步任务
//打印2
//打印10
//打印3
// new Promise()后的then函数 放到微任务队列
//打印5
微任务队列:[console.log(4)]
宏任务队列:[ console.log(1)]
- 第三题
Promise.reject(1).
then(2).
then(res => { return 3 }).
catch(error => { return 4 }).
then(Promise.resolve(5)).
then(console.log)
答案:4
解析:
//Promise.reject(1) 返回一个被拒绝的 Promise,其拒绝原因为数字 1。
//.then(2) 返回一个新的 Promise,它的回调函数不会被调用,因为前一个 Promise 被拒绝了。
//.then(res => { return 3 }) 返回一个新的 Promise,它的回调函数也不会被调用,因为前一个 Promise 被拒绝了。
//.catch(error => { return 4 }) 返回一个新的 Promise,它的回调函数被调用并返回数字 4(作为一个被解决的 Promise)。
//.then(Promise.resolve(5)) 返回一个新的 Promise,它的回调函数不会被调用,因为参数不是一个函数。
//.then(console.log) 返回一个新的 Promise,它的回调函数被调用并打印数字 4。
- 第四题
var F = function() {};
Object.prototype.a = function() {
console.log("a()");
};
Function.prototype.b = function() {
console.log("b()");
};
var f = new F();
F.a();
F.b();
f.a();
f.b();
答案:a b a f.b is not a function
解析:
//1、声明变量F,F既是对象也是函数,故F可调用原型上的方法,无论是对象还是函数上的
//2、f = new F(),通过 构造函数 F new了一个实例f,此f只是一个对象并不是函数类型,故不能调用Function原型上的方法
//证明:
3、浅拷贝/深拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝生成一个新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
- 第一题
var obj1 = {
a: 1,
b: 2,
c: 3
}
var obj2 = obj1;
obj2.a = 5;
console.log(obj1.a);
console.log(obj2.a);
答案:1 5
解析
//var obj2 = obj1属于浅拷贝,故而obj1,obj2共用一个内存,打印出5,5
- 第二题
var obj1 = {
a: 1,
b: 2,
c: 3
}
var objString = JSON.stringify(obj1);
var obj2 = JSON.parse(objString);
obj2.a = 5;
console.log(obj1.a);
console.log(obj2.a);
解析
//JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串
//JSON.parse() 方法用来解析 JSON 字符串,解析为 JavaScript 值或对象
//JSON.parse(JSON.stringify())也常被用作深拷贝
//深拷贝生成一个新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
//打印1、5
- 第三题
let obj = {
num: 117
}
var num = 935;
function func() {
console.log(this.num)
}
const wrapper = func.bind(obj);
setTimeout(() => {
wrapper()
}, 1000)
obj = {
num: 130
}
答案:117
解析:
//在执行const wrapper = func.bind(obj)时,func的this指向obj
//接着执行setTimeout,1s后执行wrapper
//在上面的一秒内,重新赋值obj = {num:130},这并不会影响到已经绑定了 obj 的 wrapper 函数
//所以打印出117
4、作用域
JS代码的执行是由浏览器中的JS解析器来执行的。JS解析器执行JS代码的时候,分为两个过程:预解析过程,代码执行过程预解析过程:
1、把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
2、把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
3、先提升var,在提升function。
预解析顺序:
1、预解析的顺序是从上到下,函数的优先级高于变量,函数声明提前到当前作用域最顶端,在var之上。
2、函数执行的时候,函数内部才会进行预解析,如果有参数,先给实参赋值再进行函数内部的预解析。
3、预解析函数是声明+定义(开辟了内存空间,形参值默认是undefined)。
4、预解析变量是只声明,不赋值,默认为undefined。
5、==函数重名时,后者会覆盖前者。 ==
6、==变量重名时,不会重新声明,但是执行代码的时候会重新赋值。==
7、变量和函数重名的时候,函数的优先级高于变量
函数执行顺序:
1、形成私有作用域
2、形参赋值(也是私有变量)
3、变量提升
4、代码从上到下执行
5、作用域是否销毁
函数作用域不销毁的条件:
1、函数的返回值为引用类型
2、返回值被其他变量使用
- 第一题
var a = 0, b = 0
function A(a) {
A = function(b) {
console.log(a + b++)
}
console.log(a++)
}
A(1)
A(2)
答案:2 4
解析
//执行A(1),传入参数1
//修改函数A的值为:function(b) { console.log(a + b++) }
// 执行 console.log(a++),打印出1
//执行A(2),传入参数2
//由于A函数被修改,所以执行的的是function(b) { console.log(a + b++) }
// 此时a为2,传入的b为2 ,所以打印4
- 第二题
console.log(a);
var a=12;
function fn(){
console.log(a);
a=13;
}
fn();
console.log(a);
答案:undefined 12 13
解析:
//如果在函数中定义变量时,如果不添加var关键字, 这个变量是一个全局变量
//打印undefined
//由于a=13在定义a变量没有用关键字,所以在这里是全局变量
//fn执行console.log(a)时没有找到私有变量a,会沿着作用域链查找变量
//打印12
//紧接着a=13修改全局变量
//打印13
- 第三题
console.log(a);
a=2;
function fn(){
console.log(a);
a=3;
}
fn();
console.log(a);
答案:Uncaught ReferenceError: a is not defined
解析:
//变量a不会被提升,因为没有var声明,
//如果在函数中定义变量时,如果不添加var关键字, 这个变量是一个全局变量
//所以会报错:Uncaught ReferenceError: a is not defined
- 第四题
var a=10,b=11,c=12;
function test(a){
a=1;var b=2;c=3;
}
test(10);
alert(a); alert(b); alert(c);
答案:1 11 3
- 第五题
var foo = function () {
console.log("foo1")
}
foo()
var foo = function () {
console.log("foo2")
}
foo()
function foo() {
console.log("foo1")
}
foo()
function foo() {
console.log("foo2")
}
foo()
答案:foo1 foo2 foo2 foo2
解析:
//该题可以把代码分开
var foo = function () {
console.log("foo1")
}
foo()
var foo = function () {
console.log("foo2")
}
foo()
//1、声明了变量foo,因为声明变量重名了,故不会重新声明,
//2、执行代码时候,会先执行
function () {
console.log("foo1")
}
//3、又对变量进行重新复制,故执行
function () {
console.log("foo2")
}
//所以打印出 foo1 foo2
----------------------------------------
function foo() {
console.log("foo1")
}
foo()
function foo() {
console.log("foo2")
}
foo()
//1、在预解析的时候,由于声明函数foo重名,故后者会覆盖前者,所以最后foo函数声明的结果如下
function foo() {
console.log("foo2")
}
//2、执行打印出foo2 foo2
5、矩阵转置
将 [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]——>[ [1, 4, 7], [2, 5, 8], [3, 6, 9] ]
let a = [
[1,2,3],
[4,5,6],
[7,8,9]
]
let b = [
[1,4,7],
[2,5,8],
[3,6,9]
]
//其实不难发现 a[0][0] = b[0][0],a[0][1] = b[1][0],a[0][2] = b[2][0]
//也就是 a[i][j] = b[j][i]
解析:
function transposeArray(array) {
// 获取原行数和列数
const rows = array.length;
const cols = array[0].length;
// 创建一个新的二维数组,长度等于原来数组列数
const transposedArray = [];
for (let j = 0; j < cols; j++) {
transposedArray[j] = new Array(rows);
}
// 将元素从原数组复制到新数组中
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
transposedArray[j][i] = array[i][j];
}
}
return transposedArray;
}
const myArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
const transposedArray = transposeArray(myArray);
console.log(transposedArray);
// 输出:[[1, 4, 7], [2, 5, 8], [3, 6, 9]]
6、类、继承
class cls{
constructor(){
this.show()
}
show(){
console.log("yoo")
}
}
class son extends cls{
constructor(){
super()
}
show(){
console.log('ohh')
}
}
new son()
new cls()
答案:ohh yoo
解析
//1、在创建 son 对象时,会先调用父类 cls 的构造函数
//2、然后再调用子类 son 的构造函数。
//3、在子类 son 的构造函数中,没有显式调用父类的 show 方法,因此会直接执行子类中重写的 show 方法,输出 “ohh”。
//4、接着创建 cls 对象时,同样会调用父类 cls 的构造函数,执行父类的 show 方法,输出 “yoo”
- 点赞
- 收藏
- 关注作者
评论(0)