js常考

167 阅读11分钟

作用域

作用域也叫执行上下文,规定了执行代码对变量的访问权限。

全局作用域和函数作用域
  1. 全局作用域
  • 最外层函数和最外层函数外面定义的变量拥有全局作用域。
  • 所有未定义直接赋值的变量自动声明为全局作用域。
  • 所有window对象的属性拥有全局作用域。
  1. 函数作用域
  • 声明在函数内部的变量拥有函数作用域
块级作用域

let,const声明的变量具有块级作用域,块级作用域可以在函数中创建也可以在代码块中创建。

var,let,const的区别

  1. var存在变量提升,声明的变量会挂载在windows上,可以重复声明。
  2. let,const具有块级作用域、不存在变量提升、暂时性死区、不能重复声明

注意:const声明之后必须马上赋值,否则会报错;const声明的基本类型不能更改,声明的复杂类型不能更改内存地址,可以更改内部属性。

在当前作用域中查找变量时,没有找到就会从父级作用域查找,还没有找到就依次向上查找,查找的最后一个对象是全局对象。查找的这条链就叫作用域链。

闭包

一个函数能够访问另一个函数内部的变量的时候,就形成闭包

为什么会内存泄漏?

因为当外层函数执行完毕后,外层函数执行环境的作用域链会销毁,但此时内部函数还在引用外层函数的活动对象,这个活动对象会留在内存中。 解决办法:执行外层函数的时候会分配一块堆内存,把堆内存中的值置为null就行了

内存泄漏的场景:
  • 意外的全局变量 使用了未声明的变量,变量就会变成全局变量,那这个变量就会一直保存在内存中无法被回收
  • 定时器:如果定时任务不被使用了,但是没有清除,那这个定时器所引用的外部变量就会一直保持在内存中无法被回收。解决办法:不使用定时器时及时清除定时任务
  • 闭包:外部函数执行时会分配一块堆内存,外部函数执行完之后如果没有对堆内存置空,那内部函数就会仍然引用外部函数的变量,导致变量无法被回收。解决办法:外部函数执行完之后将对应的堆内存置空
  • dom的引用:在引用一个dom元素之后,删除这个dom元素,此时dom元素已经不存在了,但是这个元素的引用还存在,所以会造成内存泄漏。解决办法:应该在移除之后将引用清空。 使用场景

1.回调函数

function add(num1,num2,callback){
let sum = num1+num2;
callback(sum)
}

add(1,2,function(sum){
    console.log(sum)
})

2.防抖节流

防抖:事件触发n秒后再执行回调,如果n秒内被再次触发,则重新及计时,比如浏览器上的模糊搜索

function debounce(fn, delay) {
    let timer = null;
    return function() {
        if(timer !== null) {
            clearTimeout(timer) //如果计时任务已经存在,并且又触发了相同的事件,则取消当前计时,重新计时
        }
        timer = setTimeout(
          () => fn.call(this), delay)
    }
}

节流:事件连续触发,但n秒内只执行一次回调,比如:scroll,resize事件

function throttle(fn, delay) {
   let flag = true;
   return function() {
       if(flag) {
           setTimeout(() => {
           fn.call(this);
           flag = true;
           }, delay)
       }
       flag = false 
   } 
}

apply的传参只能是一个数组;call的传参可以传多个;bind用来创建一个函数,也可以传参

  1. 柯里化函数
function uri(protocol) {
    return function (hostname, pathname) {
        return `${prootocol}${hostname}${pathname}`
    }
}
uri('http://')('xxx', 'yyy')

Promise

什么是promise?

promise是一种异步编程解决方案,它是一个对象,可以从中获取异步操作的消息。用.then的链式调用解决了多层异步操作时的回调地狱问题

promise有三种状态:pending,resolved,rejected,promise必然处于其中之一,状态一旦发生改变就不会再变,状态的改变是通过resolve()和reject()方法来实现的。

promise实例方法有catch,then,finally

