根据前端技能树补缺JS知识
作用域
作用域(scope)规定了变量能够被访问的范围,离开范围后变量即不能使用
作用域分为:
- 全局作用域
- 局部作用域
局部作用域
局部作用域可分为两类:
- 函数作用域
- 块作用域
函数作用域
在函数内部声明的变量只有函数内可以访问
1 | function fun() { |
- 函数内部声明的变量,外部无法访问
- 函数参数也是函数内部的局部变量
- 不同函数内部声明的变量无法访问
- 函数执行完毕后,内部变量实际被清空了
块作用域
JS中被{}
包裹的部分称为代码块,块外的代码有可能无法访问
1 | for (let i = 1; i <= 3; i++) { |
- let声明的变量回产生块作用域,var则不会产生块作用域
- const声明的变量也会产生块作用域
- 不同代码间的变量无法互相访问
- 推荐使用let和const
- 块作用域是ES6后新加入的
全局作用域
在<script>
标签和.js
文件的最外层就是所谓全局作用域,全局作用域中声明的变量在任何地方都可以被访问
1 | <script> |
- 为window对象动态添加的属性默认也为全局作用域(不推荐
- 函数中未使用任何关键字声明的变量为全局作用域(不推荐
- 尽可能少使用全局变量,防止变量污染
作用域链
作用域链的本质是变量查找机制
- 在函数执行时,会优先查找当前作用域中的变量
- 如果当前作用域查找不到,则会依次逐级查找父级作用域直到全局作用域
即:
- 嵌套关系的作用域串联行程了作用域链
- 相同作用域链中按从小到大的规则查找变量
- 子作用域能够访问父作用域,父作用域无法访问子作用域
1 | <script> |
垃圾回收机制
垃圾回收机制即(Garbage Collection,GC)
JS中内存的分配和回收是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
内存生命周期
JS分配内存的过程一般经过以下三个生命周期:
- 内存分配:声明变量、函数、对象时,系统会自动分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存销毁:使用完毕,由垃圾回收器自动回收不再使用的内存
注意:
- 全局变量一般不会回收(关闭页面才回收
- 一般情况下局部变量的值,不用了会被自动回收
内存泄露:程序中分配的内存由于某种原因程序未释放或无法释放叫作内存泄漏
算法说明
堆栈空间分配区别:
- 栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型都存放在栈中
- 堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放在堆中
目前浏览器中常用的垃圾回收算法有:
- 引用计数法
- 标记清除法
引用计数法
IE采用的算法,定义内存不再使用,就是看一个对象是否有指向它的引用,没有引用了就回收对象:
算法
- 跟踪记录被引用的次数
- 如果被引用了一次,那么记录次数1,多次引用会累加++
- 如果减少一个引用就减1
- 如果引用为0,则释放内存
问题
引用计数存在一个致命问题:嵌套引用(循环引用)
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器也不会进行回收,则会导致内存泄漏
1 | function fn() { |
此时两个对象的引用计数一直为1,这样的相互引用则会造成大量的内存泄露
标记清除法
现代浏览器中已经不再使用引用计数法了。
标记清除法是对其的改进
算法
- 标记清除法将“不再使用的对象”定义为**“无法到达的对象”**
- 即从根部(JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收
由于查找对象必须从根部出发,因此对于相互引用的情况,由于其无法从根部出发找到,因此均会被回收
闭包
闭包Closure
概念:一个函数堆周围状态的引用捆绑在一起,内层函数中访问到其他外层函数的作用域
即:闭包 = 内存函数 + 外层函数的变量
最简单的函数闭包写法如下:
1 | function outer() { |
其中函数f
和变量a
构成了闭包
需要注意的是:
- 简单的外层函数中定义里层函数是无法形成闭包的
- 只有当里层函数访问了外层函数变量时,才会产生闭包
此时,在函数f
内设置断点,在浏览器开发者页面的Sources标签中 -> Scope标签中会显示名为Local、Closure、Global的作用域
闭包作用与写法
闭包的作用:
- 封闭数据,提供操作,外部可以访问函数内部的变量
- 它允许将函数与其所操作的某些数据(环境)关联起来
更多情况下闭包的基本格式如下:
1 | function outer() { |
闭包应用
闭包可以实现数据私有
假设现在有一个需求要记录一个函数调用的次数:
1 | let i = 0 |
此时虽然可以实现这样的效果,但是由于i
的作用域是全局,则外部可以通过i = 1000
来修改函数调用的次数。
因此我们可以使用闭包让变量i
称为私有
1 | function count() { |
此时外部已经无法修改i
的值了
注意:
- 此时调用count()()的结果始终是1,这是因为直接调用时,count()返回fn这个函数,因此相当于在全局作用域中直接访问作用域fn,这就意味着位于作用域count的变量
i
没有被添加到作用域链,无法被标记清除法到达,调用一次后即被销毁- 调用fun()的结果才会正常累加,因为执行
const fun = count()
时,位于全局作用域的fun访问了局部作用域count,而count访问了局部作用域fn,因此其中i是可以被标记清除算法到达的,则不会被回收,发生了内存泄露
闭包存在的问题
会引发内存泄露(详见上例
变量提升
变量提升是JS的缺陷,仅存在于var
声明变量
被var声明的变量,其声明的过程(注意仅提升声明过程,初始化过程不会提升)会被提升到当前作用域的开头
即如下代码:
1 | console.log(num) // undefined |
相当于如下代码:
1 | var num |
注意事项:
- 变量未声明即被访问会发生语法报错
- 变量在var声明之前即被访问,其值未
undefined
let/const
声明的变量不存在变量提升- 变量提升出现在相同(当前)作用域中
- 实际开发中请先声明再访问,推荐使用
let/const
函数进阶
函数提升
函数允许在声明之前调用:
1 | fn() |
解释器会把函数声明提升到当前作用域的最前面
但是使用函数表达式的形式声明函数时,函数不会被提升:
1 | fun() // 此处报错,fun is not a function |
因为变量也具有变量提升,而此时的函数定义包含在赋值语句中,因此对于变量的提升,只提升其声明部分var fun
,而对其赋值部分(即函数定义)不进行提升,因此无法提前调用
函数提升的特点:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域中
函数参数
函数动态参数
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
1 | function getSum() { |
arguments
是一个伪数组,只存在于函数中- 1
arguments
的作用是动态获取函数的实参 - 可以通过for循环一次得到传递过来的实参
剩余参数
允许将不定数量的参数作为一个数组
1 | function getSum(..arr) { |
...
是语法符号,置于函数形参之前,用于获取多余的实参- 借助
...
的剩余实参,是个真数组
因此开发中还是提成使用剩余参数
展开运算符
展开运算符可以将一个数组进行展开
1 | const a = [1, 2, 3, 4] |
- 不会修改原数组
- 最典型的应用场景:求数组最大值、合并数组等
- 其返回值使用逗号隔开的
需要注意区分剩余参数和展开运算符的区别:
- 剩余参数:函数参数使用,得到真数组
- 展开运算符:数组中使用,数组展开
箭头函数
目的:箭头函数的引入是为了创造一种更简短的函数写法,并且不绑定this
使用场景:更适用于那些本来需要匿名函数的地方
写法:
1 | const fn = (x) => { |
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号()
- 箭头函数函数体只有一行代码时可以省略花括号{},并自动做为返回值被返回
- 加括号的函数体返回对象字面量表达式
箭头函数的参数
箭头函数中没有动态参数arguments
,但是有剩余参数...args
1 | const getSum = (...arr) => { |
箭头函数的this
箭头函数不会自己创建this,之后从自己的作用域链的上一层沿用this
1 | console.log(this) // window |
因此在开发中需要注意,事件回调函数使用this时,this未全局的window,因此DOM事件回调函数为了简便,不推荐使用箭头函数
解构赋值
数组解构
数组解构是将数组的单元值快速批量给一系列变量的简洁语法
1 | const arr = [100, 60, 80] |
典型的应用是在变量交换:
1 | let a = 1 |
这里需要注意两个特殊的必须加分号的地方,详见后文
单元值少变量多
1 | const [a,b,c,d] = [1,2,3] // 此时d为undefined |
单元值多变量少
1 | const [a,b,c] = [1,2,3,4] // a = 1, b = 2, c = 3 |
还可以结合剩余参数来利用这一特性:
1 | const [a,b,...c] = [1,2,3,4] // a = 1, b = 2, c = [3,4]真数组 |
防止undefined传递
可以使用类似参数默认值的方式设置默认值
1 | const [a = 0, b = 0] = [] // a = 0, b = 0 |
按需导入
1 | const [a, b, , d] = [1, 2, 3, 4] |
多维数组
1 | const [a, b, [c, d] = [1, 2, [3, 4]] // a=1,b=2,c=3,d=4 |
对象解构
解构基本解构基本语法:
1 | const { uname, age } = { uname: "teacher", age: 18} // 解构时按名解构,属性名与变量名需要相同 |
但解构时使用的变量可以通过以下方式改名:
1 | const { uname: username, age} = { uname: "teacher", age: 18} // 注意更改后的新变量名在冒号后 |
数组对象的混合解构:
1 | const [{ uname, age }] = [{ uname: "佩奇", age: 6 }] |
多层对象解构:
1 | const { uname, family: { mother, father, sister} } = { uname: "佩奇", family: { father: '猪爸爸', mother: '猪妈妈', sister: '乔治'}, age: 18} // 多级解构需要指定第次级属性名 |
解构方法可以直接出现在函数的参数中:
1 | function render({ data: myData }) { |
必须加分号的地方
- 理解执行函数前必须有分号:
1 | (function(){})(); |
- 数组解构的上一句为赋值语句时必须加分号:
1 | const str = "pink" |
forEach
forEach用于遍历某个对象,与map的区别在于只遍历不返回
1 | const arr = ['red', 'green', 'blue'] |
forEach适合于遍历数组对象,在复杂的数组遍历过程中更方便
filter
根据筛选条件返回新数组
1 | const arr = [10, 20, 30] |
reduce
累计器,返回累计处理的结果,经常用于求和
1 | // arr.reduce(function(上一次的值, 当前值){}, 起始值) |
reduce的执行过程:
- 如果没有初始值,则上一次值以数组的第一个数组元素的值作为自己的值
- 每一次循环,把返回值给作为下一次循环的上一次值
- 如果有起始值,则起始值作为上一次的值
- 即如果没有起始值,则长度为n的数组,只需要循环n-1次
需要注意处理对象累加的情况:
1 | const Obj = [{ |
数组方法总结
方法 | 作用 | 说明 |
---|---|---|
forEach | 遍历数组 | 不返回数组,经常用于遍历数组元素 |
map | 迭代数组 | 返回新数组,返回处理之后的数组元素构成的新数组 |
filter | 过滤数组 | 返回新数组,返回满足筛选条件的数组元素构成的新数组 |
reduce | 累计器 | 返回累计处理的结果,常用于求和操作 |
对象
创建对象的三种方式
- 利用对象字面量创建对象
1 | const obj = { |
- 利用
new Object
创建对象
1 | const obj = new Object() |
- 构造函数创建
构造函数
一种特殊的函数,用于初始化对象
可以通过构造函数来创建多个类似的对象
1 | function Pig(name, age, gender) { |
构造函数用如下两个约定来约束:
- 命名以大写字母开头
- 只能由new操作符来执行
注意事项:
- 使用new关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数可以省略
- 构造函数内部无需写return,返回值即为新创建的对象
- 构造函数内部的return返回值无效
new的执行过程
- 创建新对象
- 构造函数this指向新对象
- 执行构造函数代码,修改this,添加新属性
- 返回新对象
实例成员与静态成员
- 实列成员即实例化的对象具有的方法或属性
- 静态成员即构造函数具有的方法或属性
- 静态成员只能通过构造函数访问
- 静态方法中的this指向构造函数
1 | function Pig(name, age, gender) { |
内置构造函数
引用数据类型
- Object
Object.keys(obj)
获取对象中所有属性名Object.values(obj)
获取对象中所有属性值Object.assign(newObj, oldObj)
对象拷贝
- Array
- RegExp
- Date
包装数据类型
- String
- Number
- Boolean
数组常见方法
方法 | 作用 | 说明 |
---|---|---|
join | 元素拼接 | 将数组元素拼接为字符串,连接符作为参数传递,返回字符串 |
find | 查找元素 | 返回符合条件的第一个数组元素,如果没有则返回undefined |
every | 全量检测 | 检测数组中是否所有元素都满足条件,通过返回true,否则返回false |
some | 存量检测 | 检测数组中是否存在一个元素满足条件,通过返回true,否则返回false |
concat | 数组合并 | 合并两个数组,返回合并后的新数组 |
sort | 数组排序 | 对原数组元素排序 |
splice | 删除替换 | 删除或替换原数组元素 |
reverse | 反转数组 | 反转原数组 |
findIndex | 查找索引 | 查找元素索引值 |
fill | 数组填充 | 从某位置开始(参数2)到某位置结束(参数3)填充为某值(参数1),返回修改后的数组 |
Array.from() | 伪数变真 | 将伪数组转换为真数组,返回真数组 |
字符串常见方法
方法 | 作用 | 说明 |
---|---|---|
length |
字符串长度 | 与join相反 |
splt('分隔符') |
字符串拆分 | |
substring(开始索引号[, 结束索引号]) |
字符串截取 | substr已被弃用,省略结束索引时,从开始索引一直截取到结束,注意结束索引为开区间 |
startsWith(子字符串[, 开搜索位置]) |
是否以某字符开头,区分大小写 | 开搜位置从0开始,从开始位置匹配,以子字符串开头返回true,否则返回false |
includes(搜索字符串[, 检测位置索引号]) |
是否包含,区分大小写 | 位置索引默认为0,从位置索引开始匹配,匹配到了返回true,否则返回false |
toUpperCase() |
字母转换大写 | |
toLowerCase() |
字母转换小写 | |
indexOf() |
检查是否包含某字符 | |
endsWith() |
是否以某字符串结尾 | 与startsWith 相同 |
replace() |
替换字符串 | 支持正则表达式 |
match() |
查找字符串 | 支持正则表达式 |
数字常见方法
方法 | 作用 | 说明 |
---|---|---|
toFixed(n) |
保留n位小数 | 以四舍五入的保留,不够位数则以0填充,返回值为字符串 |
注意在JS小数运算会出现精度问题,尽量转换为整数后计算
例如
0.1 + 0.2
计算会出现很长的小数位因此使用
(0.1 * 10 + 0.2 * 10) / 10
面向对象
变成思想
- 面向过程
- 分析出解决问题需要的步骤
- 实现每一个步骤
- 依次调用步骤
- 优点
- 性能更高
- 适合于根硬件很紧密的东西
- 缺点
- 不易维护
- 不易复用
- 不易扩展
- 面向对象
- 把事务分解为对象
- 对象之间分工合作
- 优点:
- 灵活
- 代码可复用
- 容易维护和开发
- 缺点
- 性能低
- 特性:
- 抽象
- 封装性
- 继承性
- 多态性
构造函数
JS通过狗咱函数来实现封装
因此:
- 构造函数体现了面向对象的封装特性
- 构造函数创建的对象彼此独立互不影响
1 | function ClassName(attr1, attr2) { |
但是使用这种方式创建对象是,存在内存浪费
原因是虽然obj1和obj2中的method完全一致
但是当我们使用构造函数new两个不同的对象时,依然会为这两个对象中的method分别分配内存
1 | console.log(obj1.method === obj2 === method) // false |
原型对象prototype
因此原型则用于解决这样的浪费内存的问题
构造函数通过原型来分配的函数是所有对象共享的
- 所有构造函数都包含
prototype
属性,指向一个对象,称为原型对象 - 改对象可以挂载函数,对象实例化不会多次创建原型上函数,以节约内存
- 将不变的方法定义在
prototype
对象上,以实现共享 - 构造函数和原型对象中的this都指向实例化对象
1 | function ClassName(attr1, attr2) { |
1 | let that |
例如我们可以使用原型对象为数组添加方法:
1 | Array.prototype.max = function() { |
constructor属性
- 每个原型对象中都有一个constructor属性
- 该属性指向,该原型对象,的构造函数,即指向构造函数本身
需要注意的是如下情况:
当我们要为原型添加多个方法时,会想到直接使用一个对象取添加例如
1 | console.log(Array.constructor) // Array包含Constructor |
对象原型__proto__
对象原型即对象的原型
每个实例对象中都有一个属性__proto__
指向原型对象prototype
之所以实例化对象可以访问构造函数的prototype原型对象中的方法
就是因为对象具有__proto__
的存在
但__proto__
是JS非标准属性,即不同浏览器的名称可能不同
例如谷歌内核的浏览器使用[[prototype]]
来表示__proto__
该属性是只读的
查看时依然使用obj.__proto__
查看
其中也有一个constructor,指向创建该实例对象的构造函数
对象原型VS原型对象
名称 | 表示 | 指向 | construct | this | 说明 |
---|---|---|---|---|---|
原型对象 | prototype |
指向原型对象 | 指向原型对象的构造函数 | 指向实例化对象 | 可以挂载函数,节约内存 |
对象原型 | __proto__ |
指向prototype(同上 | 同上 | 只读 |
原型继承
JS中利用原型对象来实现继承,因此称为原型继承
深浅拷贝
进行拷贝时,由于对象类型的数据是引用数据,因此进行简单的赋值无法为新变量开辟新的内存空间,因此对新对象的修改将会导致原对象也发生改变。
1 | const obj = { |
因此针对引用数据类型出现了浅拷贝和深拷贝
浅拷贝
浅拷贝:只拷贝地址
常见的浅拷贝方法:
- 拷贝对象:
Object.assgin()
或者 展开运算符{…obj}
- 拷贝数组:
Array.prototyope.concat()
或者[…arr]
1 | const obj = { |
以这种方式则是使用{}
先创建了新的对象,因此进行的是将对旧象内的简单数据类型赋值给新对象的同名属性。因此不会出现同步修改的问题。
但如果其中遇到的对象中的某个属性是复杂数据类型时,由于直接将地址赋值给新对象中的同名属性。
因此其中的对象内容仍然会同步修改:
1 | const obj = { |
因此浅拷贝只适合拷贝单层复杂引用类型
深拷贝
深拷贝拷贝对象
常见的实现方法有:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.strngify()实现
递归实现
1 | function deepCopy(newObj, oldObj) { |
做过深拷贝吗,说一下深拷贝是怎么实现的
- 深拷贝需要做到拷贝得到的新对象的修改不会影响旧对象
- 要想实现深拷贝需要用到函数递归
- 当进行普通数据的函数,拷贝是直接赋值即可
- 但遇到数组、对象这类引用数据类型时,需要再次调用递归函数
- 需要注意的是要先处理数组,再处理对象
lodash
使用lodash中的clone
和cloneDeep
函数
1 | let obj = [ |
利用JSON实现深拷贝
1 | let obj = [ |
异常处理
throw
预估代码中可能出现的异常以方便调试
1 | function fn(x, y) { |
- 抛出异常时程序也会终止
- throw后面跟的时错误提示信息
- Error对象配合throw使用,能设置更详细的错误信息
try/catch/finally
try用于捕获浏览器提供的错误信息
1 | function fn(x, y) { |
- try…catch用于捕获错误信息
- 将预估可能发生错误的代码写在try代码块中
- 如果try代码块中出现错误,就会执行catch代码块,并截获错误信息
- finally代码块不管是否有错误都会执行
debugger
debugger
是JS中的一个关键字,即相当于在这个位置下了一个断点:
1 | function deepCopy(newObj, oldObj) { |
this指向
普通函数this
谁调用函数,this就指向谁
1 | // 普通函数 |
需要注意的是,在严格模式下,没有调用者时,this的值为undefined
箭头函数this
- 箭头函数不具备自己的this
- 箭头函数默认将this绑定为外层的this,即最近作用域的this
- 向外层作用域一层一层查找this,直到this有定义
1 | const user = { |
不推荐使用箭头函数的情况
绑定DOM事件
在操作DOM时,使用箭头函数作为回调函数绑定事件,将会导致this无指向错误的问题:
1 | const btn = document.querySelect('.btn') |
基于原型的面向对象
基于原型的面向对象不推荐使用箭头函数
1 | function Person() {} |
因为构造函数和原型对象中的this均指向实例
改变this
call
调用函数的同时为函数指定this
1 | fun.call(thisArg, arg1, arg2, ...) |
thisArg
:fun函数中的this的指arg1, arg2
:fun函数的参数- 返回值就是函数的返回值
apply
调用函数的同时为函数指定this
1 | fun.apply(thisArg, [argsArray]) |
thisArg
:fun函数中的this的指argsArray
:可选参数,fun函数参数,传递的值必须包含在数组中- 返回值就是函数的返回值
- 因此apply主要和数组有关,比如利用
Math.max()
求数组最大值
1 | const max = Math.max.apply(Math, [1, 2, 3]) |
bind
不调用函数,改变函数this的指向
1 | fun.bind(thisArg, arg1, arg2, ...) |
thisArg
:fun函数中的this的指arg1, arg2
:fun函数的参数- 返回由指定的this值和初始化参数改造的原函数拷贝(新函数
- 因此当想要改变this指向,却不想调用函数时使用,比如改变定时器内部的this指向
1 | // 需求,一个按钮,点击则禁用,2秒后开启 |
此处使用箭头函数也可以达到效果,因为箭头函数的this指向与上一层相同,此处的function是放在setTimeout中的普通函数,因此默认由window调用
方法 | 参数形式 | 是否调用 | 使用场景 |
---|---|---|---|
call | arg1,arg2,... |
是 | 需要调用函数改变this且传递参数 |
apply | [arg1,arg2,...] |
是 | 需要调用函数改变this且与数组相关 |
bind | arg1,arg2,... |
否 | 不调用函数改变this,例如定时器内部函数 |
防抖(debounce)
单位时间内,频繁出发事件,只执行最后一次
使用场景:
- 搜索框搜索输入,只需要用户最后一次输入完,在发送请求
- 手机号、邮箱验证输入检测
1 | <body> |
因此有两种方案处理这一问题:
- lodash提供的防抖函数
- 手写防抖函数
lodash
1 | _.debounce(func, [wait = 0], [options=]) |
该函数会从上一次被调用后,延迟wait
毫秒后调用func
方法
1 | <body> |
手写防抖函数
- 声明一个定时器变量
- 当事件发生时先判断是否存在定时器,如果存在则清空定时器
- 如果没有定时器则开启定时器,保存到变量中
- 在定时器中调用要执行的函数
1 | <body> |
节流(throttle)
单位时间内频繁出发事件,只执行一次
使用场景:高频事件
- 鼠标移动mousemove
- 页面尺寸缩放resize
- 滚动条滚动scroll
要求不管鼠标移动多少次,每隔500ms才 + 1
1 | <body> |
实现方法:
- lodash节流函数
- 手写节流函数
lodash.throttle
1 | _.throttle(func, [wait=0], [option=]) |
- 在wait秒内最多执行func一次
- 提供一个
cancel()
方法取消延迟调用 - 提供一个
flush()
方法立即调用 options.leading=true
指定调用在节流开始前options.leading=true
指定调用在节流开始后
1 | <body> |
手写节流函数
核心思想:
- 声明一个定时器变量
- 当事件发生时先判断是否有定时器,如果有则不开启新的定时器
- 如果没有则开启定时器,赋值给定时器变量
- 定时器中调用函数
- 定时器中把定时器清空
1 | <body> |
注意事项:
setTimeout
中无法删除定时器,因为定时器仍然在运作,因此使用timer = null
,而不是clearTimeout(timer)
防抖与节流
性能优化 | 说明 | 使用场景 |
---|---|---|
防抖 | 单位时间内频繁触发事件,只执行最后一次 | 搜索框输入、手机号、邮箱验证 |
节流 | 单位时间内频繁触发事件,只执行一次 | 高频事件:鼠标移动,页面缩放,滚动条滚动 |
Proxy
Proxy是ES6种新增的语法,用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种元编程(对编程语言进行编程)
可以理解为在目标对象之前设置了一层拦截器:
- 外界对该对象的访问都必须先通过这层拦截
- 即可以对外界的访问进行了过滤和改写
下面这段代码使用Proxy修改了一个空对象的访问与设置操作:
1 | var obj = new Proxy({}, { |
因此进行如下操作时会得到额外的结果:
1 | obj.count = 1 |
直观上俩看Proxy
重载了.
运算符
其语法模板为:
1 | let proxy = new Proxy(target, handler); |
target
接收一个对象,即需要拦截的具体对象handler
接收一个对象作为拦截器,用于定义拦截行为
需要注意的是如果希望proxy
起作用就必须对proxy
对象进行操作:
1 | var target = {}; |
一个实用技巧就是将proxy
对象设置为原对象的一个属性:
1 | var object = { proxy: new Proxy(target, handler) }; |
proxy
对象也能作为其他对象的原型对象,且其中的拦截器会被继承:
1 | var proxy = new Proxy({}, { |
拦截器一共支持13种操作的拦截:
-
get(targte, propKey, receiver)
-
拦截对象属性的读取
proxy.attr
或proxy['attr']
-
target
为拦截对象 -
propKey
接收访问的参数Key值 -
receiver
指向原始的读操作所在的那个对象即原本的target.propKey
所指向的值,如果没有则会向target
的原型中查找 -
除了使用上面的创建方式外,还可以使用
get:function(targte, propKey, receiver)
的形式来让get变为执行一个函数 -
此处展示一个使用该方法创建的函数链式调用栈的方法:
-
let pipe = function (value) { // funcStack与Proxy构成闭包 // 即进行链式调用时访问的作用域始终为pipe作用域 let funcStack = []; let oproxy = new Proxy({} , { get : function (pipeObject, fnName) { // 调用get方法则进行计算 if (fnName === 'get') { // 使用reduce函数,将value设为初始值则第一次调用时val=value // 之后val=fn(val)返回的结果 return funcStack.reduce(function (val, fn) { return fn(val); },value); } // 在调用get之前不进行计算,只将调用的函数保存 // 函数通过变量名从window对象种查找 funcStack.push(window[fnName]); // 每次返回一个代理对象,保证代理对象均包含get拦截器 return oproxy; } }); return oproxy; } // 此处函数的作用域需要时全局作用域才能使用window[fnName]访问到 var double = n => n * 2; var pow = n => n * n; var reverseInt = n => n.toString().split("").reverse().join("") | 0; pipe(3).double.pow.reverseInt.get; // 63
-
-
set(target, propKey, value, receiver)
- 阻拦对象的属性设置行为例如
proxy.propKey=v
,proxy['propKey'] = v
- 返回一个bool值
- 阻拦对象的属性设置行为例如
-
has(target, propKey)
- 拦截
propKey in proxy
的操作,返回一个布尔值。
- 拦截
-
deleteProperty(target, propKey)
- 拦截
delete proxy[propKey]
的操作,返回一个布尔值。
- 拦截
-
ownKeys(target)
- 拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环 - 返回一个数组
- 该方法返回目标对象所有自身的属性的属性名,而
Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。
- 拦截
-
getOwnPropertyDescriptor(target, propKey)
- 拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
- 返回属性的描述对象。
- 拦截
-
defineProperty(target, propKey, propDesc)
- 拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
- 返回一个布尔值。
- 拦截
-
preventExtensions(target)
- 拦截
Object.preventExtensions(proxy)
- 返回一个布尔值。
- 拦截
-
getPrototypeOf(target)
- 拦截
Object.getPrototypeOf(proxy)
- 返回一个对象。
- 拦截
-
isExtensible(target)
- 拦截
Object.isExtensible(proxy)
- 返回一个布尔值。
- 拦截
-
setPrototypeOf(target, proto)
- 拦截
Object.setPrototypeOf(proxy, proto)
- 返回一个布尔值。
- 如果目标对象是函数,那么还有两种额外操作可以拦截。
- 拦截
-
apply(target, object, args)
- 拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
- 拦截 Proxy 实例作为函数调用的操作,比如
-
construct(target, args)
- 拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
- 拦截 Proxy 实例作为构造函数调用的操作,比如
Reflect
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API
Reflect
对象的设计目的如下:
- 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上(目前Object和Reflect均包含这些方法 - 修改某些
Object
方法的返回结果,让其返回值更合理。 - 让
Object
操作都变成函数行为。例如delete obj[name]
等价于Reflect.deleteProperty(obj, name)
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。
目前Reflect
提供的方法有如下13中:
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)