# var、let 及 const 区别

  • var声明的变量会存在变量提升

    • 变量提升,是指变量和函数声明在编译阶段被 JavaScript 引擎放入内存中(存放到变量环境)。
    • 变量被提升后,会给变量设置默认值 undefined
    • 变量提升会带来一些问题:
      • 变量容易在不被察觉的情况下被覆盖掉
      • 本应销毁的变量没有被销毁
  • ES6 引入let/const 关键字解决变量提升带来的缺陷

    • 两者都可以生成块级作用域
    • 块级作用域就是通过词法环境的栈结构来实现的,
    • 两者的区别:let 声明的变量可以被改变,而使用 const 声明的变量其值是不可以被改变的。
  • JavaScript 引擎如何同时支持变量提升和块级作用域的

    • 变量提升是通过变量环境来实现,
    • 块级作用域就是通过词法环境的栈结构来实现的,
    • 通过这两者的结合,JavaScript 引擎就同时支持变量提升和块级作用域了

# 补充

变量提升所带来的问题

  1. 变量容易在不被察觉的情况下被覆盖掉
var myname = "lin"

function showName() {
	console.log(myname);
	if (0) {
		var myname = "cherry"
	}
	console.log(myname);
}
showName() 
// 输出:
// undefined
// undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 本应销毁的变量没有被销毁
// 比如 for 循环中声明的变量
function foo() {
	for (var i = 0; i < 7; i++) {}
	console.log(i);
}
foo()
1
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
})

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})

1
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

1
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) {}

1
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

1
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

1
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 和 value
  • get:获取成员属性值
  • 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()

1
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

1
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)
1
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() {}
1
2
3
4
5
6

ES6 中模块化语法更加简洁,如果只是输出一个唯一的对象,使用export default即可,代码如下

// 创建 util1.js 文件,内容如
export default {
    a: 100
}

// 创建 index.js 文件,内容如
import obj from './util1.js'
console.log(obj)

1
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()
1
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;
    }
})
1
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;
        }
    })
}
1
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();
1
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 时。
1
2

# set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

handler.set()
// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。
1
2

# apply()

  • apply方法拦截函数的调用、call和apply操作。
  • apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。
1
2

# has()

  • has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
  • has方法可以接受两个参数,分别是目标对象、需查询的属性名。
handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。
1
2

# construct()

construct方法用于拦截new命令,下面是拦截对象的写法。

var handler = {
  construct (target, args, newTarget) {
    return new target(...args);
}}
1
2
3
4

construct方法可以接受三个参数。

  • target:目标对象
  • args:构造函数的参数对象
  • newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p)
handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。
1
2

# deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。
1
2

# defineProperty()

defineProperty方法拦截了Object.defineProperty操作。

handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。
1
2

# getOwnPropertyDescriptor()

getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。

handler.getOwnPropertyDescriptor()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。
1
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) 时。
1
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) 时。
1
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
1
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对象的方法是一一对应的。
上次更新: 2021年10月30日星期六晚上8点03分