JS高级(作用域,原型链,闭包,节流,防抖等概念性)

发布时间 2023-05-08 18:03:06作者: 三三要进步

作用域


局部作用域

函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问

块作用域

let和const声明的变量会产生块作用域,var不会产生块作用域,推荐使用let和const

全局作用域

在<script>和.js文件的最外层就是全局作用域,在此声明的变量在其他任何作用域都可以被访问

作用域链

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

  • 在函数被执行时,会优先查找当前函数作用域中查找变量
  • 如果当前作用域查找不到则会一次逐级查找父级作用域直到全局作用域
  • 子作用域可以访问父作用域,父作用域无法访问子作用域

垃圾回收机制


JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

内存的生命周期

JS环境中分配的内存,一般有如下生命周期

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
  • 全局变量一般不会回收(关闭页面回收)
  • 一般情况下局部变量的值不用了会被自动回收掉

内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏

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

垃圾回收算法

引用计数法

看一个对象是否有指向它的引用,没有引用了就回收对象 基于“不再使用的对象”

主要算法是跟踪记录引用次数,被引用就记录次数1,多次引用累加,减少一个引用就减一,引用次数为0就释放内存

  • 存在问题

    嵌套引用(循环引用)即两个对象相互引用,尽管他们不再使用,垃圾回收器不会进行回收,导致内存泄漏

    	function fn(){
    		let o1 = {}
    		let o2 = {}
    		o1.a = o2
    		o2.a = o1
    		return '引用次数无法回收'
    	}
    //他们引用次数永远不会是0,所以会导致内存泄漏
    
标记清除法

现代浏览器大多是基于标记清除算法的某些改进算法,基于“无法达到的对象”

从根部(在JS中就是全局变量)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的,无法有根部出发触及到的对象被标记为不再使用,稍后进行回收

闭包


一个函数对周围状态的引用捆绑在一起,内层函数中可以访问到其外层函数的作用域

闭包 = 内层函数 + 外层函数的变量

	function outer() {
            const a = 10
            function fn() {
                console.log(a)
            }
            fn()
        }
        outer()

闭包作用:封装数据,提供操作,外部也可以访问函数内部的变量

闭包基本格式:

	function outer() {
            const a = 10
            function fn() {
                console.log(a)
            }
            return fn
        }
        let fun = outer()
        fun()

闭包实现数据私有

闭包可能会导致内存泄漏

变量提升

把所有var声明的变量提升到当前作用域最前面,只提升变量声明,不提升变量赋值

函数进阶


函数提升

会把所有函数声明提升到当前作用域最前面

函数表达式必须先声明和赋值,后调用

函数参数

动态参数

arguments是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。它可以动态获取函数的实参

		function getSum() {
            let sum = 0
            for (let i = 0; i < arguments.length; i++) {
                sum += arguments[i]
            }
            console.log(sum)
        }
        getSum(1, 2, 3)//6
        getSum(1, 2, 9, 10)//22
剩余参数

...是语法符号,置于最末函数形参之前,用于获取多余的实参,是个真数组

		function getSum(a, b, ...arr) {
            let sum = a + b
            for (let i = 0; i < arr.length; i++) {
                sum += arr[i]
            }
            console.log(sum)
        }
        getSum(1)//NaN
        getSum(1, 2)//3
        getSum(1, 5, 7, 8)//21
展开运算符
		const arr1 = [1, 2, 3]
        //展开运算符用于展开数组
        console.log(...arr1)
        //...arr === 1,2,3
        console.log(Math.max(arr1))//NaN
        console.log(Math.max(...arr1))//3
        console.log(Math.min(...arr1))//1
        //合并数组
        const arr2 = [4, 5, 6]
        const arr = [...arr1, ...arr2]
        console.log(arr)//[1,2,3,4,5,6]

展开运算符和剩余参数的区别是:展开运算符主要是数组展开,剩余参数在函数内部使用

箭头函数

目的:更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁

