前端面试题总结 — JS 篇(一)

本文详细介绍了JavaScript中的核心概念,包括对象深克隆和浅克隆的实现,事件循环机制,垃圾回收机制,原型和原型链,事件委托,基本数据类型以及null和undefined的区别,还有typeof和instanceof的区别。此外,还讨论了call、apply和bind的用途,forEach和map的区别,并列举了几种数组去重的方法。内容涵盖了JavaScript编程中的重要知识点,有助于深入理解语言原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、对象(数组)深克隆和浅克隆区别?如何实现浅克隆?如何实现深克隆?Object.assign()是浅拷贝还是深拷贝?

1)浅克隆:只对第一层属性进行了拷贝,若第一层的属性值存在复杂数据类型,则与原对象的属性指向的是同一块内存区域,对新对象做操作时,同时会影响原对象。

      深克隆:拷贝的对象与原来的对象完全隔离,互不影响。

2)浅克隆

方式1:

let obj2 = {}
for(let key in obj){
  if(!obj.hasOwnProperty(key)) break;
  obj2[key] = obj[key];
}

方式2:

let obj2 = { ...obj };

方式3:Object.assign()

Object.assign()方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,并返回目标对象。若源对象的属性值是对象,则拷贝的是对象属性的引用(指向对象的指针),而不是对象本身。

let newObj = Object.assign({}, obj);

方式4:第三方库 lodash 中的 clone 函数进行浅克隆

3)深克隆

方式1:

let obj2 = JSON.parse(JSON.stringify(obj));  

弊端:无法拷贝对象中的函数、undefined、symbol、Date、RegExp 等

方式2:第三方库 lodash 中的 deepClone 函数进行深克隆

方式3:手写深克隆

function deepClone(obj){
    // 特殊情况处理
    if (obj === null) return null
    if (typeof obj !== "object") return obj
    if (obj instanceof RegExp) {
        return new RegExp(obj)
    }
    if (obj instanceof Date) {
        return new Date(obj)
    }
    // 不直接创建空对象的目的:克隆的结果和之前保持相同的所属类
    let newObj =new obj.constructor;
    for(let key in obj){
        // hasOwnProperty 判断对象自有的属性,不包括从原型链继承的属性
 	    if(obj.hasOwnProperty(key)) {
            newObj[key] = deepClone(obj[key])
        }
    }
    return newObj;
}

2、讲一下 JS 的事件循环机制(EventLoop)

JS 是单线程语言,通过事件队列(Event Quque)来实现异步。

所有的任务分为同步任务和异步任务,同步任务会按执行顺序进入执行栈主线程中执行;而异步任务会进入任务队列,待主线程内的任务执行完毕才会去任务队列读取对应的任务,推入主线程执行。

JS 代码执行时,任务分为宏任务和微任务。

常见的宏任务:script(整体代码)、setInterval、setTimeout、setImmediate、事件绑定 及 ajax 等。

常见的微任务:promise( async / await )及 MutationObserver(监听节点、DOM变化) 等。

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout0') 
},0)  
setTimeout(function(){
    console.log('setTimeout3') 
},3)  
setImmediate(() => console.log('setImmediate'));
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function(){
    console.log('promise3')
})
console.log('script end')

上面代码浏览器执行输出结果如下:

script start
async1 start
async2
promise1
promise2
script end
async1 end
promise3
setImmediate
setTimeout0
setTimeout3

总结:先顺序执行第一个宏任务(script整体代码)内的同步代码,遇到微任务会先放到微队列中,遇到宏任务会先放到宏队列中;该宏任务中的同步代码执行完毕后,然后按队列先进先出的顺序执行微队列中的微任务;微队列中所有的微任务均执行完毕后,再从宏队列中执行下一个宏任务,进行下一轮循环。

3、讲一下 JS 的垃圾回收机制

JavaScript 具有自动垃圾回收机制 GC,即找出不再使用的变量,然后释放其占用的内存。

常用的垃圾回收方式有两种: 标记清除 和 引用计数。

引用计数:跟踪记录每个值被引用的次数,会释放那些引用次数为 0 的值所占的内存。

标记清除:会给存储在内存中的所有变量加上标记,会去掉环境中的变量以及被环境中的变量引用的标记,在此之后依然被标记的变量将被视为准备删除的变量。

4、讲一下 JS 的原型和原型链

每个对象都有一个 __proto__属性,并且指向它的 prototype原型对象;

每个构造函数都有一个 prototype原型对象,prototype原型对象里的 constructor指向构造函数本身。

每个对象都有自己的原型对象,一层层往上,就形成了原型链。原型链的顶端,就是Object.prototype,它没有原型对象(为null)。

每个对象都会从一个 prototype(原型对象)中继承属性和方法。

当需要访问一个对象的属性时,会先查找对象本身的属性,找不到的话,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的顶端。最后查找到 Object.prototype时,Object.prototype.__proto__ === null,意味着查找结束。

