# JS篇
# 1. 普通事件绑定和事件流绑定有啥区别(中级)
普通事件绑定
如果给同⼀个元素绑定了两次或者多次相同类型的事件,那么后⾯的绑定会覆盖前⾯的绑定。
不支持
dom
事件流(捕获-目标-冒泡)。
事件流绑定
- 如果说给同⼀个元素绑定了两次或者多次相同类型的事件,所以的绑定将会依次触发。
- 支持
dom
事件流(捕获-目标-冒泡)。 - 不加
on
事件。
function addEvent(obj,type,callBack){
if(obj.addEventListener){
obj.addEventListener(type,callBack);
}else{
obj.attachEvent("on"+type,callBack);
}
}
addEvent(document,"click",function(){alert("兼容写法")});
# 2. 如何阻止事件冒泡和默认事件(中级)
e.preventdefault?e.preventdefault():return value=false
e.stoppropagation?e.stoppropagation():e.cancelbubble=true
# 3. document load 和 document ready 的区别(初级)
- Document.onload 是在结构和样式加载完才执⾏ js。
- window.onload:不仅仅要在结构和样式加载完,还要执⾏完所有的样式、图片这些资源文件,全部 加载完才会触发
window.onload
事件。 - Document.ready 原⽣种没有这个⽅法,
jquery
中有$().ready(function)
。
# 4. undefined 的三种情况(初级)
- ⼀个变量定义了却没有被赋值。
- ⼀个对象上不存在的属性或者⽅法。
- ⼀个数组中没有被赋值的元素。
# 5. 对象模型 BOM 里常用的至少4个对象(初级)
- Window
- document
- location
- screen
- history
- navigator
# 6. 会造成内存泄漏的情况(中级)
setTimeout
的第⼀个参数使⽤字符串⽽非函数的话,会引发内存泄漏。- 闭包
# 7. 解释什么是jsonp(中级)
jsonp 并不是⼀种数据格式,而json是一种数据格式。
jsonp 是⽤来解决跨域获取数据的⼀种解 决⽅案。
具体是通过动态创建script
标,通过标签的src
属性获取js
文件中的js
脚本,
该脚本的内容是⼀个函数调⽤,参数就是服务器返回的数据,为了处理这些返回的数据,需要事先在⻚⾯定义好回调函数,本质上使⽤的并不是ajax
技术。
# 8. 生成随机数(初级)
Math.round(Math.random()*差值)+最小的值
# 9. link 与 import 的区别(初级)
- link 不仅仅可以加载
css
,import 只能加载css
。 - link 同时加载,import 先载入页面内容。
- link 无兼容问题。
- link ⽀持使⽤
Javascript
控制DOM
去改变样式;⽽@import
不⽀持。
# 10. 如何优化自己的代码(中级)
- 重复使用。
- 避免过多使用全局变量。
- 闭包中变量的释放。
- 适当的注释。
# 11. 简述一下你对 web 性能优化的方案(高级)
- 尽量减少
HTTP
请求。 - 使⽤浏览器缓存。
- 使⽤压缩组件。
- 图片、JavaScript的预载入。
- 将脚本放在底部。
- 将样式文件放在⻚⾯顶部。
- 使⽤外部的
JS
和CSS
。 - 精简代码。
# 12. new 操作符具体干了什么(高级)
- 创建⼀个空对象,并且 this 变量引⽤该对象,同时还继承了该函数的原型。
- 属性和⽅法被加入到 this 引⽤的对象中。
- 新创建的对象由 this 所引⽤,并且最后隐式的返回 this 。
# 13. js 的垃圾回收机制(高级)
什么是垃圾
一般来说:没有被引用的对象就叫做垃圾,还有一种可能是好几个变量之间相互引用形成闭环,没有其他的对象引用该闭环内的对象,也会被当做垃圾。
如何回收垃圾
浏览器会对内存中的变量进行标记,从根对象开始进行标记,层层递进,将被引用的进行标记,要是没有被引用就不标记,最后将没有标记的进行回收,释放内存。
# 14. 如何实现浏览器不同页面之间的通信(高级)
- 通过本地存储的方式。
- vue 中通过传值的几种方式。
- react 中的几种传值方法。
# 15. document.write 和 innerHtml 的区别(初级)
- document.write 会重绘页面整体,innerhtml 只是更新使用的那一部分。
# 16. 浏览器对象模型有哪些(中级)
document
location
screen
navigation
event
history
window中常用的是:一个事件onload
,两个定时器,三个弹出框。
# 17. dom 对象模型(中级)
document.documentElement 表示html 元素。
document.body 返回 body 元素。
document.head 返回 head 元素。
节点的添加操作1**【父加子】**
var h1=document.creatElement("h1") 父节点.appendchild(h1)
节点的添加操作2**【将新节点插入到目标节点之前】**
父节点.insertBefore(新节点, 目标节点)
删除操作
目标节点.remove()
# 18. 自定义属性的读写(中级)
setAttribute(“属性名称”,“属性值名称”)
getAttribute(“属性名称”)
oDiv.setAttribute("heihei",123);//写 document.write(oDiv.getAttribute("heihei"));//读
# 19. 数组去重的三种方法(中级)
方法一:
let arr=[3,5,3,1,3,1] function(arr){ for(var i=0;i<arr.length;i++){ for(var j=i+1;j<arr.length;j++){ if(arr[i]==arr[j]){ arr.splice(j,1) } } } return arr }
方法二:
let arr=[1,4,1,6,9] function(arr){ let newarr=[] this.arr.forEach((v,i)=>{ if(newarr.indexof(arr[i])==-1){ newarr.push(arr[i])) } }) return newarr }
方法三:
let arr=[1,4,1,6,9] function(arr){ let newarr=[] this.arr.forEach((v,i)=>{ if(!newarr.includes(arr[i])){ newarr.push(arr[i])) } }) return newarr }
# 20. 数组排序的三种方法(中级)
JavaScript中常见的三种数组排序方式,分别为:冒泡排序、选择排序、插入排序。
- 冒泡排序(Bubble Sort):
思路:
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
const arr = [1, 5, 7, 9, 16, 2, 4]
function bubbleSort(arr) {
const len = arr.length
// 外层循环用户控制比较的趟数
for (let i = 0; i < len - 1; i++) {
// 内层循环用于控制每趟比较的次数
for (let j = 0; j < len - i - 1; j++) {
// 如果前面一个数比后一个数大,就互换位置
if (arr[j] > arr[j + 1]) {
let temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
return arr
}
- 选择排序(Selection sort):
思路:
1、首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2、再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3、重复第二步,直到所有元素均排序完毕。
const arr = [1, 5, 7, 9, 16, 2, 4]
function selectionSort(arr) {
const len = arr.length
let minIndex, temp
for (let i = 0; i < len - 1; i++) {
minIndex = i
for (let j = i + 1; j < len; j++) {
// 找到最小的数
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
temp = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = temp
}
return arr
}
- 插入排序(Insertion sort):
思路:
1、将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2、从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
const arr = [1, 5, 7, 9, 16, 2, 4]
function insertionSort(arr) {
const len = arr.length;
let preIndex, current;
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
- 其它:
可以使用自带的sort排序,需要注意的是该方法会改变原始数组。
const arr = [1, 5, 7, 9, 16, 2, 4]
// x-y>0 则是递增
arr.sort((x, y) => x - y)
// x-y<0 则是递减
arr.sort((x, y) => y - x)
# 21. 面向对象(中级)
一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。
为了解决该问题,在 ES6 之前还没有出现类的概念的时候,采用的是函数来模拟类的实现,从而产生出可复用的对象创建方式。
ES5 之前通过构造函数实现面向对象:
function Student(id,name){ this.id=id this.name=name this.show=function(){ console.log(this.name+"的id号是"+this.id) } } let s1=new Student(1,"李") s1.show()
ES6 实现面向对象:
class Student{ constructor(id,name){ this.id=id this.name=name } show(){ console.log(this.name+"的id号是"+this.id) } } let s1=new Student(1,"李") s1.show()
# 22. JavaScript中什么是基本数据类型什么是引用数据类型?以及各个数据类型是如何存储的?(初级)
基本数据类型有
- Number
- String
- Boolean
- Null
- Undefined
- Symbol(ES6新增数据类型)
- bigInt
引用数据类型统称为 Object 类型,细分的话有
- Object
- Array
- Date
- Function
- RegExp
基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,每个对象在堆中有一个引用地址。引用类型在栈中会保存他的引用地址,以便快速查找到堆内存中的对象。
顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为 null,从而减少无用内存的消耗。
# 23. 在JS中为什么0.2+0.1>0.3?而0.2+0.3=0.5呢?(中级)
因为在 JS 中,浮点数是使用64位固定长度来表示的,其中的1位表示符号位,11位用来表示指数位,剩下的52位尾数位,由于只有52位表示尾数位。
而0.1
转为二进制是一个无限循环数0.0001100110011001100
......(1100循环)。
由于只能存储52位尾数位,所以会出现精度缺失,把它存到内存中再取出来转换成十进制就不是原来的0.1
了,就变成了0.100000000000000005551115123126
,而为什么 0.2 + 0.1 > 0.3 是因为:
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
0.2
和0.3
分别转换为二进制进行计算:在内存中,它们的尾数位都是等于52位的,而他们相加必定大于52位,而他们相加又恰巧前52位尾数都是0
,截取后恰好是0.1000000000000000000000000000000000000000000000000000
也就是0.5。
// 0.2 和 0.3 都转化为二进制后再进行计算
0.001100110011001100110011001100110011001100110011001101 +
0.0100110011001100110011001100110011001100110011001101 =
0.10000000000000000000000000000000000000000000000000001 //尾数为大于52位
// 而实际取值只取52位尾数位,就变成了
0.1000000000000000000000000000000000000000000000000000 //0.5
# 24. 那既然0.1不是0.1了,为什么在console.log(0.1)的时候还是0.1呢?(中级)
在console.log
的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串。
# 25. 判断数据类型有几种方法?(初级)
typeof
- 缺点:
typeof null
的值为Object
,无法分辨是null
还是Object
。
- 缺点:
instanceof
- 缺点:只能判断对象是否存在于目标对象的原型链上。
constructor
Object.prototype.toString.call()
一种最好的基本类型检测方式
Object.prototype.toString.call()
;它可以区分 null 、 string 、boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。
缺点:不能细分为谁谁的实例。
// -----------------------------------------typeof typeof undefined // 'undefined' typeof '10' // 'String' typeof 10 // 'Number' typeof false // 'Boolean' typeof Symbol() // 'Symbol' typeof Function // ‘function' typeof null // ‘Object’ typeof [] // 'Object' typeof {} // 'Object' // -----------------------------------------instanceof function Foo() { } var f1 = new Foo(); var d = new Number(1) console.log(f1 instanceof Foo);// true console.log(d instanceof Number); //true console.log(123 instanceof Number); //false -->不能判断字面量的基本数据类型 // -----------------------------------------constructor var d = new Number(1) var e = 1 function fn() { console.log("ming"); } var date = new Date(); var arr = [1, 2, 3]; var reg = /[hbc]at/gi; console.log(e.constructor);//ƒ Number() { [native code] } console.log(e.constructor.name);//Number console.log(fn.constructor.name) // Function console.log(date.constructor.name)// Date console.log(arr.constructor.name) // Array console.log(reg.constructor.name) // RegExp //-----------------------------------------Object.prototype.toString.call() console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" console.log(Object.prototype.toString.call(null)); // "[object Null]" console.log(Object.prototype.toString.call(123)); // "[object Number]" console.log(Object.prototype.toString.call("abc")); // "[object String]" console.log(Object.prototype.toString.call(true)); // "[object Boolean]" function fn() { console.log("ming"); } var date = new Date(); var arr = [1, 2, 3]; var reg = /[hbc]at/gi; console.log(Object.prototype.toString.call(fn));// "[object Function]" console.log(Object.prototype.toString.call(date));// "[object Date]" console.log(Object.prototype.toString.call(arr)); // "[object Array]" console.log(Object.prototype.toString.call(reg));// "[object RegExp]"
# 26. instanceof 原理是什么?(高级)
instanceof 原理实际上就是查找目标对象的原型链。
function myInstance(L, R) {//L代表instanceof左边,R代表右边
var RP = R.prototype
var LP = L.__proto__
while (true) {
if(LP == null) {
return false
}
if(LP == RP) {
return true
}
LP = LP.__proto__
}
}
console.log(myInstance({},Object));
# 27. 为什么 typeof null 是 Object ?(初级)
因为在JavaScript
中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object
类型,而null的二进制全是0,自然也就判断为Object
。
这个 bug 是初版本的 JavaScript 中留下的,扩展一下其他五种标识位:
000
对象1
整型010
双精度类型100
字符串110
布尔类型
# 28.==
和===
有什么区别?(初级)
===
是严格意义上的相等,会比较两边的数据类型和值大小。
- 数据类型不同返回false
- 数据类型相同,但值大小不同,返回false
==
是非严格意义上的相等。
两边类型相同,比较大小
两边类型不同,根据下方表格,再进一步进行比较
- Null == Undefined ->true
- String == Number ->先将 String 转为 Number,在比较大小
- Boolean == Number ->现将 Boolean 转为 Number,在进行比较
- Object == String,Number,Symbol -> Object 转化为原始类型
# 29. 手写call、apply、bind(中级)
- call 和 apply 实现思路主要是:
- 判断是否是函数调用,若非函数调用抛异常
- 通过新对象(context)来调用函数
- 给 context 创建一个
fn
设置为需要调用的函数 - 结束调用完之后删除
fn
- 给 context 创建一个
- bind 实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数
- 判断函数的调用方式,是否是被new出来的
- new出来的话返回空对象,但是实例的
__proto__
指向_this
的prototype
- new出来的话返回空对象,但是实例的
- 判断函数的调用方式,是否是被new出来的
- 完成函数柯里化
Array.prototype.slice.call()
call:
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1) //Array.from 把伪数组对象转为数组
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
apply:
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
blind:
Function.prototype.myBind = function(context){
// 判断是否是一个函数
if(typeof this !== "function") {
throw new TypeError("Not a Function")
}
// 保存调用bind的函数
const _this = this
// 保存参数
const args = Array.prototype.slice.call(arguments,1)
// 返回一个函数
return function F () {
// 判断是不是new出来的
if(this instanceof F) {
// 如果是new出来的
// 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,且完成函数柯里化
return new _this(...args,...arguments)
}else{
// 如果不是new出来的改变this指向,且完成函数柯里化
return _this.apply(context,args.concat(...arguments))
}
}
}
# 30. 字面量创建对象和 new 创建对象有什么区别,new 内部都实现了什么,手写一个 new(高级)
字面量:
- 字面量创建对象更简单,方便阅读
- 不需要作用域解析,速度更快
new 内部:
- 创建一个新对象
- 使新对象的
__proto__
指向原函数的prototype
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为 result
- 判断执行函数的结果是不是 null 或 Undefined,如果是则返回之前的新对象,如果不是则返回 result
手写new
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
# 31. 字面量new出来的对象和 Object.create(null)
创建出来的对象有什么区别(中级)
- 字面量和new创建出来的对象会继承 Object 的方法和属性,他们的隐式原型会指向 Object 的显式原型
- 而
Object.create(null)
创建出来的对象原型为 null,作为原型链的顶端,自然也没有继承 Object 的方法和属性
# 32. 什么是作用域,什么是作用域链?(中级)
- 规定变量和函数的可使用范围称作作用域
- 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链
# 33. 什么是执行栈,什么是执行上下文?(中级)
执行上下文分为:
- 全局执行上下文
- 创建一个全局的 window 对象,并规定 this 指向 window,执行 js 的时候就压入栈底,关闭浏览器的时候才弹出
- 函数执行上下文
- 每次函数调用时,都会新创建一个函数执行上下文
- 执行上下文分为创建阶段和执行阶段
- 创建阶段:函数环境会创建变量对象:arguments 对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定 this 指向;会确定作用域
- 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
- eval执行上下文
执行栈:
- 首先栈特点:先进后出
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
# 34. 什么是闭包?闭包的作用?闭包的应用?(中级)
函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用
作用:
- 保护
- 避免命名冲突
- 保存
- 解决循环绑定引发的索引问题
- 变量不会销毁
- 可以使用函数内部的变量,使变量不会被垃圾回收机制回收
应用:
- 设计模式中的单例模式
- for循环中的保留i的操作
- 防抖和节流
- 函数柯里化
缺点
- 会出现内存泄漏的问题
# 35. 什么是原型?什么是原型链?如何理解?(中级)
原型: 原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。
原型链: 多个__proto__
组成的集合成为原型链
- 所有实例的
__proto__
都指向他们构造函数的prototype
- 所有的
prototype
都是对象,自然它的__proto__
指向的是Object()
的prototype
- 所有的构造函数的隐式原型指向的都是
Function()
的显示原型 - Object 的隐式原型是 null
# 36. 说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点(中级)
原型继承、组合继承、寄生组合继承、ES6 的 extend
原型继承:
// ----------------------方法一:原型继承
// 原型继承
// 把父类的实例作为子类的原型
// 缺点:子类的实例共享了父类构造函数的引用属性 不能传参
var person = {
friends: ["a", "b", "c", "d"]
}
var p1 = Object.create(person)
p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性
console.log(p1);
console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性
组合继承:
// ----------------------方法二:组合继承
// 在子函数中运行父函数,但是要利用call把this改变一下,
// 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor
// 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
// 优点可传参,不共享父类引用属性
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
var s = new Son("ming", 20)
console.log(s);
寄生组合继承:
// ----------------------方法三:寄生组合继承
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
var s2 = new Son("ming", 18)
console.log(s2);
extend:
// ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
// 子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。
class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
constructor(y) {
super(200) // super(200) => Father.call(this,200)
this.y = y
}
}
# 37. 什么是内存泄漏?(中级)
内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏。
# 38. 为什么会导致的内存泄漏?(中级)
内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃。
# 39. 垃圾回收机制都有哪些策略?(中级)
- 标记清除法
- 垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
- 引用计数法
- 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
# 40. 手写浅拷贝深拷贝(高级)
// ----------------------------------------------浅拷贝
// 只是把对象的属性和属性值拷贝到另一个对象中
var obj1 = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
// 方式1
function shallowClone1(o) {
let obj = {}
for (let i in o) {
obj[i] = o[i]
}
return obj
}
// 方式2
var shallowObj2 = { ...obj1 }
// 方式3
var shallowObj3 = Object.assign({}, obj1)
let shallowObj = shallowClone1(obj1);
shallowObj.a.a1 = 999
shallowObj.b = true
console.log(obj1); //第一层的没有被改变,一层以下就被改变了
// ----------------------------------------------深拷贝
// 简易版
function deepClone(o) {
let obj = {}
for (var i in o) {
// if(o.hasOwnProperty(i)){
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
// }
}
return obj
}
var myObj = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
var deepObj1 = deepClone(myObj)
deepObj1.a.a1 = 999
deepObj1.b = false
console.log(myObj);
// 简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date
function deepClone2(o) {
if (Object.prototype.toString.call(o) === "[object Object]") { //检测是否为对象
let obj = {}
for (var i in o) {
if (o.hasOwnProperty(i)) {
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
}
return obj
} else {
return o
}
}
function isObject(o) {
return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
}
// 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap
function deepClone3(o) {
if (isObject(o)) {//检测是否为对象或者数组
let obj = Array.isArray(o) ? [] : {}
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
return obj
} else {
return o
}
}
// 有可能碰到循环引用问题 var a = {}; a.a = a; clone(a);//会造成一个死循环
// 循环检测
// 继续升级
function deepClone4(o, hash = new map()) {
if (!isObject(o)) return o//检测是否为对象或者数组
if (hash.has(o)) return hash.get(o)
let obj = Array.isArray(o) ? [] : {}
hash.set(o, obj)
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone4(o[i], hash)
} else {
obj[i] = o[i]
}
}
return obj
}
// 递归易出现爆栈问题
// 将递归改为循环,就不会出现爆栈问题了
var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
var b1 = { b: { c: { d: 1 } } }
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [ //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
{
parent: root,
key: undefined,
data: x,
}
];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent; //{} //{a:1,b:2}
const key = node.key; //undefined //c
const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' } //{ c1: 3, c2: { c21: 4, c22: 5 } }}
// 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
})
} else {
res[k] = data[k];
}
}
}
}
return root
}
function deepClone5(o) {
let result = {}
let loopList = [
{
parent: result,
key: undefined,
data: o
}
]
while (loopList.length) {
let node = loopList.pop()
let { parent, key, data } = node
let anoPar = parent
if (typeof key !== 'undefined') {
anoPar = parent[key] = {}
}
for (let i in data) {
if (typeof data[i] === 'object') {
loopList.push({
parent: anoPar,
key: i,
data: data[i]
})
} else {
anoPar[i] = data[i]
}
}
}
return result
}
let cloneA1 = deepClone5(a1)
cloneA1.c.c2.c22 = 5555555
console.log(a1);
console.log(cloneA1);
// ------------------------------------------JSON.stringify()实现深拷贝
function cloneJson(o) {
return JSON.parse(JSON.stringify(o))
}
// let obj = { a: { c: 1 }, b: {} };
// obj.b = obj;
// console.log(JSON.parse(JSON.stringify(obj))) // 报错 // Converting circular structure to JSON
深拷贝能使用hash递归的方式写出来就可以了 不过技多不压身,推荐还是看一看使用 while 实现深拷贝方法
# 41. 为什么JS是单线程的?(中级)
因为 JS 里面有可视的 Dom,如果是多线程的话,这个线程正在删除 DOM 节点,另一个线程正在编辑 Dom 节点,导致浏览器不知道该听谁的
# 42. 如何实现异步编程?(中级)
回调函数
# 43. Generator 是怎么样使用的以及各个阶段的变化如何?(高级)
- 首先生成器是一个函数,用来返回迭代器的
- 调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的
- 通过调用迭代器的 next 方法来请求一个一个的值,返回的对象有两个属性,一个是 value,也就是值;另一个是
done
,是个布尔类型,done为true 说明生成器函数执行完毕,没有可返回的值了 done
为true
后继续调用迭代器的 next 方法,返回值的value
为undefined
状态变化:
- 每当执行到
yield
属性的时候,都会返回一个对象 - 这时候生成器处于一个非阻塞的挂起状态
- 调用迭代器的 next 方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行
- 直到遇到下一次
yield
依次循环 - 直到代码没有
yield
了,就会返回一个结果对象done
为true
,value
为undefined
# 44. 说说 Promise 的原理?你是如何理解 Promise 的?(中级)
- 做到会写简易版的 promise 和 all 函数就可以
class MyPromise2 {
constructor(executor) {
// 规定状态
this.state = "pending"
// 保存 `resolve(res)` 的res值
this.value = undefined
// 保存 `reject(err)` 的err值
this.reason = undefined
// 成功存放的数组
this.successCB = []
// 失败存放的数组
this.failCB = []
let resolve = (value) => {
if (this.state === "pending") {
this.state = "fulfilled"
this.value = value
this.successCB.forEach(f => f())
}
}
let reject = (reason) => {
if (this.state === "pending") {
this.state = "rejected"
this.value = value
this.failCB.forEach(f => f())
}
}
try {
// 执行
executor(resolve, reject)
} catch (error) {
// 若出错,直接调用reject
reject(error)
}
}
then(onFulfilled, onRejected) {
if (this.state === "fulfilled") {
onFulfilled(this.value)
}
if (this.state === "rejected") {
onRejected(this.value)
}
if (this.state === "pending") {
this.successCB.push(() => { onFulfilled(this.value) })
this.failCB.push(() => { onRejected(this.reason) })
}
}
}
Promise.all = function (promises) {
let list = []
let count = 0
function handle(i, data) {
list[i] = data
count++
if (count == promises.length) {
resolve(list)
}
}
return Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(res => {
handle(i, res)
}, err => reject(err))
}
})
}
# 45. 以下代码的执行顺序是什么?(中级)
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1()
console.log('script start')
//执行到await时,如果返回的不是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行
//执行到await时,如果返回的是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码
//所以结果为
//async1 start
//async2
//script start
//async1 end
# 46. 宏任务和微任务都有哪些?(高级)
- 宏任务:
script
、setTimeOut
、setInterval
、setImmediate
- 微任务:
promise.then
,process.nextTick
、Object.observe
、MutationObserver
- 注意:Promise 是同步任务
# 47. 宏任务和微任务都是怎样执行的?(高级)
- 执行宏任务 script
- 进入 script 后,所有的同步任务主线程执行
- 所有宏任务放入宏任务执行队列
- 所有微任务放入微任务执行队列
- 先清空微任务队列
- 再取一个宏任务,执行,再清空微任务队列
- 依次循环
例题1
setTimeout(function(){
console.log('1')
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3')
});
console.log('4');
new Promise(function(resolve){
console.log('5');
resolve();
}).then(function(){
console.log('6')
});
setTimeout(function(){
console.log('7')
});
function bar(){
console.log('8')
foo()
}
function foo(){
console.log('9')
}
console.log('10')
bar()
解析
首先浏览器执行Js代码由上至下顺序,遇到 setTimeout,把 setTimeout 分发到宏任务 Event Queue中
new Promise 属于主线程任务直接执行打印2
Promis 下的 then 方法属于微任务,把 then 分到微任务 Event Queue 中
console.log(‘4’)属于主线程任务,直接执行打印4
又遇到 new Promise 也是直接执行打印5,Promise 下到 then 分发到微任务 Event Queue 中
又遇到 setTimouse 也是直接分发到宏任务 Event Queue 中,等待执行
console.log(‘10’)属于主线程任务直接执行
遇到 bar() 函数调用,执行构造函数内到代码,打印8,在 bar 函数中调用 foo 函数,执行 foo 函数到中代码,打印9
主线程中任务执行完后,就要执行分发到微任务 Event Queue 中代码,实行先进先出,所以依次打印3,6
微任务 Event Queue 中代码执行完,就执行宏任务 Event Queue 中代码,也是先进先出,依次打印1,7
- 最终结果:2,4,5,10,8,9,3,6,1,7
例题2
setTimeout(() => {
console.log('1');
new Promise(function (resolve, reject) {
console.log('2');
setTimeout(() => {
console.log('3');
}, 0);
resolve();
}).then(function () {
console.log('4')
})
}, 0);
console.log('5'); //5 7 10 8 1 2 4 6 3
setTimeout(() => {
console.log('6');
}, 0);
new Promise(function (resolve, reject) {
console.log('7');
// reject();
resolve();
}).then(function () {
console.log('8')
}).catch(function () {
console.log('9')
})
console.log('10');
运行结果: 5 7 10 8 1 2 4 6 3
# 48. 变量和函数怎么进行提升的?优先级是怎么样的?(中级)
对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值
- 开辟堆空间
- 存储内容
- 将地址赋给变量
对变量进行提升,只声明,不赋值,值为
undefined
# 49. var let const 有什么区别?(初级)
var
- var声明的变量可进行变量提升,let 和 const 不会
- var可以重复声明
- var在非函数作用域中定义是挂在到 window 上的
let
- let 声明的变量只在局部起作用
- let 防止变量污染
- 不可在声明
const
具有 let 的所有特征
不可被改变
- 如果使用 const 声明的是对象的话,是可以修改对象里面的值的
# 50. 箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?(中级)
箭头函数是普通函数的简写,但是它不具备很多普通函数的特性
第一点,this 指向问题,箭头函数的 this 指向它定义时所在的对象,而不是调用时所在的对象
不会进行函数提升
没有 arguments 对象,不能使用 arguments,如果要获取参数的话可以使用
rest
运算符没有
yield
属性,不能作为生成器 Generator 使用不能 new
没有自己的 this,不能调用 call 和 apply
没有 prototype,new 关键字内部需要把新对象的
_proto_
指向函数的 prototype
# 51. 说说你对代理的理解(中级)
代理有几种定义方式
- 字面量定义,对象里面的 get 和 set
- 类定义, class 中的
get
和set
- Proxy 对象,里面传两个对象,第一个对象是目标对象 target,第二个对象是专门放 get 和 set 的
handler
对象。Proxy 和上面两个的区别在于 Proxy 专门对对象的属性进行 get 和 set
代理的实际应用有
- Vue 的双向绑定 vue2 用的是
Object.defineProperty
,vue3 用的是proxy
- 校验值
- 计算属性值( get 的时候加以修饰)
- Vue 的双向绑定 vue2 用的是
# 52. 为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?(中级)
为什么要使用模块化
- 防止命名冲突
- 更好的分离,按需加载
- 更好的复用性
- 更高的维护性
# 53. exports
和module.exports
有什么区别?(中级)
导出方式不一样
exports.xxx='xxx'
module.export = {}
exports
是module.exports
的引用,两个指向的是用一个地址,而 require 能看到的只有module.exports
# 54. JS模块包装格式有哪些?(高级)
commonjs
- 同步运行,不适合前端
AMD
异步运行
异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数
CMD
- 异步运行
- seajs 规范
# 55. ES6和commonjs的区别(中级)
commonjs
模块输出的是值的拷贝,而 ES6 输出的值是值的引用commonjs
是在运行时加载,是一个对象,ES6 是在编译时加载,是一个代码块commonjs
的 this 指向当前模块,ES6的 this 指向 undefined
# 56. 冒泡算法排序(中级)
// 冒泡排序
/* 1.比较相邻的两个元素,如果前一个比后一个大,则交换位置。
2.第一轮的时候最后一个元素应该是最大的一个。
3.按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。 */
function bubbleSort(arr) {
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr.length; j++) {
if (arr[j] > arr[j + 1]) {
var temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
}
var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
console.log(Arr, "before");
bubbleSort(Arr)
console.log(Arr, "after");
# 57. 快速排序(中级)
/*
快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。
然后递归调用,在两边都实行快速排序。
*/
function quickSort(arr) {
if (arr.length <= 1) {
return arr
}
var middle = Math.floor(arr.length / 2)
var middleData = arr.splice(middle, 1)[0]
var left = []
var right = []
for (var i = 0; i < arr.length; i++) {
if (arr[i] < middleData) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat([middleData], quickSort(right))
}
var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
console.log(Arr, "before");
var newArr = quickSort(Arr)
console.log(newArr, "after");
# 58. 插入排序(中级)
function insertSort(arr) {
// 默认第一个排好序了
for (var i = 1; i < arr.length; i++) {
// 如果后面的小于前面的直接把后面的插到前边正确的位置
if (arr[i] < arr[i - 1]) {
var el = arr[i]
arr[i] = arr[i - 1]
var j = i - 1
while (j >= 0 && arr[j] > el) {
arr[j+1] = arr[j]
j--
}
arr[j+1] = el
}
}
}
var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
console.log(Arr, "before");
insertSort(Arr)
console.log(Arr, "after");
# 59. 是否回文(中级)
function isHuiWen(str) {
return str == str.split("").reverse().join("")
}
console.log(isHuiWen("mnm"));
# 60. 正则表达式,千分位分隔符(中级)
function thousand(num) {
return (num+"").replace(/\d(?=(\d{3})+$)/g, "$&,")
}
console.log(thousand(123456789));
# 61. 斐波那契数列(中级)
// num1前一项
// num2当前项
function fb(n, num1 = 1, num2 = 1) {
if(n == 0) return 0
if (n <= 2) {
return num2
} else {
return fb(n - 1, num2, num1 + num2)
}
}
# 62. 数组去重的方式(中级)
var arr = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
// 最low1
let newArr2 = []
for (let i = 0; i < arr.length; i++) {
if (!newArr2.includes(arr[i])) {
newArr2.push(arr[i])
}
}
console.log(newArr2);
// 最low2
let arr2 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
for (let i = 0; i < arr2.length; i++) {
var item = arr2[i]
for (let j = i + 1; j < arr2.length; j++) {
var compare = arr2[j];
if (compare === item) {
arr2.splice(j, 1)
j--
}
}
}
console.log(arr2);
// 基于对象去重
let arr3 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
let obj = {}
for (let i = 0; i < arr3.length; i++) {
let item = arr3[i]
if (obj[item]) {
arr3[i] = arr3[arr3.length - 1]
arr3.length--
i--
continue;
}
obj[item] = item
}
console.log(arr3);
console.log(obj);
// 利用Set
let newArr1 = new Set(arr)
console.log([...newArr1]);
let arr4 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
//利用reduce
newArr4 = arr4.reduce((prev, curr) => prev.includes(curr)? prev : [...prev,curr],[])
console.log(newArr4);
console.log(document);
# 63. 事件冒泡和事件捕捉有什么区别?(中级)
事件冒泡
- 在 addEventListener 中的第三属性设置为 false (默认)。
- 从下至上(儿子至祖宗)执行。
事件捕捉
在 addEventListener 中的第三属性设置为 true。
从上至下(祖宗到儿子)执行。
# 64. 什么是防抖?什么是节流?手写一个(高级)
- 防抖
- n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。
- 节流
- n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。
// ---------------------------------------------------------防抖函数
function debounce(func, delay) {
let timeout
return function () {
let arg = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func(arg)
}, delay);
}
}
// ---------------------------------------------------------立即执行防抖函数
function debounce2(fn, delay) {
let timer
return function () {
let args = arguments
if (timer) clearTimeout(timer)
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) { fn(args) }
}
}
// ---------------------------------------------------------立即执行防抖函数+普通防抖
function debounce3(fn, delay, immediate) {
let timer
return function () {
let args = arguments
let _this = this
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) { fn.apply(_this, args) }
} else {
timeout = setTimeout(() => {
func.apply(_this, arguments)
}, delay);
}
}
}
// ---------------------------------------------------------节流 ,时间戳版
function throttle(fn, wait) {
let previous = 0
return function () {
let now = Date.now()
let _this = this
let args = arguments
if (now - previous > wait) {
fn.apply(_this, arguments)
previous = now
}
}
}
// ---------------------------------------------------------节流 ,定时器版
function throttle2(fn, wait) {
let timer
return function () {
let _this = this
let args = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(_this, arguments)
}, wait);
}
}
}
# 65. 函数柯里化原理是什么?(高级)
function add() {
var args = Array.prototype.slice.call(arguments)
var adder = function () {
args.push(...arguments)
return adder
}
adder.toString = function () {
return args.reduce((prev, curr) => {
return prev + curr
}, 0)
}
return adder
}
let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)
# 66. 什么是requestAnimationFrame?(高级)
- requestAnimationFrame 请求数据帧可以用做动画执行。
- 可以自己决定什么时机调用该回调函数。
- 能保证每次频幕刷新的时候只被执行一次。
- 页面被隐藏或者最小化的时候暂停执行,返回窗口继续执行,有效节省 CPU。
var s = 0
function f() {
s++
console.log(s);
if (s < 999) {
window.requestAnimationFrame(f)
}
}
window.requestAnimationFrame(f)
# 67. js常见的设计模式(高级)
单例模式、工厂模式、构造函数模式、发布订阅者模式、迭代器模式、代理模式。
单例模式
不管创建多少个对象都只有一个实例:
var Single = (function () { var instance = null function Single(name) { this.name = name } return function (name) { if (!instance) { instance = new Single() } return instance } })() var oA = new Single('hi') var oB = new Single('hello') console.log(oA); console.log(oB); console.log(oB === oA);
工厂模式
代替new创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同):
function Animal(o) { var instance = new Object() instance.name = o.name instance.age = o.age instance.getAnimal = function () { return "name:" + instance.name + " age:" + instance.age } return instance } var cat = Animal({name:"cat", age:3}) console.log(cat);
构造函数模式
发布订阅者模式
class Watcher { // name模拟使用属性的地方 constructor(name, cb) { this.name = name this.cb = cb } update() {//更新 console.log(this.name + "更新了"); this.cb() //做出更新回调 } } class Dep {//依赖收集器 constructor() { this.subs = [] } addSubs(watcher) { this.subs.push(watcher) } notify() {//通知每一个观察者做出更新 this.subs.forEach(w => { w.update() }); } } // 假如现在用到age的有三个地方 var w1 = new Watcher("我{{age}}了", () => { console.log("更新age"); }) var w2 = new Watcher("v-model:age", () => { console.log("更新age"); }) var w3 = new Watcher("I am {{age}} years old", () => { console.log("更新age"); }) var dep = new Dep() dep.addSubs(w1) dep.addSubs(w2) dep.addSubs(w3) // 在Object.defineProperty 中的 set中运行 dep.notify()
代理模式
迭代器模式
# 68. JS 性能优化的方式有哪些?(中级)
- 垃圾回收。
- 闭包中的对象清楚。
- 防抖节流。
- 分批加载(setInterval,加载10000个节点)。
- 事件委托。
- 少用 with。
- requestAnimationFrame 的使用。
- script 标签中的 defe r和 async。
- CDN。
# 69. js延迟加载的方式有哪些?(中级)
defer和async、动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBack)、按需异步载入js
# 70. 请解释一下 JavaScript的同源策略。(高级)
概念:同源策略是客户端脚本(尤其是Javascript)的重要的安全度量标准。它最早出自Netscape Navigator2.0,其目的是防止某个文档或脚本从多个不同源装载。 这里的同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议。
var xmlHttp = new XMLHttpRequest(); xmlHttp.open('GET','demo.php','true'); xmlHttp.send() xmlHttp.onreadystatechange = function(){ if(xmlHttp.readyState === 4 & xmlHttp.status=== 200){ } }指一段脚本只能读取来自同一来源的窗口和文档的属性。
指一段脚本只能读取来自同一来源的窗口和文档的属性。