箭头函数更适用于哪些本来需要匿名函数的地方

		// const fn = function(){
        //     console.log(11)
        // }
        //箭头函数基本语法
        // const fn = () => {
        //     console.log(11)
        // }
        // fn()
        // const fn = (x) => {
        //     console.log(x)
        // }
        // fn(1)
        //只有一个形参可以省略小括号
        // const fn = x => {
        //     console.log(x)
        // }
        // fn(1)
        //只有一行代码可以省略大括号
        // const fn = x => console.log(1)
        // fn(1)
        //只有一行代码可以省略return
        // const fn = x => x + x
        // console.log(fn(1))
        //箭头函数可以直接返回一个对象
        const fn = uname => ({ uname: uname })
        fn('三三')
箭头函数参数

普通函数有arguments参数,箭头函数没有arguments动态参数,但是有剩余参数

箭头函数的this

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this

普通函数的this是如何被调用来定义函数的this值

		// console.log(this)//window
        // //函数里面的this
        // function fn() {
        //     console.log(this)//window
        // }
        // fn()
        // //对象里面的this
        // let obj = {
        //     uname: 'andy',
        //     sayHi() {
        //         console.log(this)//obj
        //     }
        // }
        // obj.sayHi()
        //箭头函数的this
        // const fn = () => {
        //     console.log(this)//window
        // }
        // fn()
        //对象箭头函数this
        // const obj = {
        //     uname: '三三',
        //     sayHi: () => {
        //         console.log(this)//window
        //     }
        // }
        // obj.sayHi()
        const obj = {
            uname: '三三',
            sayHi: function () {
                const count = () => {
                    console.log(this)//obj
                }
                count()
            }
        }
        obj.sayHi()

DOM事件回调函数为了方便,不太推荐使用箭头函数

解构赋值


数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法

		const arr = [100, 60, 80]
        const [max, min, avg] = arr
        console.log(max)//100

遇到的问题:

变量少,单元值多,多的单元值没有赋变量 可以用剩余参数解决

		const arr = [1, 2, 3, 4, 5]
        const [a, b, ...c] = arr
        console.log(a)//1
        console.log(b)//2
        console.log(c)//[3,4,5]

变量多,单元值少,多的变量打印undefined 可以设置默认值解决

		const arr = [1, 2]
        const [a = 0, b = 0, c = 0] = arr
        console.log(a)//1
        console.log(b)//2
        console.log(c)//0

按需导入赋值

		const arr = [1, 2, 3, 4]
        const [a, b, , d] = arr
        console.log(a)//1
        console.log(b)//2
        console.log(d)//4

支持多维数组解构

		//const arr = [1, 2, [3, 4]]
        //const [a, b, [c, d]] = arr
        //console.log(c)//3
        //console.log(d)//4
		const arr = [1, 2, [3, 4]]
        const [a, b, c] = arr
        console.log(c)//[3,4]

对象解构

		const obj = {
            uname: '三三',
            age: 18
        }
        const { uname, age } = {
            uname: '三三',
            age: 18
        }
        console.log(uname)

对象属性的值将被赋值给与属性名相同的变量

对象中找不到与变量名一致的属性时变量值为undefined

如果外面有相同名字,可以这样赋值

		const uname = '一一'
        const obj = {
            uname: '三三',
            age: 18
        }
        const { uname: username, age } = {
            uname: '三三',
            age: 18
        }
        console.log(username)
数组对象解构
		const data = [{
            uname: '三三',
            age: 18
        }]
        const [{ uname, age }] = data
        console.log(uname)
多级对象解构
		const pig = {
            name: '佩奇',
            family: {
                mother: '猪妈妈',
                father: '猪爸爸',
                brother: '乔治'
            },
            age: 6
        }
        const { name, family: { mother, father, brother }, age } = pig
        console.log(name)
        console.log(mother)
        console.log(father)
        console.log(age)
多级数组对象解构
		const pig = [{
            name: '佩奇',
            family: {
                mother: '猪妈妈',
                father: '猪爸爸',
                brother: '乔治'
            },
            age: 6
        }]
        const [{ name, family: { mother, father, brother }, age }] = pig
        console.log(name)
        console.log(mother)
        console.log(father)
        console.log(age)

构造函数


可以通过构造函数快速创造多个实例化对象

  • 构造函数命名以大写字母开头
  • 只能由“new”操作符来执行
  • 构造函数内部不要写return
	function Pig(name, age) {
            this.name = name
            this.age = age
        }
        const p = new Pig('佩奇', 6)
        const q = new Pig('乔治', 9)
        console.log(p, q)
    </script>

