前端面试(二)ES6系列


整理简化,有的地方添加了自己的理解

来源:面试官:说说var、let、const之间的区别 | web前端面试 - 面试官系列

一、说说var、let、const之间的区别

变量提升

var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined

letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

暂时性死区

var不存在暂时性死区

letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

块级作用域

var不存在块级作用域

letconst存在块级作用域

重复声明

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

修改声明的变量

varlet可以

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允许为函数的参数设置默认值
函数的形参是默认声明的,不能使用letconst再次声明
参数默认值可以与解构赋值的默认值结合起来使用

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

Setes6新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合

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

promiseA+规范:英文原文 网友翻译

这里简单实现一个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

区别:

通过上述代码进行分析,将promiseGeneratorasync/await进行比较:

  • promiseasync/await是专门用于处理异步操作的

-Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…)

  • promise编写代码相比Generatorasync更为复杂化,且可读性也稍差

  • Generatorasync需要与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机制,ES6Module又有什么不一样?

ES6 在语言标准的层面上,实现了Module,即模块功能,完全可以取代CommonJSAMD规范,成为浏览器和服务器通用的模块解决方案

CommonJSAMD 模块,都只能在运行时确定这些东西。比如,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模块化已经深入我们日常项目开发中,像vuereact项目搭建项目,组件化开发处处可见,其也是依赖模块化实现

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
deprecatedeprecated装饰器在控制台显示一条警告,表示该方法将废除

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: 功能废除了

猜你喜欢

转载自blog.csdn.net/weixin_67585820/article/details/123816752