JS 高级知识点学习

216 阅读16分钟

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

  • this 指向不同:
    • 箭头函数:在哪里定义,this 就指向谁,this 不可以被改变。
    • 普通函数:在哪里调用,this 就指向谁,this 可以通过 call、apply、bind 方法去改变this 指向。
  • 参数不同:
    • 普通函数有 arguments 动态参数。
    • 箭头函数没有 arguments 动态参数,但是有剩余参数 ...args。

this 指向的几种方式?

  • 在全局中、函数体中、定时器中,this 指向的是window。
  • 在构造函数和原型对象中,this指向的是对象实例。
  • 在对象中,this指向的是调用者。
  • 在事件中,this指向的是当前绑定的元素。

call、apply、bind 的作用和区别?

相同点:都可以改变函数内部的 this 指向。

不同点:

  • call 和 apply 会调用函数,并且改变函数内部 this 指向。
  • call 和 apply 传递的参数不一样,call传递参数是以参数列表的形式进行传参,apply 必须以数组形式传参。
  • bind 不会调用函数,可以改变函数内部 this 指向。

应用场景

  • call :调用函数并且可以传递参数。
  • apply :经常跟数组有关系,可以利用 apply 借助于数学内置对象求最大值、最小值。
  • const arr = [1,88,97,45,67]; const max = Math.max.apply(Math,arr);
  • bind :不调用函数,但是还想改变 this 指向。比如:改变定时器内部的 this 指向。

作用域

作用域(scope)指一个变量的作用范围,规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问。

全局作用域:在最外层声明的变量,就是全局作用域,在此声明的变量任何其他作用域都可以被访问。
  • 全局作用域在页面创建时被打开,页面关闭时被销毁。
  • 浏览器中有一个全局对象 window,代表一个浏览器窗口,由浏览器创建,可以直接使用。
  • 全局作用域中声明的变量都会作为 window 对象的属性和方法进行保存。
函数作用域:在函数内部声明的变量,只能在函数内部被访问,外部无法访问。
  • 函数被调用时,创建函数作用域,函数执行完毕后,函数作用域被销毁。
  • 每调用一次函数,都会创建一个函数作用域,它们之间相互独立。
  • 在函数作用域中,可以访问到全局作用域的变量。在函数外,无法访问到函数作用域内的变量。
块级作用域:在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量,外部无法访问。
  • let 声明的变量,const 声明的常量会产生块级作用域,var 不会产生块作用域;
  • 不同代码块之间的变量无法互相访问;
  • 推荐使用 let 或 const ;

作用域链

作用域链本质上是底层的变量向上查找机制

  • 在函数作用域中访问变量和函数时,会先在自身的作用域进行查找。
  • 如果没有找到,则会向上一级作用域中寻找,直至全局作用域。

变量提升

  • 允许在变量声明之前即被访问(仅存在于 var 声明变量)。
  • 把所有 var 声明的变量提升到当前作用域的最前面。
  • 只提升声明,不提升赋值。

函数提升

  • 是指函数在声明之前即可被调用
  • 会把所有函数声明提升到当前作用域的最前面。
  • 只提升函数声明,不提升函数调用。
  • 函数表达式必须先声明和赋值,后调用,否则报错。

什么是事件冒泡和事件捕获?

  • 事件冒泡是指事件从最内层的元素开始向外传播,直到传播到最外层的元素。
  • 事件捕获是指事件从最外层的元素开始向内传播,直到传播到最内层的元素。

闭包

  • 概念:内部函数可以访问到外层函数的变量(内层函数 + 外层函数变量)。
  • 作用:避免全局变量被污染,实现了数据的私有;延伸了变量的作用范围;可以在函数外部读取函数内部的变量。
  • 特性:内部函数未执行完成,外部函数变量不会被销毁。
  • 引起的问题:闭包长期占用内存,内存消耗很大,可能导致内存泄露。
  • 内存泄漏:程序中分配的内存由于某种原因,程序未释放或无法释放叫做内存泄漏。
  • 在退出函数之前,将不使用的局部变量全部删除,可以将变量赋值为null。避免变量的循环赋值和引用。
function outer() {
    let a = 10;
    function fn() {
        console.log(a)
        // return a
    }
    return fn
}
const fun = outer()
fun();    // 外层函数使用内部函数的变量