实例成员

实例对象的属性和方法即为实例成员

实例对象相互独立,实例成员当前实例对象使用

静态成员

构造函数的属性和方法被称为静态成员

静态成员只能构造函数访问

内置构造函数

JS中几乎所有的数据都可以基于构造函数创建,字符串、数值、布尔等基本类型都有专门的构造函数,我们称为包装类型

Object

常用静态方法(只有构造函数Object可以调用):

Object.keys获取对象中所有属性

Object.assign静态方法常用于对象拷贝

		const obj = { uname: '张安', age: 19 }
        console.log(Object.keys(obj))// ['uname', 'age']
        console.log(Object.values(obj))//['张安', 19]
        const obj1 = {}
        Object.assign(obj1, obj)
        console.log(obj1)//{uname: '张安', age: 19}
        Object.assign(obj, { gender: '女' })
        console.log(obj)//{uname: '张安', age: 19, gender: '女'}
        //相同属性会覆盖?
        Object.assign(obj, obj1)
        console.log(obj)//{uname: '张安', age: 19, gender: '女'}
数组
方法 作用 说明
forEach 遍历数组 不返回数组,经常用于查找遍历数组元素
filter 过滤数组 返回新数组,返回的是筛选满足条件的数组元素
map 迭代数组 返回新数组,返回的是处理之后的数组元素,想要使用返回的新数组
reduce 累计器 返回累计处理的结果,经常用于求和等

reduce方法

		const arr = [1, 3, 5]
        // arr.reduce(function (上一次值,当前值){},初始值)
        //没有初始值
        const total = arr.reduce(function (prev, current) {
            return prev + current
        })
        console.log(total)//9
        //有初始值
        const result = arr.reduce(function (prev, current) {
            return prev + current
        }, 10)
        console.log(result)//19

from()方法可以将伪数组转换为真数组,数组方法很多,可以查阅文档学习

String

split()方法把字符串转换为数组,里面的分隔符是字符串里面的符号

substring(开始索引[,结束索引])截取字符串

str.startWith(s[,position])是否以s开头

str.includes(搜索的字符串[,position]),判断一个字符串是否包含在另一个字符串里。严格区分大小写

Number

num.toFixed(n)决定保留n位小数

原型


原型对象

JS面向对象可以通过构造函数实现封装,但是会存在浪费内存问题

可以通过原型对象解决这个问题。

公共属性写到构造函数,公共方法写到原型对象

构造函数和原型对象都指向实例化对象

数组扩展

		const arr = [1, 2, 3]
        //用原型对象来写一个方法,所有数组都可以用
        Array.prototype.max = function () {
            return Math.max(...this)
        }
        console.log(arr.max())//3
        Array.prototype.sum = function () {
            return this.reduce((prev, current) => prev + current)
        }
        console.log(arr.sum())//6

constructor属性

每个原型对象里面都有constructor属性,该属性指向该原型对象的构造函数

		function Star() {

        }
        console.log(Star.prototype)
        //这样相当于赋值,所以要在里面添加constructor,指回创造原型对象的构造函数
        Star.prototype = {
            constructor: Star,
            sing: function () {
                console.log('唱歌')
            },
            dance: function () {
                console.log('跳舞')
            }
        }
        console.log(Star.prototype)

对象原型

对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有这个原型的存在

__proto__是Js非标准属性,[[prototype]]和__proto__意义相同,用来表明当前实例对象指向哪个原型对象prototype。

__proto__对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数

原型继承

JS大多通过原型对象实现来继承

		function Person() {
            this.eyes = 2,
                this.head = 1
        }
        function Woman() {

        }
        Woman.prototype = new Person()
        Woman.prototype.sing = function () {
            console.log('唱歌')
        }
        Woman.prototype.constructor = Woman
        const red = new Woman()
        console.log(red)
        function Man() {

        }
        Man.prototype = new Person()
        Man.prototype.constructor = Man
        const pink = new Man()
        console.log(pink)

原型链

