1. 作用域
1.1 局部作用域
局部作用域分为函数作用域 和 块级作用域
块级作用域就是用 {} 包起来的,let、const声明的变量就是产生块作用域,var不会;不同代码块之间的变量无法互相访问,里面的变量外部无法访问
1.2 全局作用域
script标签和.js文件的最外层就是全局作用域,里面的变量都能被访问。
要尽量少声明全局变量,防止被污染
1.3 作用域链
作用域链本质是底层的变量查找机制。在函数被执行时,会优先查找当前函数作用域中查找变量,如果没找到就逐级向上查找父级作用域直到全局作用域。
子作用域能访问父作用域,但父的不能访问子的
1.4 垃圾回收机制(GC)
JS中内存的分配和回收都是自动完成的,内存不使用的时候就会被垃圾回收器自动回收
分配的内存一般是如下声明周期:内存分配、内存使用、内存回收。全局变量一般不会回收,关闭页面会回收。一般情况下,局部变量的值不用了,会被自动回收掉。
程序中分配的内存由于某种原因程序未释放或无法释放就叫做内存泄漏
1.5 JS垃圾回收机制-算法说明
堆栈空间分配:
- 栈:OS自动分配释放函数的参数、局部变量等,基本数据类型放在栈里
- 堆:一般程序员分配释放,若程序员不释放,就由垃圾回收机制释放。复杂数据类型放在堆里
常见浏览器的垃圾回收算法:引用计数法 和 标记清除法
引用计数不怎么用了,IE采用的这个,定义”内存不再使用“,看一个对象是否有指向它的引用,没有就回收掉。算法基本思路:记录被引用的次数;被引用就+1,减少引用就-1;如果引用次数为0,就释放内存。
引用计数很简单有效,但存在致命的问题:嵌套引用(循环引用),如果两个对象相互引用,即使已经不再使用了,但由于引用次数永远不会是0,所以不会被回收,导致内存泄漏。
现在浏览器不再使用引用计算算法,大多都是基于标记清楚算法的某些改进算法,思路是一致的。核心主要是:定义”无法到达的对象“;从根部(JS的全局对象)出发扫描内存中的对象,凡是能从根部到达的就都是还需要使用的;到不了的对象就被标记不再使用,稍后进行回收。
1.6 闭包
简单来说,闭包 = 内部函数 + 外层函数的变量,外部也可以访问函数内部的变量
应用:数据私有、外部可以访问函数内部的变量
会存在内存泄漏的问题
1.7 变量提升
把所有var声明的变量提升到当前作用域的最前面,只提升声明,不提升赋值。
let、const不存在变量提升,实际开发还是先声明再使用。
2. 函数进阶
2.1 函数提升
指的是函数声明之前也能调用。会把所有函数声明提升到当前作用域的最前面,只提升函数声明,不提升函数调用。
函数表达式必须先声明和赋值 后调用,否则报错
2.2 函数参数
不知道参入的参数的数量时,就可以用动态参数和剩余参数
1.动态参数
arguments是函数内部内置的伪数组变量,包含了调用函数时传入的所有实参
作用:动态获取函数的实参
2.剩余参数
允许我们将一个不定数量的参数表示为一个数组,…是语法符号,用于获取多余的实参(例如a, b, …arr就是把第一二个参数给a, b,剩下的给arr)
…获取的是个真数组,开发中更建议用剩余参数
展开运算符(…):它不只是用在函数上,对象也可以用。常见是用在数组上,将一个数组展开,运用场景:求数组最大最小值、合并数组等
2.3 箭头函数
引入箭头函数是要更简短写函数并且不绑定this,更适用于那些本来需要匿名函数的地方。箭头函数属于表达式函数,不存在函数提升。
只有一个形参的时候,可以省略();只有一行代码时可以省略{},并自动做为返回值被返回;箭头函数可以返回一个对象。
箭头函数没有arguments,有剩余参数;箭头函数没有自己的this,它沿用上一层的作用域的this;
3. 解构赋值
3.1 数组解构
数组解构时将数组的单元值快速批量赋值给一系列变量的简洁语法。变量的顺序对应数组单元值的位置依次进行赋值操作,典型场景:交换两个数
JS前面必须加分号的情况:立即执行函数;数组解构(数组的开头,特别是前面有语句的一定要加分号)
变量多,单位值少时会赋值为undefined;变量是可以设置初始值的;支持多维数组解构
3.2 对象解构
对象解构时将对象属性和方法快速批量赋值给一系列变量的简洁语法,属性名和变量名一致才能解构出来
4. 深入对象
4.1 构造函数
构造函数是一种特殊的函数,主要用来初始化对象,可以通过构造函数来快速创建多个类似的对象
有两个约定:
- 首字母必须大写
- 用new关键字使用
使用new关键字调用函数的行为被称为实例化,内部无需写return,返回值就是创建的新对象
4.2 实例化执行过程
1.创建新的空对象
2.构造函数this指向新对象
3.执行构造函数代码,修改this,添加新的属性
4.返回新对象
4.3 实例成员&静态成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)
- 为构造函数传入参数,创建结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立,互不影响
构造函数的属性和方法被称为静态成员(静态属性和静态方法)
- 静态成员只能构造函数来访问
- 静态方法中的this指向创造函数
4.4 基本包装类型
基本包装类型:js底层完成:把简单数据类型包装成了引用数据类型
其实字符串、数值、布尔等基本类型也都有专门的构造函数,这些我们称为包装类型
5. 内置构造函数
5.1 Object静态方法
Object.keys():获取对象中所有属性(键)
Object.values():获取对象中所有属性值
- 返回的是一个数组
Object.assign(新的,被拷贝的):用于对象拷贝(浅拷贝
- 场景:给对象添加属性
5.2 数组方法
常见的实例方法:
方法 | 作用 | 说明 |
---|---|---|
forEach | 遍历数组 | 不返回数组,用于查找遍历数组元素 |
map | 迭代数组 | 返回新数组,返回的是处理后的数组元素 |
filter | 过滤数组 | 返回新数组,返回的是筛选满足条件的数组元素 |
reduce | 累计器 | 返回累计处理的结果,用于求和等 |
reduce基本语法:arr.reduce(function(上一次值, 当前值){}, 初始值)
- 如果有初始值,则把初始值累加到里面
// 用箭头函数更简洁
arr.reduce((sum, item) => sum + item, 0)
reduce的执行过程:
- 如果没有起始值,则上一次值为数组的第一个数组元素的值
- 每一次循环,把返回值给作为 下一次循环的上一次值
- 如果有起始值,则起始值作为上一次值
常见的其他方法:
Array.from():伪数组转换成真数组
5.3 String方法
常见的实例方法:
把字符串转换成数组用split,把数组转换成字符串用join
5.4 Number方法
Number()直接使用传数字
toFixed():设置保留小数位的长度
6. 深入面向对象
6.1 编程思想
面向过程是分析解决问题的步骤,用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用即可
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作
面向对象的特性:封装性、继承性、多态性
- 封装:将数据和作用于数据的方法封装到一个单元内部,对外隐藏内部的实现细节,只暴露必要的接口供外部访问
- 继承性:一个类可以继承另一个类的属性和方法,通过调用,子类可以重写父类的代码,添加新的属性或方法
- 多态性:允许不同的类通过共同的接口来表现不同的行为,多态性可以在运行时才确定具体方法,而不用在编译时确定
js面向对象是通过构造函数来实现的,而构造函数存在的问题是浪费内存。我们希望所有的对象使用同一个函数,这样比较节省内存,那么我们就可以通过原型来实现。
6.2 原型
6.2.1 原型
目的:能够利用原型对象实现方法共享
- 构造函数通过原型分配的函数是所有对象所共享的
- JS规定,每一个构造函数都有一个prototype属性,指向另一个对象,所以我们也称为原型对象
- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例可以共享这些方法
- 构造函数和原型对象中的this都指向实例化的对象
公共的属性写在构造函数里,公共的方法写在原型对象里
6.2.2 constructor属性
每个原型对象都有一个constructor属性,该属性指向该原型对象的构造函数,简单来说,就是指向我爸爸,我是有爸爸的孩子
使用场景:当有多个对象的方法,我们可以给原型对象采取对象形式赋值,但这样会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor就不再指向当前构造函数了,这时,我们可以在修改后的原型对象中,添加一个constructor指向原来的构造函数
6.2.3 对象原型
对象都会有一个属性__proto__
指向构造函数的prototype原型对象,之所以对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__
原型的存在
__proto__
是非标准的属性,[[prototype]]和__proto__
意义相同,只读的,只能获取不能赋值,用来表明当前实例对象指向哪个原型对象prototype;__proto__
对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数
对象原型__proto
指向原型对象prototype
6.2.4 原型继承
相同的属性和方法,但是要不同的对象,这时就可以用构造函数
子类的原型 = new 父类
6.2.5 原型链
只要是对象,就有__proto__
,指向构造函数的原型对象;而原型对象也有自己的__proto__
,又继续往上指,直到Object
只要是原型对象,就有constructor
原型链其实就是查找规则,查找属性和方法时的一条路,先查找这个对象自身有没有该属性,没有就去查找它的原型,一直往上找,直到找到Object为止
可以使用instaceof运算符来检测构造函数的prototype属性是否出现在某个实例对象的原型链上([1, 2, 3] instaceof Array true)
7. 深浅拷贝
浅拷贝和深拷贝只针对引用类型
7.1 浅拷贝
浅拷贝:拷贝的是地址
常见方法:
1.拷贝对象:Object.assign() / 展开运算符 {…obj} 拷贝对象
2.拷贝数组:Array.prototype.concat() 或者 […arr]
问题:如果是单层对象就没有问题,但遇到多层嵌套对象就会出错;浅拷贝只是拷贝浅浅的一层
7.2 深拷贝
深拷贝:拷贝的是对象,不是地址
常用方法:
1.通过递归实现拷贝
2.lodash库的_.cloneDeep()
3.通过JSON.stringify()实现
8. 异常处理
8.1 throw抛异常
throw抛出异常信息,程序也会终止执行
Error对象配合throw使用,能够设置更详细的错误信息
8.2 try / catch捕获异常
try写可能发生错误的代码
catch里面有参数,拦截错误,提示浏览器提供的错误信息,不会中断程序的执行,需要加return来中断程序
finally是一定会执行的代码
9. 处理this
9.1 this的指向
普通函数下,谁调用,this的值就指向谁。没有明确调用者时this指向window,严格模式下没有调用者this的值是undefined
箭头函数自身没有this,箭头函数中的this引用就是最近作用域中的this,向外层作用域中一层一层查找this,直到有this的定义
对象是没有this的,函数作用域才有this
PS:
- DOM事件回调函数如果里面需要DOM对象的this,就不要使用箭头函数了
- 基于原型的面向对象也不推荐使用箭头函数
9.2 改变this
有三个方法可以动态指定普通函数中this的指向:call、apply、bind
call的作用:调用函数;改变this指向
- fn.call(thisAvg, avg1, avg2):thisAvg是将this指向谁;avg1、avg2是参数
- 返回值就是函数的返回值,因为它就是调用函数
apply:调用函数;改变this指向
- fn.apply(thisAvg, [argsArray]):argsArray传递的值必须包含在数组里面
- 返回值就是函数的返回值,因为它就是调用函数
- 使用场景:求数组最大值
bind:不会调用函数,但是能改变函数内部this指向
- fn.bind(thisAvg, avg1, avg2, …)
- 返回值是个函数,但是这个函数里面的this是更改过的
- 使用场景:不想立马调用函数,例如改变定时器内部的this指向
10. 防抖节流
10.1 防抖
防抖:单位时间内,频繁触发事件,只执行最后一次
使用场景:
- 搜索框搜索输入,只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
实现方式:
- 使用lodash库的防抖函数:_.debounce(fn, time)
- 手写一个防抖函数
- 因为要每次移动都要触发这个函数,所以要return一个匿名函数,这样就相当于是一移动就调用函数
10.2 节流
节流:单位时间内,频繁触发事件,只执行一次
使用场景:
- 鼠标移动
- 页面尺寸缩放
- 滚动条滚动
实现方式:
- 使用lodash库的节流函数:_.throttle(fn, wait)
- 手写一个节流函数
- 在setTimeout中是无法删除定时器的,因为定时器还在运作,所以使用timer = null 而不是clearTimeout(timer)
一次
- 在setTimeout中是无法删除定时器的,因为定时器还在运作,所以使用timer = null 而不是clearTimeout(timer)