// 使用闭包统计函数调用次数
function count() {
    let i = 0;
    return function fn() {
        i++
        console.log(`函数被调用了${i}次`)
    }
}

const fun = count()

为什么闭包中的变量不会被垃圾回收?

  • 如果闭包函数的引用计数为 0 时,函数就会释放,它引用的变量也会释放。
  • 只有当闭包函数的引用计数不为 0 时,说明闭包函数随时有可能被调用,他被调用后,就会引用他在定义时所处的环境的变量。
  • 闭包中的变量就得需要一直在内存中,则就不会被垃圾回收掉。

垃圾回收机制?

  • JS 中内存的分配和回收都是自动完成的,内存在不使用的时候,会被垃圾回收器自动回收。
  • JS 中的垃圾就是指不会再被使用的值,就会被当成垃圾回收掉。
  • 说明:全局变量一般不会回收,因为全局变量随时有可能被使用。一般情况下局部变量的值,不用了,会被自动回收掉。

内存的生命周期

  • 内存分配:当我们声明变量、函数、对象时,系统会自动为它们分配内存。
  • 内存使用:即读写内存,也就是使用变量、函数等。
  • 内存回收:使用完毕后,由垃圾回收器自动回收不再使用的内存。

算法说明:

堆栈空间分配区别:

  • 栈:由操作系统自动分配函数,释放函数的参数值、局部变量等,基本数据类型放到栈里面。
  • 堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。

常见的浏览器垃圾回收算法:引用计数法、标记清除法。

引用计数法:定义 “内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象。

  • 跟踪记录被引用的次数。
  • 如果被引用了一次,那么就记录次数 1,多次引用会累加。
  • 如果减少一个引用就减 1。
  • 如果引用次数是 0,则释放内存。

存在问题:循环引用。

  • 如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

标记清除法:从根部扫描对象,能查找到的就是使用的,查找不到的就要回收。

  • 标记清除算法将 “不再使用的对象” 定义为 “无法达到的对象”。
  • 就是从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
  • 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

编程思想

面向过程

  • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步的实现,使用的时候再一个一个的依次调用。

面向对象

  • 面向对象是把事务分解成一个个对象,然后由对象之间分工合作。
  • 面向对象编程具有灵活、代码可复用、容易维护和开发的优点。
  • 面向对象的特性:封装性、继承性、多态性

面向过程和面向对象的对比

面向过程

  • 优点:性能比面向对象高,适合跟硬件联系很紧密的东西。
  • 缺点:没有面向对象易维护、易复用、易扩展。

面向对象

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
  • 缺点:性能比面向过程低。

原型链

原型

  • 每个函数都有一个 protoType 属性,称为原型,因为这个属性的值是个对象,也称为原型对象。
    • 共享方法:原型可以放一些属性和方法,共享给实例对象使用。
    • 公共属性写在构造函数里面,公共的方法写在原型对象上。
    • 原型可以做继承。

原型链:原型链是一个向上查找规则

  • 每个对象都有__proto__属性,这个属性指向它的原型对象,原型对象也是对象,也有__proto__属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回 null。

公共方法为什么不能放到构造函数里面?

  • 在创建实例时,都会为这个方法单独开辟一个内存空间来存放同一个函数,比较浪费内存。
  • 解决方法:将这些方法定义到原型对象上面,这样就可以实现了这些方法的共享。
__proto__对象原型:为对象成员查找机制提供一个方向。

image.png

原型对象、对象原型、构造函数 三者之间的关系

  • prototype 原型对象:构造函数自动有原型。
  • constructor 构造函数:该属性指向构造函数本身。
  • __proto__对象原型:指向构造函数的 prototype 原型对象。 原型对象-对象原型-构造函数三者之间的关系.png

instanceof 运算符

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

typeof 和 instanceof 的区别

  • 相同点:都是用来检测判断类型的。
  • 不同点:
    • typeof:可以判断基础数据类型(null 除外),但是无法判断引用数据类型(function 除外)
    • instanceof:可以准确的判断引用数据类型,但不能正确的判断基础数据类型。
    • typeof:返回一个变量的基本类型,instanceof 返回的是一个布尔值。
  • 如果需要检测数据类型,建议使用 Object.prototype.toString.call() 方法。
