前端必须掌握的JavaScript基础知识

824 阅读17分钟

做为前端三剑客的JavaScript,可以说是重中之重了,是前端开发必不可缺的基础,因为有了JavaScript,才使静态的页面产生了一些动态的效果,你对JavaScript了解有多少?本文整理了个人学习中一些Javascript基础知识,以便查漏补缺。未经允许,禁止转载,持续更新中...

1. 说一下你了解的主要浏览器及其内核

浏览器内核.png

2. js中数据类型分为几种,都有哪些

  • 简单数据类型: numberstringBooleannullundefinedsymbol

  • 复杂数据类型: objectarrayfunction

    注意: 简单数据类型是没有属性和方法的且简单数据类型的值不可改变,保存复制的是值本身,储存在栈内存;引用数据保存与复制的是一个地址,改变其中一个另一个也会改变

3. 栈内存与堆内存的了解

栈内存堆内存.png

  • 栈内存 栈内存储存的都是一些比较简单的数据和堆内存的地址,举个例子

简单数据类型值.png

图中可以看出当改变了第一个num的值,num1的值不会因为num的值改变而改变,也再次印证了简单数据类型保存复制的是值本身

  • 堆内存 堆内存储存的是一些比较复杂的数据,数据储存在堆内存,数据的地址保存在栈内存里面,举个例子

Snipaste_2021-07-14_14-51-50.png

图中可以看出当改变了obj的name属性,obj1的name属性也跟着改变了,原因是 引用类型复制的是引用地址,obj和obj1指向的是堆内存中同一块空间,无论改变obj还是obj1 其实都是改变的同一个数据

3. typeof返回值有哪些

numberstringBooleanundefinedobjectfunction

  • typeof typeof 返回 string

  • typeof null 、 typeof 对象 、 typeof 数组 都返回object

  • 返回false的情况: 0 "" null false NaN undefined 不成立的表达式

4. 如何检测一个对象是不是数组

  • 第一种方法: instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

instanceOf检测数组.png

分析过程:

图中可知 arr是Array构造出来的实例,arr.__proto__属性指向了构造它的构造函数的原型对象Array.prototype,由此得知 arr对象的原型链上有Array构造函数的prototype属性。而arr.constructor指向了构造它的构造函数,因为arr实例没有constructor属性,就去原型链找,原型链上有constructor ,由此得知 arr.constructor === Array

注意: 但是此方法有漏洞,用此方法判断不一定准确,因为我们可以改变一个对象的this指向,这里就不做过多叙述了

  • 第二种方法: Object.prototype.toString 借用Object原型上的方法来实现检测数组

Object.prototype检测数组.png

分析过程:

toString方法是Object原型上的方法,像functionArray作为Object的实例,重写了toString的方法。而我们通过原型链的知识可以知道,我们在调用toString方法时,并不能直接调用Object原型上的toString方法,调用的是重写后的方法。所以,我们想得到对象具体类型的话,应该使用call借调Object原型上的toString方法,这点MDN文档也有说明

  • 第三种方法: Array.isArray此方法接收一个参数,用于确定传递的值是否是一个 Array

Array.isArray.png

分析过程:

此方法为ES5新增的方法,可以直接判断参数是不是一个数组类型,MDN文档说明

5. 自增自减运算符和逻辑运算符

  • ++运算符: 写到后面叫做后自增,写到前面叫做先自增
let a = 10
let b = ++a + a++
console.log(a) // 12
console.log(b) // 22
//分析: ++a 是a先加1赋值给自己,此时a是11,再加上11,所以b等于11 + 11 = 22,最后a++,a变成了12

let a = 5
let b = a++ + ++a + a++
console.log(b)  // 19
console.log(a) // 8

let a = 10
let b = a-- + ++a
console.log(b) // 20
console.log(a) // 10
  • --运算符: 和自增运算符同,就不多说了

  • 逻辑运算符

    • && : 假前真后,全真才为真 有一个假就是假

    • || : 真前假后,全假才为假 有一个真就是真

    • ! : 取反 转换为布尔值

