作用域
局部作用域
局部作用域分为函数作用域和块作用域。
- 函数作用域:在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
-
- 函数内部声明的变量,在函数外部无法被访问
-
- 函数的参数也是函数内部的局部变量
-
- 不同函数内部声明的变量无法互相访问
-
- 函数执行完毕后,函数内部的变量实际被清空了
-
- 块作用域:
在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。-
- let 声明的变量会产生块作用域,var 不会产生块作用域
-
- const 声明的常量也会产生块作用域
-
- 不同代码块之间的变量无法互相访问
-
- 推荐使用 let 或 const
-
全局作用域
<script>
标签 和 .js 文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
全局作用域中声明的变量,任何其它作用域都可以被访问
注意:
- 为 window 对象动态添加的属性默认也是全局的,不推荐!
- 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
作用域链
- 作用域链本质上是底层的变量查找机制。
- 在函数被执行时,会优先查找当前函数作用域中查找变量
- 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
- 总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
JS垃圾回收机制
垃圾回收机制
- 垃圾回收机制(Garbage Collection) 简称 GC
- JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
- 正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题
- 但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况
- 不再用到的内存,没有及时释放,就叫做内存泄漏
内存的生命周期
JS环境中分配的内存, 一般有如下生命周期:
1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
2. 内存使用:即读写内存,也就是使用变量、函数等
3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
4. 说明:
- 全局变量一般不会回收(关闭页面回收);
- 一般情况下局部变量的值, 不用了, 会被自动回收掉
垃圾回收算法说明
所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉
两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法
- 引用计数
- 跟踪记录每个值被引用的次数。
- 如果这个值的被引用了一次,那么就记录次数1
- 多次引用会累加。
- 如果减少一个引用就减1。
- 如果引用次数是0 ,则释放内存。
const person = {
age: 18,
name: 'pink老师'
}
const p = person
person = 1
p = null
缺点:嵌套引用
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function fn() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用计数无法回收'
}
fn()
因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露
- 标记清除法
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进 行回收。
闭包
- 概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
- 简单理解:闭包 = 内层函数 + 外层函数的变量
- 闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量
- 闭包会引起内存泄漏
- 闭包的基本格式:
- 闭包应用:实现数据的私有
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
- 变量在未声明即被访问时会报语法错误
- 变量在var声明之前即被访问,变量的值为 undefined
- let/const 声明的变量不存在变量提升
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
ES6 引入了块级作用域,用let 或者 const声明变量,让代码写法更加规范和人性化
函数进阶
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
函数参数
动态参数
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
- arguments 是一个伪数组,只存在于函数中
- arguments 的作用是动态获取函数的实参
- 可以通过for循环依次得到传递过来的实参
剩余参数
剩余参数允许我们将一个不定数量的参数表示为一个数组
- … 是语法符号,置于最末函数形参之前,用于获取多余的实参
- 借助 … 获取的剩余实参,是个真数组
箭头函数
- 目的:引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁
- 使用场景:箭头函数更适用于那些本来需要匿名函数的地方
基本写法
- 语法一
- 语法2:只有一个参数可以省略小括号
- 语法3:如果函数体只有一行代码,可以写到一行上,并且无需写 return 直接返回值
- 语法4:加括号的函数体返回对象字面量表达式
箭头函数参数
- 普通函数有arguments 动态参数
- 箭头函数没有 arguments 动态参数,但是有 剩余参数 …args
箭头函数 this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
注意
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此
DOM事件回调函数为了简便,还是不太推荐使用箭头函数
解构赋值
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值。
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法。
- 基本语法:
- 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
- 变量的顺序对应数组单元值的位置依次进行赋值操作
- 典型应用交互2个变量
- 变量多 单元值少的情况:
- 变量少 单元值多的情况:
- 利用剩余参数解决变量少 单元值多的情况:
- 防止有undefined传递单元值的情况,可以设置默认值:
允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效
- 按需导入,忽略某些返回值:
- 支持多维数组的结构:
对象解构
基本语法:
- 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
- 对象属性的值将被赋值给与属性名相同的变量
- 注意解构的变量名不要和外面的变量名冲突否则报错
- 对象中找不到与变量名一致的属性时变量值为 undefined
- 给新的变量名赋值:
冒号表示“什么值:赋值给谁”
- 数组对象解构
- .多级对象解构:
深入对象
创建对象的三种方式
1. 利用对象字面量创建对象
2. 利用 new Object 创建对象
3. 利用构造函数创建对象
构造函数
- 构造函数 :是一种特殊的函数,主要用来初始化对象
- 使用场景:常规的 {…} 语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一
遍,此时可以通过构造函数来快速创建多个类似的对象。
- 构造函数在技术上是常规函数。
不过有两个约定:- 它们的命名以大写字母开头。
- 它们只能由 “new” 操作符来执行。
构造函数语法:大写字母开头的函数
- 创建构造函数:
说明:- 使用 new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数时可以省略 ()
- 构造函数内部无需写return,返回值即为新创建的对象
- 构造函数内部的 return 返回的值无效,所以不要写return
- new Object() new Date() 也是实例化构造函数
- 实例化执行过程
说明:- 创建新对象
- 构造函数this指向新对象
- 执行构造函数代码,修改this,添加新的属性
- 返回新对象
实例成员&静态成员
实例成员:
- 通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
说明:- 实例对象的属性和方法即为实例成员
- 为构造函数传入参数,动态创建结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立互不影响。
静态成员:
- 构造函数的属性和方法被称为静态成员
说明:- 构造函数的属性和方法被称为静态成员
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的 this 指向构造函数本身
内置构造函数
在 JavaScript 中最主要的数据类型有 6 种:
- 基本数据类型:
- 字符串、数值、布尔、undefined、null
- 引用类型:
- 对象
其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型。
JS中几乎所有的数据都可以基于构成函数创建。
- 引用类型
- Object,Array,RegExp,Date 等
- 包装类型
- String,Number,Boolean 等
Object
- Object 是内置的构造函数,用于创建普通对象。
推荐使用字面量方式声明对象,而不是 Object 构造函数
-
三个常用静态方法(静态方法就是只有构造函数Object可以调用的)
新方法 -
作用:Object.keys 静态方法获取对象中所有属性(键)
- 语法:
- 注意: 返回的是一个数组
- 语法:
-
作用:Object.values 静态方法获取对象中所有属性值
- 语法:
- 注意: 返回的是一个数组
- 语法:
-
作用:Object. assign 静态方法常用于对象拷贝
- 语法:
- 使用:经常使用的场景给对象添加属性
- 语法:
Array
https://editor.csdn.net/md/?articleId=127022546
String
- 在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法
= 之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被
称为包装类型。
常见的实例方法
Number
- Number 是内置的构造函数,用于创建数值
常用方法:
- toFixed() 设置保留小数位的长度
其他
展开运算符
展开运算符(…),将一个数组进行展开
典型运用场景: 求数组最大值(最小值)、合并数组等
** js 前面必须加分号情况**
-
立即执行函数
-
数组解构
编程思想
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次
调用就可以了。
- 优点:性能比面向对象高,适合跟硬件联系很紧密
的东西,例如单片机就采用的面向过程编程。 - 缺点:没有面向对象易维护、易复用、易扩展
面向对象编程 (oop)
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
- 特征:封装、继承、多态。
- 优点:易维护、易复用、易扩展,由于面向对象有封装
、继承、多态性的特性,可以设计出低耦合的系统,使
系统 更加灵活、更加易于维护 - 缺点:性能比面向过程低
封装(构造函数)
封装的本质是将具有关联的代码组合在一起,其优势是能够保证代码复用且易于维护,函数是最典型也是最基础的代码封装形式,面向对象思想中的封装仍以函数为基础,但提供了更高级的封装形式。
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
- 存在浪费内存的问题(每次new对象都会产生一个新的的复杂类型,不管用不用)
- 命名空间式的封装无法保证数据的独立性
原型
原型对象
实际上每一个构造函数都有一个名为 prototype
的属性,译成中文是原型的意思,prototype
的是对象类据类型,称为构造函数的原型对象,每个原型对象都具有 constructor
属性代表了该原型对象对应的构造函数。
利用原型对象实现方法共享
- 构造函数通过原型分配的函数是所有对象所 共享的。
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
原型对象里面的this指向还是实例对象
constructor 属性
每个原型对象里面都有个constructor 属性(constructor 构造函数)
- 作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子
- 使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
对象原型
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype
原型对象的属性和方法,就是因为对象有 proto 原型的存在。
注意:
- proto 是JS非标准属性
- [[prototype]]和__proto__意义相同
- 用来表明当前实例对象指向哪个原型对象prototype
- __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承
的特性。
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对
象的链状结构关系称为原型链
原型链-查找规则
① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
2.拷贝数组:Array.prototype.concat() 或者 […arr]
- 直接赋值和浅拷贝的区别
- 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
- 浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响
深拷贝
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
通过递归实现深拷贝
- 函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
- 通过递归函数实现深拷贝(简版)
js库lodash里面cloneDeep内部实现了深拷贝
通过JSON.stringify()实现
异常处理
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
throw 抛异常
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
try/catch 捕获错误信息
- try…catch 用于捕获错误信息
- 将预估可能发生错误的代码写在 try 代码段中
- 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
- finally 不管是否有错误,都会执行
debugger
this
默认值
普通函数
- 普通函数的调用方式决定了
this
的值,即【谁调用this
的值指向谁】 - 注: 普通函数没有明确调用者时
this
值为window
,严格模式下没有调用者时this
的值为undefined
。
箭头函数
- 箭头函数中的
this
与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在this
!箭头函数中访问的this
不过是箭头函数所在作用域的this
变量。 - 在开发中【使用箭头函数前需要考虑函数中
this
的值】,事件回调函数使用箭头函数时,this
为全局的window
,因此DOM事件回调函数不推荐使用箭头函数 - 同样由于箭头函数
this
的原因,基于原型的面向对象也不推荐采用箭头函数
this指向
一般的谁调用就指向谁,严格模式,没有指向undefined。
箭头函数
- 向外层作用域中查找this。
- 原型,构造函数和事件最好不用箭头函数
call
- 使用
call
方法调用函数,同时指定函数中this
的值call
方法能够在调用函数的同时指定this
的值- 使用
call
方法调用函数时,第1个参数为this
指定的值 call
方法的其余参数会依次自动传入函数做为函数的参数
apply
apply
方法能够在调用函数的同时指定this
的值- 使用
apply
方法调用函数时,第1个参数为this
指定的值 apply
方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
bind
方法并不会调用函数,而是创建一个指定了this
值的新函数,- 注:
bind
方法创建新的函数,与原函数的唯一的变化是改变了this
的值。
call apply bind
- 区别
- call 和 apply 会调用函数, 并且改变函数内部this指向.
- call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2…形式 apply 必须数组形式[arg]
- bind 不会调用函数, 可以改变函数内部this指向.
- 主要应用场景:
- call 调用函数并且可以传递参数
- apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
性能优化(防抖节流)
防抖(debounce)
- 所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
- 开发使用场景- 搜索框防抖
- 假设输入就可以发送请求,但是不能每次输入都去发送请求,输入比较快发送请求会比较多
我们设定一个时间,假如300ms, 当输入第一个字符时候,300ms后发送请求,但是在200ms的时候又输入了一个字符,
则需要再等300ms 后发送请求
- 假设输入就可以发送请求,但是不能每次输入都去发送请求,输入比较快发送请求会比较多
- 方法:利用延时器 实现原理:提前定义容器变量用来保存setTimeout的返回值,在每次触发事件,准备开启新的setTimeout之前,先检查容器变量中是否保存有setTimeout的返回值,如果有,那么不再开启setTimeout,保证同一时间只有一个setTimeout存在。setTimeout执行完毕之后,手动清空容器变量的返回值。
//定时器
document.onmousemove = debounce (function(){
console.log("函数节流");
})
// 函数防抖
function debounce (cd,time=300){
var t = null;
return function(){
if(t) return;
t = setTimeout(() => {
cd.call(this);
t = null;
}, time);
}
}
案例
const box = document.querySelector('.box')
let i = 1 // 让这个变量++
// 鼠标移动函数
function mouseMove() {
box.innerHTML = ++i
// 如果里面存在大量操作 dom 的情况,可能会卡顿
}
// 防抖函数
function debounce(fn, t) {
let timeId
return function () {
// 如果有定时器就清除
if (timeId) clearTimeout(timeId)
// 开启定时器 200
timeId = setTimeout(function () {
fn()
}, t)
}
}
// box.addEventListener('mousemove', mouseMove)
box.addEventListener('mousemove', debounce(mouseMove, 200))
节流(throttle)
-
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数
-
开发使用场景 –
- 小米轮播图点击效果 、 鼠标移动、页面尺寸缩放resize、滚动条滚动 就可以加节流
- 假如一张轮播图完成切换需要300ms, 不加节流效果,快速点击,则嗖嗖嗖的切换
- 加上节流效果, 不管快速点击多少次, 300ms时间内,只能切换一张图片。
-
方法:利用时间戳 实现原理:提前设定变量,准备存储事件结束后的时间戳,在事件开启之后,立即保存时间戳,并判断当前时间戳和事件结束后的时间戳的差值,决定是否需要执行本次事件。事件执行完毕之后,保存事件结束时的时间戳,以供下次开启事件时计算差值。
//时间相减
document.onmousemove = throttle(function(){
console.log("函数节流")
})
function throttle(cb, wait=300){
let last = 0;
return function(){
var now = new Date().getTime();;
if (now - last > wait) {
cb.call(this);
last = new Date().getTime();;
}
}
}
案例,滑动加1
const box = document.querySelector('.box')
let i = 1 // 让这个变量++
// 鼠标移动函数
function mouseMove() {
box.innerHTML = ++i
// 如果里面存在大量操作 dom 的情况,可能会卡顿
}
// console.log(mouseMove)
// 节流函数 throttle
function throttle(fn, t) {
// 起始时间
let startTime = 0
return function () {
// 得到当前的时间
let now = Date.now()
// 判断如果大于等于 500 采取调用函数
if (now - startTime >= t) {
// 调用函数
fn()
// 起始的时间 = 现在的时间 写在调用函数的下面
startTime = now
}
}
}
box.addEventListener('mousemove', throttle(mouseMove, 500))
Lodash 库 实现节流和防抖
例页面打开,可以记录上一次的视频播放位置