Object.prototype.toString.call({})  // [object Object]
Object.prototype.toString.call(1)  // [object Number]
Object.prototype.toString.call('1')  // [object String]
Object.prototype.toString.call(true)  // [object Boolean]
Object.prototype.toString.call(function () {})  // [object Function]
Object.prototype.toString.call(null)  // [object Null]
Object.prototype.toString.call(undefined)  // [object Undefined]
Object.prototype.toString.call(/123/g)  // [object RegExp]
Object.prototype.toString.call(new Date())  // [object Date]
Object.prototype.toString.call([])  // [object Array]
Object.prototype.toString.call(document)  // [object HTMLDocument]
Object.prototype.toString.call(window)  // [object Window]

深浅拷贝:只针对引用类型

  • 浅拷贝是指将对象或数组的一级属性或元素复制到新的对象或数组中,而不会复制它们的子属性或元素

浅拷贝:拷贝的是地址

  • 拷贝对象:Object.assgin() / {...obj} 展开运算符
  • 拷贝数组:Array.prototype.concat() 或者 [...arr]

直接赋值和浅拷贝有什么区别?

  • 直接赋值:只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址。
  • 浅拷贝:如果是一层对象,不相互影响,如果出现多层对象拷贝,还是会互相影响。

深拷贝:拷贝的是对象

  • 深拷贝是指将对象或数组的所有属性或元素都复制到新的对象或数组中,包括它们的子属性或元素。

常用方法

  • 通过递归实现深拷贝。
  • lodash/cloneDeep。
  • 通过 JSON.parse(JSON.stringify()) 实现。

总结

  • 深拷贝是要做到拷贝出来的对象,更改数据时不会影响到原始数据的值,实现深拷贝,需要使用递归函数。
  • 在进行递归时,如果是简单数据类型,直接赋值,如果是数组、对象等引用数据类型的,需要再次利用递归函数解决,需要先处理数组,在处理对象。
const obj = {
    uname: '小明',
    age: 18,
    hobby: ['乒乓球', '足球'],
    family: {
        baby: '小孩'
    }
}
const o = {}
/* 实现深拷贝 */
function deepCopy(newObj, oldObj) {
    Object.keys(oldObj).map(k => {
        // 数组处理
        if (oldObj[k] instanceof Array) {
            newObj[k] = []
            deepCopy(newObj[k], oldObj[k])
        } else if (oldObj[k] instanceof Object) {
            // 对象处理
            newObj[k] = {}
            deepCopy(newObj[k], oldObj[k])
        } else {
            newObj[k] = oldObj[k]
        }
    })
}

deepCopy(o, obj)
o.age = 20
o.hobby[0] = '篮球'
o.family.baby = '大人'
console.log(o, obj)

函数递归:如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。

  • 简单理解:函数内部自己调用自己,这个函数就是递归函数。
  • 递归的效果和循环类似。
  • 因为递归容易出现“栈溢出”,所以必须要加退出条件 return。
/* 使用 递归函数 模拟使用 setTimeout 实现 setInterval  */
function getTime() {
    document.querySelector("h1").innerHTML = new Date().toLocaleString()
    setTimeout(getTime, 1000)
}

getTime()

防抖与节流

防抖:单位时间内,频繁触发事件,只执行最后一次。

核心思路:利用定时器(setTimeout)实现。

  • 定义一个定时器变量。
  • 每次事件触发时,先判断是否有定时器,如果有定时器,则先清除以前的定时器。
  • 如果没有定时器,则开启定时器,存入到定时器变量里面。
  • 在定时器里面,调用要执行函数。

应用场景

  • 搜索框输入:只需用户输入最后一次完成,再发送请求。
  • 手机号、邮箱验证输入校验
function debounce(fn, time) {
    let timer; // 声明定时器变量
    // return 返回一个匿名函数
    return function () {
        // 每次执行事件,都需要先判断是否有定时器,如果有定时器,清除定时器
        if (timer) clearTimeout(timer)
        // 如果没有定时器,则开启定时器,存入定时器变量里面
        timer = setTimeout(() => {
            console.log(this)
            // 调用函数
            fn()
        }, time)
    }
}

节流:单位时间内,频繁触发事件,只执行一次。

核心思路:利用定时器(setTimeout)实现。

  • 定义一个定时变量。
  • 每次事件触发时,判断是否开启定时器,如果有定时器,则不开启定时器。
  • 如果没有定时器,则开启定时器,存入到定时器变量里面。
    • 定时器里面调用要执行的函数。
    • 定时器里面把定时器清空(timer = null)。

