整理简化,有的地方添加了自己的理解
来源:面试官:说说var、let、const之间的区别 | web前端面试 - 面试官系列
一、说说var、let、const之间的区别
变量提升
var
声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
暂时性死区
var
不存在暂时性死区
let
和const
存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
块级作用域
var
不存在块级作用域
let
和const
存在块级作用域
重复声明
var
允许重复声明变量
let
和const
在同一作用域不允许重复声明变量
修改声明的变量
var
和let
可以
const
声明一个只读的常量。一旦声明,常量的值就不能改变
二、数组新增了哪些扩展?
1. 扩展运算符的应用
ES6通过扩展元素符...
,好比 rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列
2. 构造函数新增的方法
Array.from()
将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map),还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
Array.of()
用于将一组值,转换为数组
Array() // [] 没有参数的时候,返回一个空数组
Array(3) // [, , ,] 当参数只有一个的时候,实际上是指定数组的长度
Array(3, 11, 8) // [3, 11, 8] 参数个数不少于 2 个时,Array()才会返回由参数组成的新数组
3. 实例对象新增的方法
关于数组实例对象新增的方法有如下:
- copyWithin()
- find()、findIndex()
- fill()
- entries(),keys(),values()
- includes()
- flat(),flatMap()
copyWithin()
将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
/*
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
*/
[1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
// [4, 5, 3, 4, 5]
find()、findIndex()
find()
用于找出第一个符合条件的数组成员,findIndex()
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1,这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
fill()
使用给定值,填充一个数组,还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
['a', 'b', 'c'].fill(7) // [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
entries(),keys(),values()
keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
includes()
用于判断数组是否包含给定的值
方法的第二个参数表示搜索的起始位置,默认为0,参数为负数则表示倒数的位置
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
flat(),flatMap()
将数组扁平化处理,返回一个新数组,对原数据没有影响,
flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为1
flatMap()
方法对原数组的每个成员执行一个函数相当于执行Array.prototype.map()
,然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组
flatMap()
方法还可以有第二个参数,用来绑定遍历函数里面的this
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
4.数组的空位
数组的空位指,数组的某一个位置没有任何值
ES6 则是明确将空位转为undefined
,包括Array.from、扩展运算符、copyWithin()、fill()、entries()、keys()、values()、find()和findIndex()
5.排序稳定性
将sort()默认设置为稳定的排序算法
const arr = [
'peach',
'straw',
'apple',
'spork'
];
const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};
arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]
三、对象新增了哪些扩展?
1. 属性的简写
const baz = {foo:foo}
// 等同于
const baz = {foo}
const obj = {
f() {
this.foo = 'bar';
}
};
//注意:简写的对象方法不能用作构造函数,否则会报错
new obj.f() // 报错
2.属性名表达式
ES6 允许字面量定义对象时,将表达式放在括号内
表达式还可以用于定义方法名
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
3.super关键字
this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象
4.扩展运算符的应用
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
//解构赋值是浅拷贝
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2; // 修改obj里面a属性中键值
x.a.b // 2,影响到了结构出来x的值
5.属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性。
-
for...in
:循环遍历对象自身的和继承的可枚举属性(不含Symbol
属性) -
Object.keys(obj)
:返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol
属性)的键名 -
Object.getOwnPropertyNames(obj)
:回一个数组,包含对象自身的所有属性(不含Symbol
属性,但是包括不可枚举属性)的键名 -
Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身的所有Symbol
属性的键名 -
Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol
或字符串,也不管是否可枚举
上述遍历,都遵守同样的属性遍历的次序规则:
- 首先遍历所有数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有 Symbol 键,按照加入时间升序排
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
对象新增的方法
关于对象新增的方法,分别有以下:
- Object.is()
- Object.assign()
- Object.getOwnPropertyDescriptors()
- Object.setPrototypeOf(),Object.getPrototypeOf()
- Object.keys(),Object.values(),Object.entries()
- Object.fromEntries()
Object.js()
严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
Object.assign()
方法用于对象的合并,将源对象source的所有可枚举属性,复制到目标对象target
Object.assign()
方法的第一个参数是目标对象,后面的参数都是源对象
Object.getOwnPropertyDescriptors()
返回指定对象所有自身属性(非继承属性)的描述对象
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
Object.setPrototypeOf()
Object.setPrototypeOf
方法用来设置一个对象的原型对象
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
Object.getPrototypeOf()
用于读取一个对象的原型对象
Object.keys()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组
Object.values()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组
Object.entries()
返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
Object.fromEntries()
用于将一个键值对数组转为对象
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
四、函数新增了哪些扩展?
1.参数
ES6
允许为函数的参数设置默认值
函数的形参是默认声明的,不能使用let
或const
再次声明
参数默认值可以与解构赋值的默认值结合起来使用
2.属性
函数的length属性
length
将返回没有指定默认值的参数个数
-rest
参数也不会计入length
属性- 如果设置了默认值的参数不是尾参数,那么
length
属性也不再计入后面的参数了
(function(...args) {}).length // 0
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
name属性
- 返回该函数的函数名
- 如果将一个具名函数赋值给一个变量,则
name
属性都返回这个具名函数原本的名字 Function
构造函数返回的函数实例,name
属性的值为anonymous
bind
返回的函数,name
属性值会加上bound
前缀
const bar = function baz() {};
bar.name // "baz"
(new Function).name // "anonymous"
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
3.作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域
等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的
下面例子中,y=x
会形成一个单独作用域,x
没有被定义,所以指向全局变量x
let x = 1;
function f(y = x) {
// 等同于 let y = x
let x = 2;
console.log(y);
}
f() // 1
4.严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};
5.箭头函数
注意点:
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
- 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
- 不可以使用yield命令,因此箭头函数不能用作 Generator 函数
五、你是怎么理解ES6新增Set、Map两种数据结构的?
如果要用一句来描述,我们可以说
Set
是一种叫做集合
的数据结构,Map
是一种叫做字典
的数据结构
什么是集合?什么又是字典?
-
集合
是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合 -
字典
是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同
区别?
- 共同点:集合、字典都可以存储不重复的值
- 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
1.Set
Set
是es6
新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合
Set的实例关于增删改查的方法:
-
add()
添加某个值,返回Set
结构本身
当添加实例中已经存在的元素,set
不会进行处理添加 -
delete()
删除某个值,返回一个布尔值,表示删除是否成功 -
has()
返回一个布尔值,判断该值是否为Set
的成员 -
clear()
清除所有成员,没有返回值
遍历
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
作用
- 扩展运算符和Set 结构相结合实现数组或字符串去重
- 实现并集、交集、和差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
2.Map
增删改查
- size 属性
size属性返回 Map 结构的成员总数。 - set()
设置键名key
对应的键值为value
,然后返回整个Map
结构
如果key
已经有值,则键值会被更新,否则就新生成该键
同时返回的是当前Map
对象,可采用链式写法 - get()
get
方法读取key
对应的键值,如果找不到key
,返回undefined
- has()
has
方法返回一个布尔值,表示某个键是否在当前Map
对象之中 - delete()
delete
方法删除某个键,返回true
。如果删除失败,返回false
- clear()
clear
方法清除所有成员,没有返回值
遍历
Map
结构原生提供三个遍历器生成函数和一个遍历方法:
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回所有成员的遍历器
- forEach():遍历 Map 的所有成员
3. WeakSet 和 WeakMap
六、你是怎么理解ES6中 Promise的?使用场景?
介绍
Promise
,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
promise
解决异步操作的优点:
- 链式操作减低了编码难度
- 代码可读性明显增强
用法省略……
Promise构造函数存在以下方法:
- all()
- race()
- allSettled()
Promise.allSettled()
方法接受一组Promise
实例作为参数,包装成一个新的Promise
实例
只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束 - resolve()
- reject()
- try()
当一个函数f不确定是同步或异步,想要用promise处理的时候
使用场景
将图片的加载写成一个Promise
,一旦加载完成,Promise
的状态就发生变化
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
通过链式操作,将多个渲染数据分别给个then
,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题
getInfo().then(res=>{
let { bannerList } = res
//渲染轮播图
console.log(bannerList)
return res
}).then(res=>{
let { storeList } = res
//渲染店铺列表
console.log(storeList)
return res
}).then(res=>{
let { categoryList } = res
console.log(categoryList)
//渲染分类列表
return res
})
通过all()
实现多个请求合并在一起,汇总所有请求结果,只需设置一个loading即可
function initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//数据初始化
initLoad()
通过race
可以设置图片请求超时
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
//img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确的
img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()]).then(function(results){
console.log(results);
}).catch(function(reason){
console.log(reason);
});
手写一个promise
这里简单实现一个promise
const statusMap = {
PENDING: 'pending',
FUlFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class Promise {
constructor(fn) {
this.value = undefined;
this.reason = undefined;
this.status = statusMap.PENDING;
this.onFulfilled = [];//成功的回调
this.onRejected = []; //失败的回调
const resolve = (value) => {
this.value = value;
this.status = statusMap.FUlFILLED;
this.onFulfilled.forEach(fn => fn(value))
}
const reject = (reason) => {
this.reason = reason;
this.status = statusMap.REJECTED;
this.onRejected.forEach(fn => fn(reason))
}
fn(resolve, reject)
}
then(onFulfilled, onRejected) {
if (this.status === statusMap.FUlFILLED) {
typeof (onFulfilled) === 'function' && onFulfilled(this.value)
return;
}
if (this.status === statusMap.REJECTED) {
typeof (REJECTED) === 'function' && REJECTED(this.reason)
return;
}
if (this.status === statusMap.PENDING) {
typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
typeof onRejected === 'function' && this.onRejected.push(onRejected)
}
}
}
七、你是怎么理解ES6中 Generator的?使用场景?
1.介绍
Generator
函数是ES6
提供的一种异步编程解决方案,语法行为与传统函数完全不同
执行Generator
函数会返回一个遍历器对象,可以依次遍历 Generator
函数内部的每一个状态形式上,Generator
函数是一个普通函数,但是有两个特征:
function
关键字与函数名之间有一个星号- 函数体内部使用
yield
表达式,定义不同的内部状态
2.使用
Generator
函数会返回一个遍历器对象,即具有Symbol.iterator属性,并且返回给自己
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
通过yield
关键字可以暂停generator函数返回的遍历器对象的状态
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上述存在三个状态:hello、world、return
通过next
方法才会遍历到下一个内部状态,其运行逻辑如下:
遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式
如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
如果该函数没有return
语句,则返回的对象的value
属性值为undefined
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
done
用来判断是否存在下个状态,value
对应状态值
yield
表达式本身没有返回值,或者说总是返回undefined
通过调用next
方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
正因为Generator
函数返回Iterator
对象,因此我们还可以通过for...of
进行遍历
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
原生对象没有遍历接口,通过Generator
函数为它加上这个接口,就能使用for...of
进行遍历了
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
3.异步解决方案
回顾之前展开异步解决的方案:
- 回调函数
- Promise 对象
- generator 函数
- async/await
区别:
通过上述代码进行分析,将promise
、Generator
、async/await
进行比较:
promise
和async/await
是专门用于处理异步操作的
-Generator
并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…)
-
promise
编写代码相比Generator
、async
更为复杂化,且可读性也稍差 -
Generator
、async
需要与promise
对象搭配处理异步情况 -
async
实质是Generator
的语法糖,相当于会自动执行Generator
函数 -
async
使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案
4.使用场景
Generator
是异步解决的一种方案,最大特点则是将异步操作同步化表达出来
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
包括redux-saga
中间件也充分利用了Generator
特性
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
还能利用Generator
函数,在对象上实现Iterator
接口
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
八、你是怎么理解ES6中Proxy的?使用场景?
1.介绍
-
定义: 用于定义基本操作的自定义行为
-
本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于
元编程(meta programming)
元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译 -
Proxy
亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
2.用法
Proxy
为 构造函数,用来生成Proxy
实例
var proxy = new Proxy(target, handler)
参数:
-
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)) -
handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为
handler解析
关于handler拦截属性,有如下:
get(target,propKey,receiver)
:拦截对象属性的读取
-set(target,propKey,value,receiver)
:拦截对象属性的设置has(target,propKey)
:拦截propKey in proxy
的操作,返回一个布尔值deleteProperty(target,propKey)
:拦截delete proxy[propKey]
的操作,返回一个布尔值ownKeys(target)
:拦截Object.keys(proxy)
、for...in
等循环,返回一个数组getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)
,返回一个布尔值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
实例作为函数调用的操作
-construct(target, args)
:拦截Proxy
实例作为构造函数调用的操作
Reflect
若需要在Proxy内部调用对象的默认行为,建议使用Reflect
,其是ES6中操作对象而提供的新API
基本特点:
- 只要
Proxy
对象具有的代理方法,Reflect
对象全部具有,以静态方法的形式存在 - 修改某些
Object
方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
)
让Object
操作都变成函数行为
取消代理
Proxy.revocable(target, handler);
3.使用场景
Proxy
其功能非常类似于设计模式中的代理模式,常用功能如下:
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
使用Proxy
保障数据类型的准确性
let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("属性只能是number类型");
}
return Reflect.set(target, key, value, proxy);
}
});
numericDataStore.count = "foo"
// Error: 属性只能是number类型
numericDataStore.count = 333
// 赋值成功
声明了一个私有的 apiKey
,便于api
这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey
let api = {
_apiKey: '123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`);
} return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
} return Reflect.get(target, key, value, proxy);
}
});
console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误
还能通过使用Proxy
实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行
observable
函数返回一个原始对象的Proxy
代理,拦截赋值操作,触发充当观察者的各个函数
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
观察者函数都放进Set
集合,当修改obj
的值,在会set
函数中拦截,自动执行Set
所有的观察者
九、你是怎么理解ES6中Module的?使用场景?
1.介绍
模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。
两个基本的特征:外部特征和内部特征
-
外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能
-
内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)
为什么需要模块化
- 代码抽象
- 代码封装
- 代码复用
- 依赖管理
如果没有模块化,我们代码会怎样?
- 变量和方法不容易维护,容易污染全局作用域
- 加载资源的方式通过script标签从上到下。
- 依赖的环境主观逻辑偏重,代码较多就会比较复杂。
- 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃
因此,需要一种将JavaScript程序模块化的机制,如
- CommonJs (典型代表:node.js早期)
- AMD (典型代表:require.js)
- CMD (典型代表:sea.js)
AMD
Asynchronous ModuleDefinition(AMD)
,异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行
代表库为require.js
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
CommonJs
CommonJS
是一套Javascript
模块规范,用于服务端
// a.js
module.exports={ foo , bar}
// b.js
const { foo,bar } = require('./a.js')
其有如下特点:
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块是同步加载的,即只有加载完成,才能执行后面的操作
- 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
require
返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值
既然存在了AMD
以及CommonJs
机制,ES6
的Module
又有什么不一样?
ES6
在语言标准的层面上,实现了Module
,即模块功能,完全可以取代CommonJS
和 AMD
规范,成为浏览器和服务器通用的模块解决方案
CommonJS
和AMD
模块,都只能在运行时确定这些东西。比如,CommonJS
模块就是对象,输入时必须查找对象属性
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6
设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
// ES6模块
import { stat, exists, readFile } from 'fs';
上述代码,只加载3个方法,其他方法不加载,即 ES6 可以在编译时就完成模块加载
由于编译加载,使得静态分析成为可能。包括现在流行的typeScript也是依靠静态分析实现功能
2.使用
ES6
模块内部自动采用了严格模式,这里就不展开严格模式的限制,毕竟这是ES5
之前就已经规定好
模块功能主要由两个命令构成:
- export:用于规定模块的对外接口
- import:用于输入其他模块提供的功能
export
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
或
// 建议使用下面写法,这样能瞬间确定输出了哪些变量
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
输出函数或类
export function multiply(x, y) {
return x * y;
};
通过as
可以进行输出变量的重命名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
import
使用export
命令定义了模块的对外接口以后,其他JS
文件就可以通过import
命令加载这个模块
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
同样如果想要输入变量起别名,通过as
关键字
import { lastName as surname } from './profile.js';
当加载整个模块的时候,需要用到星号*
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// main.js
import * as circle from './circle';
console.log(circle) // {area:area,circumference:circumference}
输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属性
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
a = {}; // Syntax Error : 'a' is read-only;
不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错
import
后面我们常接着from
关键字,from
指定模块文件的位置,可以是相对路径,也可以是绝对路径
import { a } from './a';
如果只有一个模块名,需要有配置文件,告诉引擎模块的位置
import { myMethod } from 'util';
在编译阶段,import会提升到整个模块的头部,首先执行
foo();
import { foo } from 'my_module';
多次重复执行同样的导入,只会执行一次
import 'lodash';
import 'lodash';
上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加载
如果不需要知道变量名或函数就完成加载,就要用到export default
命令,为模块指定默认输出
// export-default.js
export default function () {
console.log('foo');
}
加载该模块的时候,import
命令可以为该函数指定任意名字
// import-default.js
import customName from './export-default';
customName(); // 'foo'
动态加载
允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势
这个新功能允许您将import()
作为函数调用,将其作为参数传递给模块的路径。 它返回一个promise
,它用一个模块对象来实现,让你可以访问该对象的导出
import('/modules/myModule.mjs')
.then((module) => {
// Do something with the module.
});
复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
同理能够搭配as
、*
搭配使用
3.使用场景
如今,ES6
模块化已经深入我们日常项目开发中,像vue
、react
项目搭建项目,组件化开发处处可见,其也是依赖模块化实现
vue组件
<template>
<div class="App">
组件化开发 ---- 模块化
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
react组件
function App() {
return (
<div className="App">
组件化开发 ---- 模块化
</div>
);
}
export default App;
包括完成一些复杂应用的时候,我们也可以拆分成各个模块
十 、你是怎么理解ES6中 Decorator 的?使用场景?
1.介绍
Decorator
,即装饰器,从名字上很容易让我们联想到装饰者模式
简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论。
ES6中Decorator
功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方法
//这里定义一个士兵,这时候他什么装备都没有
class soldier{
}
//定义一个得到 AK 装备的函数,即装饰器
function strong(target){
target.AK = true
}
//使用该装饰器对士兵进行增强
@strong
class soldier{
}
//这时候士兵就有武器了
soldier.AK // true
上述代码虽然简单,但也能够清晰看到了使用Decorator
两大优点:
- 代码可读性变强了,装饰器命名相当于一个注释
- 在不改变原有代码情况下,对原来功能进行扩展
2.使用
Docorator修饰对象为下面两种:
- 类的装饰
- 类属性的装饰
类的装饰
当对类本身进行装饰的时候,能够接受一个参数,即类本身
将装饰器行为进行分解,大家能够有个更深入的了解
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
下面@testable
就是一个装饰器,target
就是传入的类,即MyTestableClass
,实现了为类添加静态属性
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
如果想要传递参数,可以在装饰器外层再封装一层函数
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
类属性的装饰
当对类属性进行装饰的时候,能够接受三个参数:
- 类的原型对象
- 需要装饰的属性名
- 装饰属性名的描述对象
// 首先定义一个readonly装饰器
function readonly(target, name, descriptor){
descriptor.writable = false; // 将可写属性设为false
return descriptor;
}
//使用readonly装饰类的name方法
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
//相当于以下调用
readonly(Person.prototype, 'name', descriptor);
如果一个方法有多个装饰器,就像洋葱一样,先从外到内进入,再由内到外执行
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) =>console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
外层装饰器@dec(1)
先进入,但是内层装饰器@dec(2)
先执行
注意
装饰器不能用于修饰函数,因为函数存在变量声明情况
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
编译阶段,变成下面
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function () {
counter++;
};
意图是执行后counter
等于 1,但是实际上结果是counter
等于 0
3.使用场景
基于Decorator
强大的作用,我们能够完成各种场景的需求,下面简单列举几种:
使用react-redux
的时候,如果写成下面这种形式,既不雅观也很麻烦
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
通过装饰器就变得简洁多了
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
将mixins
,也可以写成装饰器,让使用更为简洁了
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
// 使用
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"
下面再讲讲core-decorators.js
几个常见的装饰器
@antobind
autobind
装饰器使得方法中的this
对象,绑定原始对象
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
@readonly
readonly
装饰器使得属性或方法不可写
import { readonly } from 'core-decorators';
class Meal {
@readonly
entree = 'steak';
}
var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]
@deprecate
deprecate
或deprecated
装饰器在控制台显示一条警告,表示该方法将废除
import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('功能废除了')
facepalmHard() {}
}
let person = new Person();
person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
// DEPRECATION Person#facepalmHard: 功能废除了