JS进阶
一、作用域
作用域规定了变量可以被访问的范围。
1.1 局部作用域
函数作用域:在函数内部声明的变量只能在函数内部被访问,外部无法访问。
块作用域:在JS中使用{}包裹的代码称为代码块,代码块内部声明的变量有可能无法被访问。
- let声明的变量会产生块作用域,var不会产生块作用域
- const声明的变量也会产生块作用域
- 不同代码块之间的变量无法相互访问
1.2 全局作用域
-
<script>标签内部,函数作用域和块作用域之外的区域
-
.js文件中
1.3 作用域链-------------面试
作用域链的本质是底层的变量查找机制。
过程:
- 在函数被执行时,会优先在当前函数作用域查找变量
- 若当前作用域中找不到,则会依次逐级查找父作用域直到全局作用域
总结:
-
嵌套关系的作用域串联起来形成了作用域链
-
相同作用域链中按着从小到大的规则查找变量
-
子作用域能够访问父级作用域,父级作用域无法访问子级作用域
1.4 垃圾回收机制------面试
内存的生命周期:
-
内存分配: 当声明变量、函数、对象的时候,系统会自动为他们分配内存,
-
内存使用: 读写内存,也就是使用变量、函数等,
-
内存回收: 使用完毕,由垃圾回收器回收不再使用的内存,
notice:
- 全局变量一般不会回收(页面关闭时才回收)
- 一般情况下局部变量的值不使用了,会被自动回收掉
- 内存泄漏: 程序中分配的内存由于某种原因程序未释放或无法释放
垃圾回收机制算法:
- 引用计数法: 看一个对象是否有指向它的引用,若没有引用就释放对象
- 跟踪记录被引用的次数
- 若被引用一次,那么记录数就加一,多次引用会累加
- 若减少一个引用就减一
- 如果引用次数为0,则释放内存
引用计数法的问题:嵌套引用时,不再使用也无法回收内存,导致内存泄漏
function fn(){
let obj1 = {}
let obj2 = {}
obj1.a = obj2
obj2.a = obj1
return '引用计数无法回收'
}
fn()
- 标记清除法
- 标记清除法将“不再使用的对象”定义为“无法到达的对象”;
- 从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的;
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后回收。
1.5 闭包-----------面试
闭包: 内层函数+外层函数的变量
作用:
-
封闭数据,实现数据私有
-
外部也可以访问函数内部的变量(只能访问,不能修改)
-
允许将函数与其所操作的某些数据关联起来
问题:可能会引起内存泄漏
// 闭包示例
function counter(){
let i = 0
function fn(){
i ++
console.log(`函数被调用了${i}次`);
}
return fn
}
let func = counter()
func()
1.6 变量提升
流程: 先把var声明的变量提升到当前作用域的最前面(只提升声明,不提升赋值),然后依次执行代码
notice:
- 变量在未声明前被访问时会报语法错误
- 变量在var声明之前即被访问,变量值为undefined
- let/const声明的变量不存在变量提升
- 变量提升出现在相同作用域中
二、函数进阶
2.1 函数提升
会将所有的函数声明提升到当前作用域的最前面,不提升函数调用。即以下代码可以执行。
fun()
function fun(){
console.log('函数提升')
}
notice: 函数表达式必须先声明和赋值后调用,否则会报错。
2.2 函数参数
动态参数:arguments是函数内部内置的伪数组变量,包含了调用函数时传入的所有实参
-
arguments是一个只存在于函数中的伪数组
-
arguments用于动态获取函数的实参
-
可以通过for循环得到传递过来的实参
剩余参数: 允许我们将不定量的参数表示为一个数组
- …是语法符号,置于最末函数形参之后,用于获取多余的参数
- 借助…获取的剩余实参,是个真数组
function getSum(){
let sum = 0
for(let i = 0; i < arguments.length;i ++){
sum += arguments[i]
}
return sum
}
console.log('====================================================================')
// 剩余参数
function getSum(a,b,...arr){
let sum = a + b
for(let i = 0; i < arr.length;i ++){
sum += arr[i]
}
return sum
}
展开运算符:(…)将一个数组或对象展开,不修改原数组 arr = [1,2,3,4] …arr = 1,2,3,4
应用场景:求最大最小值,合并数组
<script>
arr1 = [8,3,9,4]
arr2 = [3,2,7,5]
console.log(Math.max(...arr1)) // 求数组最大值
console.log(Math.min(...arr1)) // 求数组最小值
console.log([...arr1,...arr2]) // 合并数组
</script>
2.3 箭头函数
目的:实现更简短函数写法且不绑定this,箭头函数的语法比函数表达式更简洁
使用场景:用于本来需要匿名函数的地方
语法:
箭头函数属于表达式,因此不存在函数提升。
// 箭头函数基本语法
const fn = () =>{
console.log('这是箭头函数')
}
fn()
箭头函数只有一个参数时,可以省略圆括号();只有一行代码可以省略return。
// 只有一行代码时,可以不用return
const fn4 = x => x + x
console.log(fn4(444))
箭头函数只有一行代码时可以省略花括号{},并自动作为返回值被返回。
// 箭头函数只有一行代码时,可以不写{}
const fn3 = x => console.log(x)
fn3(333)
箭头函数中加括号的函数体返回对象字面量表达式。
// 箭头函数可以直接返回一个对象
const re_obj = (uname,age) => ({uname:uname,age:age})
console.log(re_obj('WXR',24))
**箭头函数参数:**没有动态参数arguments,但是有剩余参数…arr
剪头函数的this:箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this
<script>
const fn = () => console.log(this) // this指向window
fn()
const obj1 = {
uname : 'WRX',
fun1 : () => console.log(this) // this指向window,函数内才有this
}
obj1.fun1()
const obj2 = {
uname : 'WRX',
fun1 : function(){
const fun2 = () => console.log(this) // this指向obj
fun2()
}
}
obj2.fun1()
</script>
三、解构赋值
3.1 数组解构
数组解构是将数组元素快速批量赋值给一系类变量的简洁语法。
基本语法:
-
赋值运算符=左侧的[]用于批量声明变量,右侧数组的元素将被依次赋值给左侧的变量。
-
变量的顺序对应数组元素的位置依次进行赋值。
JS中前面必须加分号的情况:(1)立即执行函数 (2)数组解构
// 数组解构的相关知识点
<script>
// 基本语法
const [max,min,avg] = [100,60,80]
console.log(max,min,avg)
// 应用,交换两个变量
let a = 1
let b = 100;
[b,a] = [a,b]
console.log(a,b);
// 变量值少,单元值多
const [c,d,e,f] = [1,2,3] // d是undefined
// 变量多,单元值少
const [g,h,...arr] = [1,2,3,4] // arr是数组[3,4]
// 可以赋初值,防止undefined
const [i=1,j=2] = [100,80]
// 支持多维数组解构
const [x,y,[aa,bb]] = [1,2,[11,22]]
console.log(x,y,aa,bb)
</script>
3.2 对象解构
对象解构是将对象属性和方法快速批量赋值给一系类变量的语法。
基本语法:
- 赋值运算符=左侧{}用于批量声明变量,右侧对象的属性将被赋值给左侧变量。
- 对象的属性值将被赋值给与属性名相同的变量。
- 解构的变量名不能与外面的变量名相同,否则会报错。
- 对象中找不到与变量名一致的属性时变量值为undefined。
<script>
// 对象解构,基本语法
const {uname,uage} = {uname:'wxr',uage:24}
console.log(uname,uage)
// 对象解构,修改变量名
const {uname:name,uage:age} = {uname:'WXR',uage:23}
console.log(name,age)
// 数组对象解构
const [{name1,age1}] = [{name1:'WYX',age1:22}]
console.log(name1,age1)
// 对象的多级解构
const pig1 = {
name3 : '佩奇',
family : {
mother : '猪妈妈',
father : '猪爸爸',
sister : '乔治'
}
}
const {name3,family:{mother,father,sister}} = pig1
console.log(name3,mother,father,sister)
</script>
3.3 forEach()和filter()
forEach()作用: 遍历数组的每个元素(不返回数组,也不需要return,map会返回数组)
语法:
被遍历的数组.forEach(function(item,index){
// 函数体
})
filter()作用: 按条件选取数组中的值。
语法:
let arr = [1,2,4,5,4,8,7,6]
let arr1 = arr.filter(function(item){ // arr1为[5,8,7,8]
return item >= 5
})
四、深入对象
4.1 创建对象的三种方式
- 对象字面量创建对象:const obj = { }
- 利用new Object创建对象:const obj = new Object()
- 利用构造函数创建对象
4.2 构造函数
是一种特殊的函数,主要用来初始化对象
构造函数两个约定:
- 命名以大写字母开头。
- 只能由’new’操作符来执行。
说明:
- 使用new关键字调用函数被称为实例化
- 构造函数内部无需写return(即使写了return,返回的值也无效),返回值即为新创建的对象
- new Object() new Date() 也是实例化构造函数
- 存在内存浪费问题,由于某些对象的方法是相同的,但是在实例化时会给每个实例对象的该方法都开辟内存
4.3 new实例化过程-------面试
- 创建新的空对象
- 构造函数的this指向新对象
- 执行构造函数代码,修改this,添加新属性
- 返回新对象
4.4 实例成员和静态成员
实例成员:
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)
说明:
- 为构造函数传入参数,创建的是结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立互不影响
<script>
function Person(name,age){
Person.number = 123 // 静态成员
Person.type = 'people' // 静态成员
Person.info = () =>{ // 静态方法
console.log(Person.number,Person.type)
}
this.name = name
this.age = age
}
const son = new Person('WXR',24)
son.job = 'IT'
son.info = () => { // 实例方法
console.log(son.name,son.age,son.job) // 三个实例成员
}
son.info()
console.log(Person.number,Person.type)
Person.info()
</script>
静态成员:
构造函数的属性和方法被称为静态成员(静态属性和静态方法)
说明:
- 静态成员只能构造函数访问
- 静态方法中的this指向构造函数
- 如Date.now()、Math.PI、Math.random()
五、内置构造函数
JS中基本数据类型:字符串、数值、布尔、undefined、null
JS中字符串、数值、布尔等基本类型也都有专门的构造函数,这些称为包装类型。
JS中的对象(Object)、数组(Array)、正则(RegExp)、时间(Date)称为引用类型。
5.1 Object
三个常用的静态方法:
方法 | 解释 |
---|---|
Object.keys(对象名) | 获取对象中的所有属性名 |
Object.values(对象名) | 获取对象中的所有属性值 |
Object.assign(对象名1,对象名2) | 将对象2拷贝到对象1中 |
<script>
const Person = {name : 'WXR',age : 24}
// 获取Person的所有属性名
console.log(Object.keys(Person))
// 获取Person的所有属性值
console.log(Object.values(Person))
// 给Person添加一个属性gender
Object.assign(Person,{gender:'男'})
console.log(Person)
</script>
5.2 Array
数组常见实例方法:
方法 | 作用 | 说明 |
---|---|---|
forEach | 遍历数组 | 不返回数组,常用于查找遍历数组元素 |
filter | 过滤数组 | 返回新数组,返回的是满足筛选条件的数组元素 |
map | 迭代数组 | 返回新数组,返回的是处理之后的数组元素 |
reduce | 累计器 | 返回累计处理的结果。常用语数组求和等 |
<script>
// reduce方法实现数组累加
let arr = [1, 7, 2, 5, 3]
let sum = arr.reduce((prev,current) => prev + current , 12)
console.log(sum)
</script>
- 实例方法join(): 将数组元素拼接为字符串,然后返回字符串
- 实例方法find():查找元素,返回符合条件的第一个数组元素值,如果没有符合条件的则返回undefined
- 实例方法every():检测数组中所有元素是否都符合指定条件,若所有元素均满足返回true,否则返回false
- 实例方法some():检测数组中是否有元素满足指定条件,若数组中有元素满足条件返回true,否则返回false
- 实例方法concat(): 合并两个数组,返回生成的新数组
- 实例方法sort(): 对原数组的元素值排序
- 实例方法splice(): 删除或替换原数组元素值
- 实例方法reverse(): 反转数组
- 实例方法findIndex(): 查找元素的索引值
- 静态方法:伪数组转换为真数组 Array.from(伪数组名)
5.3 String
- 实例属性length: 用来获取字符串的长度
- 实例方法split(‘分隔符’): 用来将字符串拆分成数组--------和array.join()相反
- **实例方法substring(需要截取的第一个字符索引,[结束的索引号]):**用于字符串截取
- 实例方法startsWith(检测的字符串,[检测位置索引号]): 检测是否以某字符串开头
- 实例方法includes(搜索的字符串,[检测位置索引号]): 判断一个字符串是否包含在另一个字符串中,根据情况返回true或者false
- 实例方法toUpperCase(): 用于将字母转换成大写
- 实例方法toLowerCase(): 用于将字母转换成小写
- 实例方法indexOf(): 检测是否包含某字符
- 实例方法endsWith(): 检测是否以某字符串结尾
- 实例方法replace(): 用于替换字符串,支持正则匹配
- 实例方法match(): 用于查找字符串,支持正则匹配
5.4 Number
- 实例方法toFixed(数字): 保留多少位小数
六、深入面向对象
6.1 两种编程思想
面向过程: 分析出解决问题需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再调用就可以了。
面向对象: 是把问题分解为一个个对象,然后由对象之间分工与合作完成。
面向对象的特性:封装、继承、多态
6.2 构造函数
补充: 公共的属性写到构造函数里,公共的方法写到原型对象里
6.2 原型对象(构造函数名.prototype)
作用: 共享方法
- 构造函数通过原型分配的函数是所有实例对象所共享的
- JS中,每一个构造函数都会有一个prototype属性,指向另一个对象,所以我们也称为原型对象
- 原型对象可以挂载函数,使用构造函数创建对象时不会多次创建原型上的函数,节约内存
- 构造函数和原型对象中的this都指向实例化的对象
6.3 constructor
作用: 指向该原型对象的构造函数
<script>
function Person(){
}
console.log(Person.prototype)
console.log(Person.prototype.constructor === Person)
Person.prototype = {
constructor : Person, // 指回原来的对象
sing : function() {console.log('唱歌');},
dance : function() {console.log('跳舞');}
}
console.log(Person.prototype);
</script>
6.4 对象原型
语法:实例对象._proto_
- [[prototype]]和__proto__意义相同
- __proto__是只读属性,不能赋值
- 用来表明当前实例对象指向哪个原型对象prototype
- 对象原型(_proto_)指向原型对象prototype
构造函数、实例对象、原型对象三者之间的指向关系:
6.5 原型继承
原型继承格式: Boy.prototype = new Person()
<script>
function Person(){
this.eyes = 2
this.hands = 2
this.mouth = 1
}
function Boy(){}
function Girl(){}
Boy.prototype = new Person() // 原型继承的格式
Boy.prototype.constructor = Boy // 指回原实例对象
Girl.prototype = new Person()
Girl.prototype.constructor = Girl
Girl.prototype.hair = function (){console.log('通常是长头发');} // 给一个实例对象添加方法不影响其他实例对象
const ldh = new Boy() // 实例化
const ym = new Girl()
console.log(ldh)
console.log(ym)
</script>
6.6 原型链
- 只要是原型对象就有constructor
- 只要是对象,就有 _proto_
原型链是一种查找规则,提供了一条查找属性和方法的路径,先从自身的原型开始查找,若没有找到,则去上一层查找,直到对象为空为止。
instanceof 运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
七、拷贝
拷贝只针对引用类型**,浅拷贝是地址的拷贝,深拷贝是对象的拷贝。
7.1 浅拷贝
- 拷贝对象:Object.assign() / 展开运算符 {…obj}
- 拷贝数组:Array.prototype.concat() / 展开运算符 […arr]
<script>
const person = {
gender : '男',
year : 24,
}
const person1 = person // 直接赋值是将person的地址复制给person1,改变person1的属性值,person的属性值也会一起变化
console.log(person1 === person) // true
// 两种浅拷贝方法
const person2 = {...person}
console.log(person2 === person) // false
const person3 = {}
Object.assign(person3, person)
console.log(person3 === person) // false
</script>
浅拷贝总结:拷贝对象之后,若里面的属性值是简单数据类型则直接拷贝值;若属性值是引用数据类型则拷贝地址。
7.2 深拷贝
深拷贝目的: 使拷贝得到的对象不会影响原始的对象。
1.递归实现
<script>
const person = {
uname : 'ZGR',
age : 24,
hobby : ['sing','dance'],
song : {
first : '千千阙歌',
second : '沉默是金'
}
}
const per = {}
function deepCopy(newObj, oldObj){
for(let k in oldObj){
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(per,person)
per.hobby[0] = 'act'
console.log(per)
console.log(person)
</script>
函数递归步骤: 1、普通值拷贝直接赋值 2、如果遇到数组,再次调用递归函数 3、若遇到对象,再次调用递归函数 4、先处理数组,再处理对象。
2.利用js库ladash里面的_.cloneDeep()
3.利用JSON字符串转换(有函数时,用递归)
<script src="./lodash.min.js"></script>
<script>
const person = {
uname : 'ZGR',
age : 24,
hobby : ['sing','act'],
song : {
first : '千千阙歌',
second : '沉默是金'
}
}
const per1 = _.cloneDeep(person) // 方式二
const per2 = JSON.parse(JSON.stringify(person)) // 方式三
console.log(per1)
console.log(per2)
</script>
八、异常处理
8.1 throw抛异常
- throw抛出异常,程序也会终止运行
- throw后面跟的是错误提示信息
- Error对象配合throw使用,能够设置更详细的错误信息
<script>
function getSum(x,y){
if(!x || !y){
throw new Error('参数不能为空')
}
return x + y
}
getSum()
</script>
8.2 try/catch捕获异常
- try…catch用于捕获错误信息
- 将预估可能发生错误的代码写在try代码段中
- 若try代码段中出现错误,则会执行catch代码段,并截获到错误信息
- finally不管是否有错误,都会执行
<script>
function try_catch(){
try{
const tag = document.querySelector('.div')
tag.style.color = 'red'
}
catch(err){
console.log(err.message)
}
finally{
alert('程序执行完毕')
}
}
try_catch()
</script>
8.3 debugger
写在JS程序中,在调试时不用另外打断点即可调试。
九、处理this
9.1 this指向
普通函数中,谁调用函数this就指向谁;严格模式下,指向undefined;
箭头函数内不存在this。它的this是沿用上一层函数的。过程:向外层作用域中一层一层查找this,直到找到this的定义为止。
- 箭头函数不适用于构造函数、原型函数、字面量对象中的函数、DOM事件函数
- 箭头函数适用于需要使用上层this的地方
9.2 改变this指向
call():调用函数,同时指定被调用函数的指向(语法:fn.call(this指向,参数1,参数2,…))
apply():调用函数,同时指定被调用函数的指向(语法:fn.apply(this指向,[参数1,参数2,…])) 用数组传递参数(可用于求数组最大最小值)
bind():不调用函数,只改变this指向(语法:fn.bind(this指向,参数1,参数2,…))
总结:
- call()、apply()都会调用函数,返回值为函数执行结果,call()传递参数与普通函数传参一样,apply()传递的参数要放在数组里。
- bind()不会调用函数,返回值为改变this指向的原函数。
十、性能优化
10.1 防抖–debounce
防抖: 单位时间内,频繁触发事件,只执行最后一次
实现方法:(1) lodash提供的防抖函数(_.debounce(fn, 延迟时间)) (2) 手写防抖函数
body>
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
const box = document.querySelector('.box')
let i = 0
function mouseMove(){
box.innerHTML = i ++
}
// box.addEventListener('mousemove', _.debounce(mouseMove, 500)) // 方式一:调用lodash库中的防抖函数
function debounce(fn, t){ // 方式二:用定时器实现
let timer
return function (){
if(timer) clearTimeout(timer)
timer = setTimeout(fn, t)
}
}
box.addEventListener('mousemove', debounce(mouseMove, 500))
</script>
</body>
核心思路: 用定时器(setTimeout)实现
- 声明一个定时器
- 当有事件触发时,先判断是否已经有定时器,如果有定时器,则先清除
- 如果没有定时器则开启定时器,将其存到变量里
- 在定时器里调用要执行的函数
10.2 节流–throttle
节流: 单位时间内,频繁触发事件,只执行一次
实现方式:(1) lodash提供的节流函数(_.throttle(fn, 间隔时间)) (2) 手写节流函数
<body>
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
const box = document.querySelector('.box')
let i = 0
function mouseMove(){
box.innerHTML = i ++
}
// box.addEventListener('mousemove', _.throttle(mouseMove, 3000)) // 方式一:调用lodash库中的节流函数
function throttle(fn, t){ // 方式二:使用延时函数实现
let timer = null
return function (){
if(!timer){
timer = setTimeout(function(){
fn()
timer = null // 此处用这种方式清除定时器计时器,setTimeout内部不能使用clearTimeout清除
}, t)
}
}
}
box.addEventListener('mousemove', throttle(mouseMove, 3000))
</script>
核心思路: 用定时器(setTimeout)实现
-
声明一个定时器
-
当事件触发时都先判断是否有定时器了,如果有新的定时器则不开启新的定时器
-
如果没有定时器则开启定时器,将其存放到变量里
-
定时器里调用要执行的函数
-
执行完后要把定时器清空
-