原型链是一种查找规则,当访问一个对象的属性(方法)时,首先查找对象自身有没有该属性,没有就查找它的原型(即__proto__指向的prototype原型对象),如果还没有就查找原型对象的原型(Object的原型对象),以此类推一直找到Object为止(null),__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。可以通过instanceof来检测构造函数的prototype属性是否出现在某个实例对象的原型链上

深拷贝和浅拷贝


浅拷贝

浅拷贝简单数据类型拷贝的是值,引用数据类型拷贝的是地址

		const obj = {
            uname: '三三',
            age: 20,
            family: {
                baby: '伞伞'
            }
        }
        // const o = { ...obj }
        // o.age = 19
        // o.family.baby = 'sam'
        // console.log(o)//{uname: '三三', age: 20,family: {baby: 'sam'}}
        // console.log(obj)//{uname: '三三', age: 20,family: {baby: 'sam'}}
        const o = {}
        Object.assign(o, obj)
        o.age = 19
        o.family.baby = 'sam'
        console.log(o)//{uname: '三三', age: 19,family: {baby: 'sam'}}
        console.log(obj)//{uname: '三三', age: 20,family: {baby: 'sam'}}

深拷贝

  • 通过递归实现深拷贝

函数递归:一个函数在内部调用其本身,这个函数就是递归函数

递归作用和循环效果类似。递归很容易发生“栈溢出”错误,所以必须要加退出条件return

一个简单的递归函数:

		let i = 0
        function fn() {
            console.log(`这是第${i}次`)
            if (i > 6) return
            i++
            fn()
        }
        fn()

一个简单的深拷贝:

		const obj = {
            uname: '三三',
            age: 18,
            dance: {
                name: '跳舞'
            }
        }
        function deepCopy(newObj, oldObj) {
            for (let k in oldObj) {
                //先写数组再写对象,因为数组属于对象
                if (oldObj[k] instanceof Array) {
                    newObj[k] = []
                    deepCopy(newObj[k], oldObj[k])
                }
                if (oldObj[k] instanceof Object) {
                    newObj[k] = {}
                    deepCopy(newObj[k], oldObj[k])
                } else {
                    newObj[k] = oldObj[k]
                }
            }
        }
        let o = {}
        deepCopy(o, obj)
        o.age = 20
        o.dance.name = '唱跳'
        console.log(o)//{uname: '三三', age: 20, dance: {name: '唱跳'}}
        console.log(obj)//{uname: '三三', age: 18, dance: {name: '跳舞'}}
  • JS库lodash里面cloneDeep内部实现了深拷贝

    需要引入JS的lodash库

	<script src="lodash.min.js"></script>
        const obj = {
            uname: '三三',
            age: 18,
            hobby: {
                name: '跳舞'
            }
        }
        const o = _.cloneDeep(obj)
        o.age = 20
        o.hobby.name = '唱歌'
        console.log(o)//{uname: '三三', age: 20, hobby: {name: '唱跳'}}
        console.log(obj)//{uname: '三三', age: 18, hobby: {name: '跳舞'}}
  • JSON字符串实现深拷贝
		const obj = {
            uname: '三三',
            age: 18,
            hobby: {
                name: '跳舞'
            }
        }
        const o = JSON.parse(JSON.stringify(obj))
        o.hobby.name = '唱歌'
        o.age = 20
        console.log(o)//{uname: '三三', age: 20, hobby: {name: '唱跳'}}
        console.log(obj)//{uname: '三三', age: 18, hobby: {name: '跳舞'}}

异常处理

throw抛出异常

会立刻中断程序

		function fn(x, y) {
            if (!x || !y) {
                throw new Error('参数不能为空!')
            }
            return x + y
        }
        console.log(fn())

try catch处理异常

		function fn() {
            try {
                const p = document.querySelector('.p')
                p.style.color = 'red'
            } catch (err) {
                //拦截错误,提示浏览器提供的错误信息,但是不中断程序执行
                console.log(err.message)
                //需要加return中断程序,也可以搭配throw使用
                throw new Error('选择器错了')
                // return
            } finally {
                //无论是否有错误finally里面的都会执行
                alert('弹出对话框')
            }
            console.log(11)
        }
        fn()

debugger

相当于断点

this


this指向