6. 冒泡排序

  • 初级版本

    • 让数组中每个值都两两进行比较一趟的结果,最大的数会在数组的最后面
    let arr = [23, 46, 45, 39, 66, 82]
    for (let i = 0; i < arr.length - 1; i++) {
        if (arr[i] > arr[i + 1]) {
            let temp = arr[i]
            arr[i] = arr[i + 1]
            arr[i + 1] = temp
        }
    }
    console.log(arr)
    //分析:数组从arr[0]两两和后面数相比,一共比五次。所以让length - 1,让i最大取值到4就可以了。当i取值到4的时候 arr[4]和arr[5]相比,也就是arr.length - 2 和 arr.length - 1相比
    
    • 让两两比较的结果多次运行,就会一次一次的把最大的数往后排,于是外面套一个for循环控制比多少趟
    for (j = 0; j < arr.length - 1; j++) {
        for (let i = 0; i < arr.length - 1; i++) {
          if (arr[i] > arr[i + 1]) {
                let temp = arr[i]
                  arr[i] = arr[i + 1]
                  arr[i + 1] = temp
          }
          }
    }
    
  • 高级版本

    //第0趟  比较5次  多比了0次  
    //第1趟  比较4次  多比了1次  找到了1个最大的数
    //第2趟  比较3次  多比了2次  找到了2个最大的数
    //思路:遍历第一圈的时候,两两相比 一共比了5次,第二圈的时候由于已经找到了1个最大的数,此圈少比一次就可以了。但是for循环此时还是走了5圈,所以此时是多比了一次的 ,以此类推
    let arr = [23, 46, 45, 39, 66, 82]
    for (j = 0; j < arr.length - 1; j++) {
        for (let i = 0; i < arr.length - 1 - j; i++) {  // 让length再减j
            if (arr[i] > arr[i + 1]) {
                let temp = arr[i]
                    arr[i] = arr[i + 1]
                    arr[i + 1] = temp
            }
            }
    }
    
  • 终极版本 如果在排序过程中,发现数组已经排好序了,后面的次数就没必要排了

    • 假设成立法
    //判断数组中的成绩是否都及格了  
    let arr = [80,100,90,65,54]
    //第一种思路:
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] >= 60) {
            console.log('都及格了')   
        }else {
            console.log('不及格')
        }
    }
    //第二种思路:一旦找到小于60的 就代表并不是都及格了
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < 60) {
            console.log('并未都及格')
    }
    

    思路:排序过程中,假设数组是有顺序的那么就不用再次排序了。也就是说只要找到后一项比前一项大,那么假设就不成立

    for (let i = 0; i < arr.length; i++) {
        if (arr[i] > arr[i + 1]) {
            console.log('假设失败')
    }
    
    • 优化冒泡排序
    let arr = [23, 46, 45, 39, 66, 82]
    for (j = 0; j < arr.length - 1; j++) {
        let flag = true
        for (let i = 0; i < arr.length - 1 - j; i++) {
        	if (arr[i] > arr[i + 1]) {
                flag = false
                let temp = arr[i]
            	arr[i] = arr[i + 1]
            	arr[i + 1] = temp
        	} else if (flag == true){
                break
            }
    	}
    }
    

7. 简述下浅拷贝与深拷贝

  • 浅拷贝: 拷贝的是对象的一层属性,如果对象里面还有对象,则只会拷贝地址,修改其中一个会相互影响,适合拷贝简单数据类型
let obj = {
    name: 'zs',
    age: 18,
    money: 1000
}
function Copy(obj) {
    let newObj = {}
    for (let k in obj) {
        newObj[k] = obj[k]
    }
    return newObj
}
console.log(Copy(obj))
  • 深拷贝: 拷贝对象的多层对象,如果对象里面还有对象,使用递归的方式去实现
let obj = {
    name: 'zs',
    age: 18,
    money: 1000,
    smoke: {
        brand: 'suyan',
        num: 20
    }
}
function Copy(obj) {
    let newObj = {}
    for (let k in obj) {
        newObj[k] = typeof obj[k] === 'object'? Copy(obj[k]) : obj[k]
    }
    return newObj
}
console.log(Copy(obj))  // 修改obj不会影响到newObj

8. 说一下你对原型和原型链的理解

函数都有prototype属性,这个属性是一个对象,我们称之为原型对象;每一个对象都有__proto__属性,该属性指向了原型对象,原型对象也是对象,也有__proto__属性,这样一层一层形成了链式结构,我们称之为原型链

9. 闭包的理解

相互嵌套关系的两个函数,当内部函数引用外部函数的变量的时候就形成了闭包,闭包将会导致原有的作用域链不释放,造成内存泄露。有些地方说内部函数被保存到外部的时候形成闭包其实是不具体的,保存到外部只是为了方便调用内部的这个函数,而函数嵌套的原因则是因为需要局部变量,如果是全局变量就达不到闭包的目的了

  • 闭包的优点: 形成私有空间,避免全局污染;持久化内存,保存数据

  • 闭包的缺点: 持久化内存导致的内存泄露,解决办法 尽量避免函数的嵌套;执行完的变量赋值为null,让垃圾回收机制回收释放内存

经典案例:点击li获取下标

<ul>
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
  <li>555</li>
</ul>
  var lis = document.querySelectorAll('li')
  for (var i = 0; i < lis.length; i++) {
    (function (j) {
      lis[j].onclick = function () {
        console.log(j)
      }
    })(i)
  }

10. call、apply、bind方法的区别

  • call和apply方法都可以调用函数,方法内的第一个参数可以修改this指向

  • call方法可以有多个参数,除了第一个参数标识this指向,其他参数作为函数的实参传递给函数; apply方法最多有两个参数,第一个参数标识this指向,第二个参数是一个数组或者伪数组,数组里面的每一项作为函数的实参传递给函数

  • bind方法不能自动调用函数,它会创建一个副本函数,并且绑定新函数的this指向bind返回的新函数

11. 伪数组有哪些

  • 函数参数列表 arguments

  • DOM 对象列表 和 childNodes 子节点列表

  • jquery对象 比如$("div")

12. 伪数组和真数组有什么区别,伪数组如何转换为真数组呢

区别

  • 伪数组其实是一个对象,真数组是Array

  • 伪数组拥有length属性,但长度不可以改变,真数组长度是可以改变的

  • 伪数组不具备真数组的方法,比如 push 、 slice等等

转换

  • call借调数组方法

伪数组转真数组.png

  • ES6新语法 Array.from方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例

伪数组转真数组1.png

  • ES6新语法 扩展运算符

Snipaste_2021-07-12_22-53-25.png

扩展运算符转数组.png

这里有个注意的点,使用自己定义的伪数组的时候,扩展运算符无法转换成真数组,百度了下才知道自己定义的伪数组由于缺少遍历器Iterator会报错

13. 了解过数组的降维(扁平化)吗

  • 借调数组原型上的concat方法

    let arr = [1,2,3,[4,5]]
    Array.prototype.concat.apply([], arr)
    
  • 使用数组的concat方法和扩展运算符

    let arr = [1,2,3,[4,5]]
    [].concat(...arr)
    

注意: 以上两种方法只能实现一层嵌套,如果是多层的嵌套就用下面两个方法

  • 利用Array.some方法判断数组中是否还存在数组,如果存在用展开运算符配合concat连接数组

    let arr = [1,2,3,[4,[5,6]]]
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    console.log(arr);
    
  • ES6中的flat函数实现数组的扁平化

    let arr = [1,2,3,[4,[5,6]]]
    arr.flat( Infinity )
    //flat方法的infinity属性,可以实现多层数组的降维
    

14. var const let 有哪些不同

  • var声明的变量存在变量提升,let const无

  • var可以重复声明同名变量, let const 不可以,会报错 has already been declared

  • let const 声明变量有块级作用域,var没有

  • const定义的变量是常量不能修改,但是如果是对象或者数组可以修改属性,增加属性

const常量.png

const数组.png

14. this指向问题

  • 函数调用模式, this指向window

  • 构造函数调用模式, this指向新创建的实例对象

  • 方法调用模式, this指向调用方法的对象

  • 上下文调用模式, call和apply方法中, this指向方法内的第一个参数;bind方法中, bind创建的新函数的this绑定为bind方法中新的函数

  • 在事件处理函数中,this指向触发事件的当前元素

  • 定时器中,this指向window

  • 箭头函数中没有this指向问题,它的this和外层作用域的this保持一致

  • 匿名函数中的this总是指向window

15. 你是如何理解面向对象的,它和面向过程有什么区别

  • 面向对象是一种软件开发的思想,就是把程序看作一个对象,将属性和方法封装中,以提高代码的灵活性、复用性、可扩展性。面向对象有三大特性:封装 继承 多态。封装指的是把属性或者方法储存在对象中的能力,继承指的是由另一个类得来的属性和方法的能力,多态指的是编写能以多种方法运行的函数或方法的能力。面向对象开发优点是易维护 易扩展 降低工作量,缩短开发周期,缺点是性能低

  • 面向过程是一种以过程为中心的编程思想,就是把解决问题分为一个一个的步骤,先干什么后干什么,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向过程的优点就是性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素,缺点就是没有面向对象那样易维护、易扩展、易复用

16. 数组去重的方法

//第一种方法
const arr = [1,2,3,3,4,5,5,5,7]
const newArr = []
arr.forEach(item =>{
    if(!newArr.includes(item)) {
        newArr.push(item)
	}
})
console.log(newArr)
//第二种方法 es6新增一个set方法 set有一个特征,内部的值不允许重复
const arr = [1,2,3,3,4,5,5,5,7]
const set = new Set(arr)
const newArr = [...set]  // ... 展开运算符 可以展开数组或者对象
//第三种方法 使用indexof方法
let arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
let newArr = []
for (let i = 0; i < arr.length; i++) {
 if (newArr.indexOf(arr[i]) === -1) {
   newArr.push(arr[i])
 }
}
//第四种方法 sort方法
let arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
arr.sort()
let newArr = [arr[0]]
for (let i = 1; i < arr.length; i++) {
 if (arr[i] !== newArr[newArr.length - 1]) {
   newArr.push(arr[i])
 }
}

17. 求两个数组的交集

const a = [1,2,2,2,3,4,5]
const b = [2,2,3,3]
//第一种方法:
let arr = a.filter(item => b.includes(item)) //判断b里面是否包含item 此时会把数组a的每一位拿出来去看看b里面有没有 有的话就返回true 而filter方法只要是true的都会保留  a数组里面有3个2 b里面是有2的,所以a里面3个2都保留了  arr = [2,2,2,3].但我们要求的是交集,多了一个2,此方法求数组交集考虑不周 行不通

//第二种方法
let arr = a.filter(item => {
    const idx = b.indexOf(item)
    if(idx !== -1) {
        b.splice(idx,1)
        return true
	} else {
        return false
    }
})

18. 求数组最大值

const arr = [1,2,3,3,5,7,5]
Math.max() //不接收数组作为参数 只可以接收1,2,3,4 这种值 所以调用apply方法借数组的来用
Math.max.apply(arr,arr) //第一种方法
Math.max(...arr) //第二种方法

19. for...in和for...of区别

  • for...in是遍历数组、对象的key

  • for...of是遍历数组的value

20. 作用域链

访问变量的时候,现在当前作用域中查找,找不到就会去外层作用域中找,一层一层的查找形成了作用域链,找不到的话就返回undefined

21. 怎么理解事件循环机制

Js是一门单线程的语言,执行的时候可能会出现阻塞的情况,所以js分了同步和异步任务。同步任务和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的会丢给浏览器执行,浏览器读取异步的代码并把异步任务的回调函数加入到任务队列中,js主线程的任务执行完后,会去读取任务队列的回调,推入主线程执行,js不断重复上面的过程形成事件循环,也就是event loop

事件循环.png

异步任务有哪些: JavaScript 中常见的异步函数有:定时器,事件和 ajax 等

22.宏任务 微任务

js把任务队列分为宏任务和微任务队列,每一个宏任务执行完后,都会清空一下微任务队列,再继续执行下一个宏任务

  • 宏任务
    • 整个script标签
    • 异步ajax请求
    • setTimeout、setInterval
  • 微任务
    • Promise
    • process.nextTick

一般是这几个,其他的暂且不知道,来几道面试题

console.log(1)

setTimeout(function() {
  console.log(2)
}, 0)

const p = new Promise((resolve, reject) => {
  resolve(1000)
})
p.then(data => {
  console.log(data)
})

console.log(3)
// 输出结果 1 3 1000 2
/**分析: 同步执行 1 然后遇到定时器异步任务丢给浏览器,
执行同步promise 把promise状态标记为resolve 
然后then是异步微任务 不执行暂时  接着执行同步 3
然后去清空微任务列表 执行data 输出1000
最后执行宏任务定时器 输出 2
**/
console.log(1)
setTimeout(function() {
  console.log(2)
  new Promise(function(resolve) {
    console.log(3)
    resolve()
  }).then(function() {
    console.log(4)
  })
})

new Promise(function(resolve) {
  console.log(5)
  resolve()
}).then(function() {
  console.log(6)
})
setTimeout(function() {
  console.log(7)
  new Promise(function(resolve) {
    console.log(8)
    resolve()
  }).then(function() {
    console.log(9)
  })
})
console.log(10)
// 输出 1 5 10 6 2 3 4 7 8 9

23. 你对Promise了解多少

其实Promise就是一个js的对象,它使异步的操作具备了同步的效果,回调函数不必一层层的嵌套。每一个异步的任务立刻返回一个Promise对象,这个Promise对象有三种状态pending resolved rejected,成功状态变为resolved 失败状态变为rejected,且状态是不可逆的

  • Promise.then 原型上的方法 作用是为Promise实例状态改变时的回调函数,接受两个回调函数作为参数,第一个回调函数是Promise对象状态变为resolved时调用,第二个回调函数是状态变为rejected时调用。其中第二个函数可以不用。

  • Promise.catch 原型上的方法 用于指定发生错误时的回调函数

  • Promise.finally 原型上的方法 作用是不管Promise对象状态最后如何,都会执行这个回调函数

  • Promise.resolve() 自身api

    • 不传参 返回一个新的状态为resolve的Promise对象

    • 传参 参数是一个Promise实例,返回当前的Promise实例

  • Promise.reject() 返回一个值,返回的值会传递到下一个then的resolve方法参数中

  • Promise.all() 参数接收异步操作,可以把多个异步的操作同步进行,等所有异步操作执行完后才执行回调

  • Promise.race() 哪个异步结果返回的最快就是哪个结果,不管成功还是失败

24. async 和 await

async用于修饰一个函数是异步的,await等待异步操作的结果,async函数返回的是一个Promise对象,两者是搭配使用的

25. es6-es10新增的常用方法

es6:
1letconst
2、解构赋值  let { a, b } = { a: 1, b: 2 }
3、箭头函数
4、字符串模板
5、扩展运算符
6、数组方法:map、filter等等
7、类:class关键字
8、promise
9、函数参数默认值 fn(a = 1) {}
10、对象属性简写 let a = 1; let obj = {a}
11、模块化:import--引入、exprot default--导出

es7:
1includes()方法,用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回true,否则返回falsees8:
1async/await

es9:
1Promise.finally() 允许你指定最终的逻辑

es10:
1、数组Arrayflat()和flatmap()
   flat:方法最基本的作用就是数组降维
      var arr1 = [1, 2, [3, 4]];
            arr1.flat(); 
            // [1, 2, 3, 4]

        var arr3 = [1, 2, [3, 4, [5, 6]]];
        arr3.flat(2);
        // [1, 2, 3, 4, 5, 6]

        //使用 Infinity 作为深度,展开任意深度的嵌套数组
        arr3.flat(Infinity); 
        // [1, 2, 3, 4, 5, 6]
   flatmap:方法首先使用映射函数映射(遍历)每个元素,然后将结果压缩成一个新数组

26. 函数的防抖和节流是什么,你在项目中哪些地方用到了防抖和节流

防抖指事件触发后 在这个时间内只执行一次,如果在这个时间内又重复触发了该事件,则会重新计算执行时间。类似游戏中的法师放技能读进度条和回城效果

  • 防抖应用场景:

    • 登录按钮,防止用户一直点击不停的发送请求,可以使用防抖来避免

    • 调整浏览器窗口大小时,resize 次数过于频繁,此时可以使用防抖一次到位

    • 文本编辑器实时保存,当无任何更改操作一秒后进行保存

    • input输入框搜索的时候,防止多次发送请求造成性能浪费,可以使用防抖处理,当用户停止输入一段时间后再发送请求

  • 防抖实现

var timerId = null
document.querySelector('.ipt').onkeyup = function () {
    // 防抖
    if (timerId !== null) {
      clearTimeout(timerId)
    }
    timerId = setTimeout(() => {
      console.log('我是防抖')
    }, 1000)
}

节流指这个时间段内无论触发多少次该事件,它也只会在这个时间段内只执行一次函数,不会重新计算执行时间。类似游戏里的平a吧,速度是固定的,触发多少次也只会在固定时间内攻击一下

  • 节流应用场景:

    • 轮播图固定一段时间转换一张图片
  • 节流实现

document.querySelector('.ipt').onkeyup = function () {
// 节流
    console.log(2)
    if (timerId !== null) {
      return
    }

    timerId = setTimeout(() => {
      console.log('我是节流')
      timerId = null
    }, 1000)
}

27. new操作符都做了什么

  • 创建了一个新对象

  • 执行了构造函数并且将对象与构造函数链接起来

  • 改变了构造函数的this指向新创建的对象

  • 返回这个对象

28. js实现继承的几种方式

ES5中实现继承的方式

  • 借用构造函数继承
function Super(name, age) {
  this.name = name
  this.age = 20
}
function Person(name, age) {
  //改变Super的this指向,
  //所以此时的Person实例上也会有Super的所有属性
  Super.call(this, name)
  this.age = age
}

注意: 构造函数继承无法继承原型上的属性方法

构造函数继承.png

  • 原型继承
function Super(name, age) {
  this.name = name
  this.age = age
  this.leg = 2
}
Super.prototype.say = function () {
  console.log('大家好', this.name)
}
function Person(name, age) {
  this.name = name
  this.age = age
}
/**
如果用实例作为Person的原型对象,那么Person原型对象会出现多余属性的情况
**/
Person.prototype = new Super()
Person.prototype.constructor = Person

这里穿插一句 用构造函数继承属性,原型继承方法就实现了组合式继承

  • 寄生式继承
function Super(name, age) {
  this.name = name
  this.age = age
  this.legs = 2
}
Super.prototype.say = function () {
  console.log('大家好', this.name)
}
function Person(name, age) {
  this.name = name
  this.age = age
}
// 这样继承的原型方法就是干净的,没有多余的属性
// Object.create ES5新出的语法能够生成一个新对象,
//如果参数直接传一个构造函数,新对象会集成obj对象的属性和方法,
Person.prototype = Object.create(Super.prototype) // 这里只继承原型方法
Person.prototype.constructor = Person

这里加上构造函数继承就是组合式寄生继承了

ES6中实现继承的方式

  • class 和 extends
// class 和 extends是语法糖,本质上是通过原型链实现的继承
class Animal {
  constructor(name) {
    this.name = name
  }
  eat() {
    console.log('都会吃')
  }
}
// Dog
class Dog extends Animal {
  constructor(name, color) {
    super(name)
    this.color = color
  }
  // 吠
  bark() {
    console.log('汪汪汪')
  }
}

const d = new Dog('金毛', '金色')
console.log(d)

打印结果: 狗继承了name和eat,自己身上有个bark

es6继承.png