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]
仅列举以上三种方法,也可查阅下其他类似方法。
注:若有解答不详的问题,可以再查阅下其他资料,仅供参考;
若有误,还请大佬们及时指出,谢谢!