# var、let 及 const 区别
用
var
声明的变量会存在变量提升;- 变量提升,是指变量和函数声明在编译阶段被 JavaScript 引擎放入内存中(存放到变量环境)。
- 变量被提升后,会给变量设置默认值
undefined
。 - 变量提升会带来一些问题:
- 变量容易在不被察觉的情况下被覆盖掉
- 本应销毁的变量没有被销毁
ES6 引入
let/const 关键字
解决变量提升带来的缺陷- 两者都可以生成块级作用域。
- 块级作用域就是通过词法环境的栈结构来实现的,
- 两者的区别:
let
声明的变量可以被改变,而使用const
声明的变量其值是不可以被改变的。
JavaScript 引擎如何同时支持变量提升和块级作用域的
- 变量提升是通过变量环境来实现,
- 块级作用域就是通过词法环境的栈结构来实现的,
- 通过这两者的结合,JavaScript 引擎就同时支持变量提升和块级作用域了
# 补充
变量提升所带来的问题
- 变量容易在不被察觉的情况下被覆盖掉
var myname = "lin"
function showName() {
console.log(myname);
if (0) {
var myname = "cherry"
}
console.log(myname);
}
showName()
// 输出:
// undefined
// undefined
2
3
4
5
6
7
8
9
10
11
12
13
- 本应销毁的变量没有被销毁
// 比如 for 循环中声明的变量
function foo() {
for (var i = 0; i < 7; i++) {}
console.log(i);
}
foo()
2
3
4
5
6
- 如果使用 C 语言或者其他的大部分语言实现类似代码,在 for 循环结束之后,i 就已经被销毁了
- 但是在 JavaScript 代码中,i 的值并未被销毁
变量提升的原因
- 之所以会实现变量提升,是因为JavaScript 代码是 先编译,再执行。
- 实际上变量和函数声明在代码里的位置是不会改变的,而在编译阶段被 JavaScript 引擎放入内存中(存放到变量环境)。
注意
- 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
- 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略
什么是暂时性死区
- let 和 const
- 当我们在声明 a 之前如果使用了 a,就会出现报错的情况
- 报错的原因是因为存在暂时性死区,不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。
# 箭头函数
箭头函数是 ES6 中新的函数定义形式,function name(arg1, arg2) {...}
可以使用 (arg1, arg2) => {...}
来定义。示例如下:
// JS 普通函数
var arr = [1, 2, 3]
arr.map(function (item) {
console.log(index)
return item + 1
})
// ES6 箭头函数
const arr = [1, 2, 3]
arr.map((item, index) => {
console.log(index)
return item + 1
})
2
3
4
5
6
7
8
9
10
11
12
13
14
箭头函数存在的意义,第一写起来更加简洁,第二可以解决 ES6 之前函数执行中this
是全局变量的问题,看如下代码
function fn() {
console.log('real', this) // {a: 100} ,该作用域下的 this 的真实的值
var arr = [1, 2, 3]
// 普通 JS
arr.map(function (item) {
console.log('js', this) // window 。普通函数,这里打印出来的是全局变量,令人费解
return item + 1
})
// 箭头函数
arr.map(item => {
console.log('es6', this) // {a: 100} 。箭头函数,这里打印的就是父作用域的 this
return item + 1
})
}
fn.call({a: 100})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Set 和 Map
Set 和 Map 都是 ES6 中新增的数据结构,是对当前 JS 数组和对象这两种重要数据结构的扩展。
- Set 类似于数组,但数组可以允许元素重复,Set 不允许元素重复
- Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型
Set 和 Map 在实际中有着非常重要的应用价值。在学习数据结构与算法的过程中深有体会,比如经常被用作查询和计数。
# Set
Set 实例不允许元素有重复,可以通过以下示例证明。可以通过一个数组初始化一个 Set 实例,或者通过add
添加元素,元素不能重复,重复的会被忽略。
// 例1
const set = new Set([1, 2, 3, 4, 4]);
console.log(set) // Set(4) {1, 2, 3, 4}
// 例2
const set = new Set();
[2, 3, 5, 4, 5, 8, 8].forEach(item => set.add(item));
for (let item of set) {
console.log(item);
}
// 2 3 5 4 8
2
3
4
5
6
7
8
9
10
11
12
Set 实例的属性和方法有
size
:获取元素数量。add(value)
:添加元素,返回 Set 实例本身。delete(value)
:删除元素,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否是 Set 实例的元素。clear()
:清除所有元素,没有返回值。
const s = new Set();
s.add(1).add(2).add(2); // 添加元素
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
s.clear();
console.log(s); // Set(0) {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Set 实例的遍历,可使用如下方法
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。不过由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys()
和values()
返回结果一致。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每个成员。
let set = new Set(['aaa', 'bbb', 'ccc']);
for (let item of set.keys()) {
console.log(item);
}
// aaa
// bbb
// ccc
for (let item of set.values()) {
console.log(item);
}
// aaa
// bbb
// ccc
for (let item of set.entries()) {
console.log(item);
}
// ["aaa", "aaa"]
// ["bbb", "bbb"]
// ["ccc", "ccc"]
set.forEach((value, key) => console.log(key + ' : ' + value))
// aaa : aaa
// bbb : bbb
// ccc : ccc
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Map
Map 的用法和普通对象基本一致,先看一下它能用非字符串或者数字作为 key 的特性。
const map = new Map();
const obj = {p: 'Hello World'};
map.set(obj, 'OK')
map.get(obj) // "OK"
map.has(obj) // true
map.delete(obj) // true
map.has(obj) // false
2
3
4
5
6
7
8
9
10
需要使用new Map()
初始化一个实例,下面代码中set
get
has
delete
顾名即可思义(下文也会演示)。其中,map.set(obj, 'OK')
就是用对象作为的 key (不光可以是对象,任何数据类型都可以),并且后面通过map.get(obj)
正确获取了。
Map 实例的属性和方法如下:
size
:获取成员的数量set
:设置成员 key 和 valueget
:获取成员属性值has
:判断成员是否存在delete
:删除成员clear
:清空所有
const map = new Map();
map.set('aaa', 100);
map.set('bbb', 200);
map.size // 2
map.get('aaa') // 100
map.has('aaa') // true
map.delete('aaa')
map.has('aaa') // false
map.clear()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map 实例的遍历方法有:
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历 Map 的所有成员。
const map = new Map();
map.set('aaa', 100);
map.set('bbb', 200);
for (let key of map.keys()) {
console.log(key);
}
// "aaa"
// "bbb"
for (let value of map.values()) {
console.log(value);
}
// 100
// 200
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// aaa 100
// bbb 200
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// aaa 100
// bbb 200
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 模块化
# 为什么要使用模块化
使用一个技术肯定是有原因的,那么使用模块化可以给我们带来以下好处
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
# 实现模块化的方式
- 立即执行函数 在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
2
3
4
- AMD 和 CMD 用得不多了,这里不做介绍
- CommonJS
- ES Module
# ES Module 与 CommonJS 的区别
ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
- CommonJS 支持动态导入,也就是
require(${path}/xx.js)
,后者目前不支持,但是已有提案 - CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- ES Module 会编译成
import/exports
来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
2
3
4
5
6
ES6 中模块化语法更加简洁,如果只是输出一个唯一的对象,使用export default
即可,代码如下
// 创建 util1.js 文件,内容如
export default {
a: 100
}
// 创建 index.js 文件,内容如
import obj from './util1.js'
console.log(obj)
2
3
4
5
6
7
8
9
如果想要输出许多个对象,就不能用default
了,且import
时候要加{...}
,代码如下
// 创建 util2.js 文件,内容如
export function fn1() {
alert('fn1')
}
export function fn2() {
alert('fn2')
}
// 创建 index.js 文件,内容如
import { fn1, fn2 } from './util2.js'
fn1()
fn2()
2
3
4
5
6
7
8
9
10
11
12
补充&参考:
# Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
在MDN上对于Proxy的解释是:
- Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
Proxy的语法是:
let p = new Proxy(target, handler)
- target是要代理的对象.它可以是JavaScript中的任何合法对象.如: (数组, 对象, 函数等等)
- handler是要自定义操作方法的一个集合.
- p是一个被代理后的新对象,它拥有target的一切属性和方法.只不过其行为和结果是在handler中自定义的.
Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。
- 其中,new Proxy()表示生成一个Proxy实例,
- target参数表示所要拦截的目标对象,
- handler参数也是一个对象,用来定制拦截行为。
# get与set
Proxy支持13种拦截行为(handle),针对解决上一节的问题,简单介绍下其中2种拦截行为,get与set。
# get
get(target, propKey, receiver)
用于拦截/代理某个属性的读取操作,可以接受三个参数:
- target:目标对象
- propKey:属性名
- receiver(可选):proxy 实例本身(严格地说,是操作行为所针对的对象)
# set
set(target, propKey, value, receiver)
用于拦截/代理某个属性的赋值操作,可以接受四个参数:
- target:目标对象
- propKey:属性名
- value:属性值
- receiver(可选):Proxy 实例本身
// 试图想使用代理访问对象时,此时会触发 get 方法。
// 试图将代理的对象进行赋值时,会触发调用 set方法。
const obj = {name:'xiaolu'}
const representtive = new Proxy(obj, {
get: (target, key) =>{
return key in target ? target[key]:"不存在该值“
},
set: (target, key, value)=>{
target[key] = value;
}
})
2
3
4
5
6
7
8
9
10
11
# Proxy 的基本应用
最基本的应用如下:
- 拦截和监视外部对对象的访问
- 校验值 —— 有效的避免指定属性类型错误的发生。
- 日志记录 —— 当访问属性时,可以在 get 和 set 中记录访问日志。
- 定义如何计算属性值 —— 每次访问属性值,都会进行计算属性值。
- 数据的双向绑定(Vue)—— 在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。
# 拦截
在上面介绍中已讲到
# 校验值
// 校验值
function xiaolu(){
let count = 0;
Object.defineProperty(this,'skillLevel',{
get:() => {
return count;
},
set:value => {
if(!Number.isInteger(value)){
throw new TypeError("抛出错误")
}
count = value;
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 访问日志
对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用 Proxy 充当中间件的角色,轻而易举实现日志功能:
let api = {
_apiKey: '123abc456def',
getUsers: function() { /* ... */ },
getUser: function(userId) { /* ... */ },
setUser: function(userId, config) { /* ... */ }
};
function logMethodAsync(timestamp, method) {
setTimeout(function() {
console.log(`${timestamp} - Logging ${method} request asynchronously.`);
}, 0)
}
api = new Proxy(api, {
get: function(target, key, proxy) {
var value = target[key];
return function(...arguments) {
logMethodAsync(new Date(), key);
return Reflect.apply(value, target, arguments);
};
}
});
api.getUsers();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
参考-w3cplus.com (opens new window)
# 数据的双向绑定(Vue)
待完成
【参考以下内容--整理出回答】 ES6 系列之 defineProperty 与 proxy (opens new window) 面试官: 实现双向绑定Proxy比defineproperty优劣如何? (opens new window)
# 补充
那么为什么在 handler ,定义 get 和 set 这两个函数名之后就代理对象上的 get 和 set 操作了呢?
实际上 handler 本身就是 ES6 所新设计的一个对象.它的作用就是用来自定义代理对象的各种可代理操作。它本身一共有 13 中方法,每种方法都可以代理一种操作.其13种方法如下:
# get()
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
handler.get()
// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。
2
# set()
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
handler.set()
// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。
2
# apply()
- apply方法拦截函数的调用、call和apply操作。
- apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。
2
# has()
- has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
- has方法可以接受两个参数,分别是目标对象、需查询的属性名。
handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。
2
# construct()
construct方法用于拦截new命令,下面是拦截对象的写法。
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}}
2
3
4
construct方法可以接受三个参数。
- target:目标对象
- args:构造函数的参数对象
- newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p)
handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。
2
# deleteProperty()
deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。
handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。
2
# defineProperty()
defineProperty方法拦截了Object.defineProperty操作。
handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。
2
# getOwnPropertyDescriptor()
getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。
handler.getOwnPropertyDescriptor()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。
2
# getPrototypeOf()
getPrototypeOf方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
# handler.getPrototypeOf()
在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。
# isExtensible()
isExtensible方法拦截Object.isExtensible操作。
handler.isExtensible()
// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。
2
# ownKeys()
ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in循环
# handler.ownKeys()
在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。
# preventExtensions()
preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true,否则会报错。
# handler.preventExtensions()
在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。
# setPrototypeOf()
setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。
handler.setPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。
2
# Proxy.revocable()
Proxy.revocable方法返回一个可取消的 Proxy 实例。
let target = {};let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
2
3
4
5
6
7
Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。
Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
# Reflect相关
为什么要设计Reflect--Reflect正是 ES6 为了操作对象而提供的新 API。
# 基本特点
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
- 这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
- 将Object对象的一些方法放到Reflect对象上。
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
- 现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
- 修改某些Object方法的返回结果,让其变得更合理。
- 比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。
- 某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
# 静态方法
Reflect对象一共有 13 个静态方法(匹配Proxy的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)
大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。