# 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对象的方法是一一对应的。