Promise的静态方法有all,race,resolve,reject等,all比较常用,通常用来处理没有先后顺序的请求。

  • .all中只要有一个promise返回失败就会失败
  • .race含有竞速的意思,将多个Promise放在一个数组中,数组中第一个得到结果的promise,不管是" 完成
  • (resolved)"还是" 失败(resolved)" , 那么这个 .race 方法就会返回这个结果。
  • .resolve 返回一个resolved状态的Promise实例
  • .reject返回一个rejected状态的Promise实例

解决了什么问题?

  • 回调地狱:比如ajax请求有前后依赖关系时,那就需要嵌套ajax请求,多了的话就会嵌套很多层,也就是回调地狱promise的作用就是解决ajax的回调地狱问题,将嵌套请求变成链式调用
  • 可支持并发请求,获取并发请求的结果
async/await

async/await其实是一个语法糖,返回的是一个promise,他能实现的promise都能实现,相当于promise的一个优化。

  • async代码更清晰易于阅读,Promise的链式调用长了就会造成阅读负担。
  • async几乎是同步的写法,非常优雅,promise传递中间值非常麻烦
  • async调试更友好。promise中,由于没有代码块,你不能在一个返回表达式的箭头函数中设置断点。如果你在.then代码块中设置断点,调试器不能进入下一个.then。

js中有哪些数据类型?有什么区别

基本数据类型: undefined、null、string、boolean、number、symbol

symbol代表独一无二的数据类型,主要是为了解决可能出现的全局变量冲突的问题

原始数据类型存储在栈中,大小固定,占用空间小

复杂数据类型: 对象、数组、函数

复杂数据类型存储在堆中,引用类型的变量其实是一个指针,指向的是堆内存中的地址。

typeof和instanceof的区别

typeof判断数组|对象|null结果都是object.判断其余基本数据类型和function都能正确判断

instanceof能够正确判断引用数据类型。instanceof的运行机制是在数据的原型链中能否找到该类型的原型。

Object处理了一些特殊情况,比如

== 和 === Object.is的区别

===:严格相等,比较类型和值 ==:抽象相等,先对左右两边的值进行类型转换再比较值 Object.is 一般和===的判断结果相同,它处理了一些特殊情况,比如:

Object.is(-0,+0) //false
Object.is(NaN, NaN) //true
undefined == null //true
undefined === null //false

浅拷贝和深拷贝

引用类型的变量其实是一个指针,这个指针指向的是堆内存中的地址。

浅拷贝新旧对象还是共享同一块内存,但深拷贝是为新对象开辟了一个新的内存空间。

image.png

  1. 直接赋值 引用类型直接复制其实是不同的指针指向了相同的引用对象,所以会相互影响。
  2. 浅拷贝 浅拷贝是创建一个新的对象,浅拷贝只是复制了第一层对象的属性,因此如果对象内的属性是引用类型,修改是会相互影响的。但第一层数据类型为基本类型时,浅拷贝后不会相互影响。

浅拷贝的方式:

数组:Array.concat()  Array.slice()
对象:Object.assign({}, obj)
复制代码
  1. 深拷贝 递归拷贝对象中的每一个属性,所以深拷贝后两个对象之间互不影响。

深拷贝的方式:

JSON.parse(JSON.stringify(obj)) //缺点:对象中包括日期或者正则表达式时,会跟源数据不一致
function deepClone(source) {
    if(source ===null) return source;
    if(source instanceof Date) return new Date();
    if(source instanceof RegExp) return RegExp;
    if(typeof source !== 'object') return source
    let obj = Array.isArray(source) ? [] : {};
    for(let i in source) {
        if(source.hasOwnProperty(i)) {
            obj[i] = deepClone(source[i])
        }
    }
    return obj;
}
lodash中的cloneDeep

原型和原型链

什么是原型?

原型分为隐式原型和显示原型。每个引用类型的数据都有一个隐式原型__proto__,这个值指向它的构造函数的显式原型prototype, 这两个值完全相等。