5、讲一下 JS 的事件委托

事件委托,简单的来说,就是把一个元素的响应事件的函数委托到另一个元素。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

事件的传播分为三个阶段:捕获阶段、目标阶段、冒泡阶段。

捕获阶段:从window,document 和根元素开始,事件向下扩散至目标元素的祖先;
目标阶段:事件在用户单击的元素上触发;
冒泡阶段:最后,事件冒泡通过目标元素的祖先,一直到根元素 document 和 window。

由 addEventListener(ev, fn, useCapture) 第三个参数控制,当 useCapture为 true,表示该事件在捕获阶段触发,当 useCapture为 false,表示该事件在冒泡阶段触发。

事件委托的好处:减少内存消耗,提高性能;动态绑定事件;替代原生JS中循环绑定事件的操作。

6、JS 的基本数据类型?null 和 undefined 的区别?0*null 和 0*undefined 的值分别为?为什么?

1)字符串(String)、数字(number)、空(null)、未定义(Undefined)、Symbol

2)null 表示“没有值”,转为数值时为 0;

      undefined表示“缺少值”,转为数值时为 NaN;

3)0*null = 0

      0*undefined = NaN

4)null 转化为 0,0*null = 0*0 = 0

      undefined 转化为 NaN ,NaN是 JavaScript 之中唯一不等于自身的值,不等于任何值,包括它本身。NaN 在布尔运算时被当作 false,NaN 与任何数(包括它自己)的运算,得到的都是NaN。当运算失败或者运算无法返回正确的数值时,就会返回NaN。所以,0*undefined = 0*NaN = NaN

7、typeof() 和 instanceof() 的区别?如何区分对象和数组?

1)typeof 是一元运算符,用于判断变量的类型,但对于 Array,NUll 等 typeof 返回的是 Object;

      instanceof 是用于判断一个变量是否属于某个对象的实例,本质是基于原型链的查询。

2)

// 方法1:利用 Array.isArray() 判断一个对象是否为数组
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false

// 方法2:利用 instanceof 判断一个变量是否属于某个对象的实例
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false


// 方法3:利用 constructor 属性返回对象的构造函数,返回值是函数的引用,不是函数名
console.log([].constructor == Array); // true
console.log({}.constructor == Object); // true


// 方法4:利用 Object.prototype.toString.call
console.log(Object.prototype.toString.call([])); // [object Array]  
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call([]).substr(8,5) == 'Array'); // true

8、call、apply 和 bind 的区别?

三者都是用于改变函数体内 this 的指向,第一个参数都是 this 要指向的对象。

bind 方法返回的仍然是一个函数,因此后面还需要 () 来进行调用才可以;

apply 和 call 的调用返回函数执行结果;apply 的第二个参数是一个参数数组,而 call 的第二个及其以后的参数都是数组里面的元素,就是说要全部列举出来。

9、forEach 和 map 的区别?

两者都是用来循环遍历数组中的每一项,都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组),且匿名函数中的 this 都是指向 window。

forEach 方法会修改原先的数组,而 map 则是返回一个新的数组,且 map 的执行速度比 forEach 快。

forEach 和 map 本身均不能跳出循环,不支持 break,且使用 return 无效。可以通过抛出 new throw error() 然后使用 try catch 去捕获这个错误才可以终止循环,如下图:

 forEach 删除自身元素 index 不会被重置 (底层控制 index 自增,无法左右)

10、列举 JS 数组去重方法?

1)利用 Set 数据结构,它类似于数组,但其成员的值都是唯一的。

function unique(arr){
    return Array.from(new Set(arr));  // 利用 Array.from 将 Set 结构转换成数组
}
let arr = [1,-5,-4,0,-4,7,7,3];
console.log(unique(arr)); // 输出结果:[1, -5, -4, 0, 7, 3]

2)利用数组的 indexOf 方法去重,array.indexOf(item,statt) 返回数组中某个指定的元素的位置,没有则返回-1。

function unique(arr) {
  let arr1 = [];
  for (let i = 0, len = arr.length; i < len ; i++) {
    if (arr1.indexOf(arr[i]) === -1) {
      arr1.push(arr[i]);
    }
  }
  return arr1;
}
let arr = [1,-5,-4,0,-4,7,7,3];
console.log(unique(arr)); // 输出结果:[1, -5, -4, 0, 7, 3]

3)利用 对象属性名不可重复 去重。

function unique(arr) {
  let newArr = [];
  let obj = {};
  arr.forEach(item => {
    if (!obj[item]) {
      newArr.push(item);
      obj[item] = true;
    }
  })
  return newArr;
}
let arr = [1,-5,-4,0,-4,7,7,3];
console.log(unique(arr)); // 输出结果:[1, -5, -4, 0, 7, 3]

仅列举以上三种方法,也可查阅下其他类似方法。

注:若有解答不详的问题,可以再查阅下其他资料,仅供参考;

       若有误,还请大佬们及时指出,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值