普通函数谁调用指向谁,箭头函数指向上一层this

改变this

  • call()方法调用函数,可以同时指定被调用函数中的this

fn().call(thisArg,arg1,arg2,...)

thisArg:在fn函数运行时指定this的值1,arg1,arg2是函数传递的其它参数

		const obj = {
            uname: '三三',
            age: 20
        }
        function fn(x, y) {
            console.log(this)//{uname: '三三', age: 20}
            console.log(x + y)//3
        }
        fn.call(obj, 1, 2)
  • apply方法改变this指向

和call()基本无差别,就在传递参数有差别

		const obj = {
            uname: '三三',
            age: 20
        }
        function fn(x, y) {
            console.log(this)//{uname: '三三', age: 20}
            console.log(x + y)//3
        }
        fn.apply(obj, [1, 2])
        //可以用来求数组最大值
        const arr = [100, 3, 77]
        const max = Math.max.apply(Math, arr)
        const min = Math.min.apply(Math, arr)
        console.log(max, min)//100 3
  • bind()方法改变this指向

bind不会调用函数,返回值是一个指定了this值的新函数(原函数拷贝),只想改变this指向不想调用函数可以使用bind

	<button>点击</button>
    <script>
        const obj = {
            uname: '三三',
            age: 20
        }
        function fn() {
            console.log(this)
        }
        const fun = fn.bind(obj)
        fun()
        document.querySelector('button').addEventListener('click', function () {
            this.disabled = true
            setTimeout(function () {
                this.disabled = false
            }.bind(this), 2000)//这里的this指的button
        })
    </script>

防抖


防抖

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

  • 通过lodash库来实现防抖

    _.debounce(fun,wait())第一个参数是时间,第二个参数是等待时间

  • 手写防抖函数

核心利用setTimeout定时器,每次事件触发判断是否有定时器,有则清除,没有则开启定时器

分析:在每次鼠标移动都会触发debounce函数,但实际本身只被调用了一次。每次调用返回的新函数时,会检查timer是否已经启动,没有则设置一个新的定时器,从而延迟执行回调函数。

用return是因为1.addEvenListener函数的参数是一个函数,如果使用箭头函数不用return会让这个debounce函数一直被调用,timer一直是undefined。而使用闭包会继续保存变量的状态。

<div class="box"></div>
  <!-- <script src="lodash.min.js"></script> -->
  <script>
    let i = 1
    const box = document.querySelector('.box')
    function move() {
      box.innerHTML = i++
    }
    // box.addEventListener('mousemove', _.debounce((move), 500))
    //手写防抖函数
    function debounce(fn, t) {
      let timer
      return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(function () {
          fn()
        }, t)
      }
    }
    box.addEventListener('mousemove', debounce(move, 500))
  </script>

节流

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

  • 通过lodash库实现节流

    _.throttle(fun,wait())第一个参数是时间,第二个参数是等待时间

  • 手写节流函数

核心思想是利用setTimeout定时器,声明一个定时器变量,鼠标每次滑动先判断是都有定时器,有则不开启新的定时器,没有则开启定时器,存到变量里面,定时器里面调用执行的函数,还要把定时器清空

分析:节流就是每次定时器结束再开新的定时器,所以判断定时器还有没有,没有则加新的定时器并执行函数,有则不进行操作。定时器内部执行结束之后要清除定时器让下一个定时器开启。

<div class="box"></div>
  <!-- <script src="lodash.min.js"></script> -->
  <script>
    let i = 0
    const box = document.querySelector('.box')
    function move() {
      box.innerHTML = i++
    }
    // box.addEventListener('mousemove', _.throttle(move, 500))
    //手写节流函数
    function throttle(fn, t) {
      let timer = null
      return function () {
        if (!timer) {
          timer = setTimeout(function () {
            fn()
            timer = null//setTimeout里面是无法删除定时器的,这时候定时器还在运作,所以用timer = null
          }, t)
        }
      }

    }
    box.addEventListener('mousemove', throttle(move, 500))
  </script>

节流防抖总结

防抖一般用于搜索框输入、手机号、邮箱验证输入检测

节流一般用于高频事件,鼠标移动、页面尺寸缩放、滚动条滚动等