应用场景

  • 高频事件:鼠标移动 mousemove、页面尺寸缩放 resize,滚动条滚动 scroll 等等。
function throttle(fn, time) {
    let timer = null // 定义定时器变量
    return function () {
        // 判断定时器是否开启,未开启的话,开启定时器
        if (!timer) {
            // 存入到定时器变量里面
            timer = setTimeout(() => {
                fn() // 调用函数
                timer = null // 清空定时器
            }, time)
        }
    }
}

为什么用 timer 等于 null 清空定时器,而不用 clearTimeout 清空定时器?

  • 在 setTimeout 中是无法清除定时器的,因为定时器还在运作,所以使用 timer = null,而不是 clearTimeout(timer)。

简述 Promise

  • Promise 是异步操作的一种方案。每一个异步任务返回一个 Promise 对象,该对象有一个 then 方法,允许指定回调函数。可以解决回调地狱的问题。
  • promise 的本身是同步执行,then() 和 catch() 方法是异步执行。
  • Promise有三种状态:pending(等待中)、fulfilled(已成功)和 rejected(已失败)。当一个 Promise 被创建时,它会处于 pending 状态,当异步操作完成时,它会变成 fulfilled 状态或 rejected 状态。在 Promise 对象的 then() 方法中可以分别处理这两种状态的结果。
  • Promise对象有两个重要的方法:resolve()reject()。当异步操作成功时,我们可以调 resolve() 方法将结果传递给 then() 方法;当异步操作失败时,我们可以调用 reject() 方法将错误信息传递给 catch() 方法。

async / await 的原理?

  • Async/await是ES2017引入的一种异步编程模型:将异步操作转换为同步操作,使得代码看起来像同步代码一样,但实际上是异步执行的。
  • async 函数会返回一个 Promise 对象,而 await 关键字可以放在 async 函数内部的任意异步操作之前,它会暂停 async 函数的执行等待异步操作完成并返回结果,然后将结果作为 await 表达式的值,继续执行 async 函数

Promise 和 await async 有什么区别吗

  • 简洁的代码:async / await 更加优雅简洁,代码看起来像同步代码一样,不需要像 Promise 通过 then 来获取返回结果。
  • 错误处理:Promise 错误可以通过 .catch 来捕捉;async / await 既可以用 .catch 来捕捉,也可以使用 try-catch 捕捉。
  • Promise 是 es6 新特性,async / await 是 es7 的新特性。

JS 的事件循环(event loop)

  • js 语言的特点:单线程,同一时间内,只能做一件事情
  • 同步和异步的区别:执行顺序不同。
  • JS 是单线程,防止代码阻塞,我们把代码分为:同步和异步。
  • 同步代码交给 JS 引擎执行,异步代码交给宿主环境。
  • 同步代码放入执行栈中,异步代码等待时机成熟送入任务队列排队。
  • 执行栈执行完毕,会去任务队列看是否有异步任务,有就送到执行栈执行,反复循环查看执行,这个过程是事件循环(event loop)。

同步任务:

  • 立即放入 JS 引擎(JS 主线程)执行,并原地等待结果。

异步任务:

  • JS 的异步是通过回调函数实现的。
  • 先放入宿主环境(浏览器 / node),不必原地等待结果,并不阻塞主线程继续往下执行,异步结果在将来执行
  • 异步任务的三种类型:
    • 普通事件,如:click,resize 等;
    • 资源加载,如:load,error 等;
    • 定时器,包括 setInterval、setTimeout 等;
  • 异步任务相关添加到任务队列中(任务队列也称为消息队列)。

执行机制:

  • 先执行执行栈中的同步任务
  • 异步任务放到任务队列中。
  • 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行。

宏任务、微任务

  • JS 把异步任务分为宏任务和微任务
  • 宏任务:由宿主环境发起的。
  • 微任务:由 JS 引擎发起的任务。例如:Promise 本身是同步的,then / catch 的回调函数是异步的。

执行过程

  • 同步代码(JS 执行栈 / 回调栈)。
  • 微任务的异步代码(JS 引擎)。
    • Promise.then() catch()。
    • Async / Await。
    • Object.observe 等等。
  • 宏任务的异步代码(宿主环境)。
    • script(代码块)
    • setTimeout / setInterval 定时器。

区别

  • 微任务:在 DOM 渲染之触发,如:Promise。
  • 宏任务:在 DOM 渲染之触发,如:setTimeOut。