什么是原型链?

image.png 当查找对象中的属性或者方法时,首先会从自身查找,如果没有查找到,就会去它的原型上去找,如果再找不到,就会去原型的原型上去找,直到null为止。查找的这条链就叫原型链。

原型链是一个过程,原型则是这个过程中的单位,贯穿整个原型链。

js运行机制

js是单线程的,也就是所有的任务都需要排队,如果前面的任务没有执行完,那么后面的任务就需要一直等,这样用户体验很差,所以就有了异步的概念。

同步任务:在主线程排队的任务

异步任务:不进入主线程,会被放进任务队列。分为宏任务和微任务

```
宏任务:script,setTimeout,setInterval
复制代码
```
```
微任务:promise.then,nextTick,async await   (需要耗时的一些任务)
复制代码
```

同步任务依次执行完之后,会从任务队列中拿到宏任务来执行,宏任务执行完后会执行该任务中的微任务,以此循环。即事件循环。 计时器为什么不准?计时器不是到点就执行,而是到点放到任务队列中,轮到它了再去执行

事件冒泡和捕获

事件冒泡:事件在最内层元素发生,并且向上传播。

事件捕获:事件在最外层元素发生,直到最具体的元素。

element.addEventListener(event, fn, useCapture)   //useCapature为true时表示在捕获阶段调用处理函数,为false表示在冒泡阶段调用处理函数。

场景: 在点击li标签时,需要执行某些操作,常规做法是去遍历li标签绑定事件,那当li标签特别多的时候,很消耗性能。如果利用事件冒泡,那此时只需要在ul元素上去监听点击事件,事件在li元素上发生,会传播到ul元素,此时会触发ul的点击事件,再利用target去判断事件的发生地,就可以去执行操作了

继承

  1. 原型链继承
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(){}
B.prototype = new A()
复制代码

缺点:原型链上的修改都会变成通用的

  1. 构造函数继承
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
const b = new B('aa')
复制代码

缺点:不能继承原型链上的方法

  1. 组合继承(原型+构造函数)
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
B.prototype = new A('hh')
复制代码

缺点:初始化了两次

  1. 寄生组合继承
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
B.prototype = Object.create(A.prototype)

get和post请求的区别

类别get请求post请求备注
用途用来获取资源用来创建或者获取资源
参数位置get的参数会携带在url上Post的参数在请求体中
安全性get请求信息在url上,不安全post比较安全
参数大小url中参数最大为2048个字符Post的参数大小没有限制
数据包get请求只会产生一个tcp数据包post请求会产生两个tcp数据包1、对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);2、而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
浏览器回退get请求在浏览器回退时不会重新发起请求post请求会

Proxy

proxy是ES6中新增的功能,可以自定义对象中的操作。Vue3中使用Proxy代替Object.defineProperty来实现数据响应式。

箭头函数和普通函数的区别

  1. 箭头函数写法更简洁
  2. 箭头函数没有自己的this,通过上下文确定自己的this
  3. 箭头函数没有自己的原型
  4. 箭头函数不能作为构造函数使用
  5. 函数体内没有arguments对象

es6新特性

  1. let, const
  2. 数组
    • 扩展运算符
    • Array.from(),Array.of()
    • find(),findIndex()
    • fill(value, startIndex, endIndex)
    • includes()
    • flat()
    • entries(),keys(),values()
  3. 对象
    • Object.is(),Object.assign()
    • Object.keys(),values(),entries()
  4. promise
  5. 模块化(import,export)

数组方法

  1. map
  2. forEach
  3. push
  4. pop
  5. shift
  6. unshift
  7. filter
  8. reduce
  9. concat
  10. slice
  11. splice
  12. reverse
  13. sort
  14. some, every

字符串方法

trim,concat,slice,substring,includes,chartAt,split,match,toUpperCase,toLowerCase

reduce方法

reduce(function, 初始值)