ES6 总结

概述

笔记内容为参考《JavaScript 高级程序设计 (第4版)》相关内容进行 ES6 部分知识的总结。主要涉及的知识是变量声明、对象解构、函数和对象的扩展内容、集合引用类型的扩展和面向对象编程等。

ES6 学习系列笔记

  1. ES6 总结
  2. Symbol、Map、Set
  3. ES6 中的类(class
  4. 代理与反射
  5. Promise 与异步函数
  6. 迭代器和生成器

1. 变量声明

ES6 新增了两个用于变量声明的关键字 letconst,且这两个关键字压倒性地超越了 var 成为首选。(参考章节 3.3 变量4.2.2 变量声明

1.1 let 声明

let 关键字声明的范围具有块级作用域,且同一个块级作用域中不允许同样一个变量标识符重复声明let 关键字声明的变量本质在作用域中会被提升,但是其具有暂时性死区特性,所以无法表现出被提升的特点(相当于没有提升)。还有使用 let 在全局作用域下声明的变量不会成为 window 对象的属性。

let a = 'a';
/* 1、let 声明具有块级作用域 */
{
    let b = 'b';
    console.log(b); // 'b'
}
// console.log(b); // ReferenceError: b is not defined

/* 2、同一个块级作用域中不允许同样一个变量标识符重复声明。 */
{
    let a = 'insideA';
    // 块级作用域并不会改变搜索流程。
    console.log(a); // 'insideA',
    let c; // 这里使用 var 声明,下面也是重复声明。
    // let c = 'declare again'; // Uncaught SyntaxError: Identifier 'c' has already been declared
}

/* 3、暂时性死区(没有变量提升)。 */
{
    // console.log(d); // ReferenceError: Cannot access 'd' before initialization
    let d = 'temporal dead zone';
}

/* 4、不会成为全局对象的属性 */
console.log(this.a); // undefined

暂时性死区和声明不提前的理解:在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明(所以能够看到它确实也提升了,如图),只不过在此之前不能以任何方式来引用未声明的变量。let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError

ppOJRkn.png

1.2 const 声明

const 声明的变量也叫常量变量const 声明变量的同时必须对其进行初始化,之后在程序中再也不允许修改该变量的值或者引用。尝试修改变量的值或者引用会导致运行时错误。 const 的其他行为与 let 基本相同。

let _i = 1;
// const 声明的 i 无法作为迭代变量使用(需要自增)。
// TypeError: Assignment to constant variable
// for (const i = 1; i < 3; i++) {
//     console.log(i);
// }
// 除非声明的变量不被修改
for (const i = 1; —_i < 3; _i++) {
    console.log(i);
}

// 但是适用与 for-in 和 for-of 遍历结构。
// 每次遍历都只是创建一个新变量,且在循环内部没有去修改。
const user = { uname: 'tkop', uid: 0 };
const users = [user];
for (const key in user) {
    console.log(user[key]);
}
for (const item of users) {
    console.log(item['uname']);
}

如果开发流程并不会因此而受很大的影响,就应该尽可能地多使用 const 声明,除非确实需要一个将来会重新赋值地变量。因此声明变量应该有以下习惯。不使用 var,限制自己只使用 letconst 有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。const 优先,let 次之。使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。在能够预知变量值未来会修改时则使用 let

2. 展开语法(扩展操作符)

展开语法 (Spread syntax,表示为 ...), 可以在函数调用或者数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。(参考 MDN 文档)

// 可以将数组表达式或者 string 在语法层面展开
const arr0 = [...['tkop1', 'tkop2'], 'tkop3', ...'ALL', 4];
console.log(arr0); // [ 'tkop1', 'tkop2', 'tkop3', 'A', 'L', 'L', 4 ]

// 对象字面量也可以展开( ES9 才支持)
const obj0 = { ...{ id: 1 }, ...{ uname: 'tkop', age: 18 } };
console.log(obj0); // { id: 1, uname: 'tkop', age: 18 }

// 展开的包含结构必须能够与展开后的数据重新构建合法的数据结构
const obj1 = { ...{ id: 2 }, ...[1, 2, 3] };
console.log(obj1); // { '0': 1, '1': 2, '2': 3, id: 2 }
const obj2 = { ...[1, 2, 3], ...['tkop1', 'tkop2'] };
console.log(obj2); // { '0': 'tkop1', '1': 'tkop2', '2': 3 }
const obj3 = { ...[1, 2], ...['tkop1', 'tkop2', 'tkop3'] };
console.log(obj3); // { '0': 'tkop1', '1': 'tkop2', '2': 'tkop3' }

// 展开后的数据无法构建为数组
const arr1 = [...{ id: 2 }];
// TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

扩展操作符可以操作的集合引用类型除了数组外,还有 MapSet 类型。具体展开的结果这里不做演示,可以看看相应的引用类型专题。

2.1 数组或对象字面量构造使用展开语法

  • 进行数组拷贝
  • 连接多个数组
  • 进行对象浅拷贝和对象合并
// 数组拷贝,注意是浅拷贝
const a = [[1], [2], [3]];
const b = [...a];
b[0][0] = NaN;
console.log(a); // [ [ NaN ], [ 2 ], [ 3 ] ]

// 代替 concat 进行数组连接
const arr0 = [[1], [2], [3]];
const arr1 = [1, 2, 3];
const arr = [...arr1, ...arr0, 'end'];
arr[3][0] = 'arr0_start';
console.log(arr); // [ 1, 2, 3, [ 'arr0_start' ], [ 2 ], [ 3 ], 'end' ]
console.log(arr0); // [ [ 'arr0_start' ], [ 2 ], [ 3 ] ]

// 进行对象浅拷贝和对象合并
const obj = { friends: ['小红', '小李'] };
const obj0 = { ...{ id: 1 }, ...{ uname: 'tkop', age: 18 }, ...obj };
obj0.friends.push('扬尘');
// { id: 1, uname: 'tkop', age: 18, friends: [ '小红', '小李', '扬尘' ] }
console.log(obj0);
// { friends: [ '小红', '小李', '扬尘' ] }
console.log(obj);

2.2 函数中运用展开语法

扩展语法可以非常简洁地操作和组合集合数据。此外扩展操作符的另一重要应用场景就是对函数参数的操作。其中可以在函数调用时,使用扩展操作符将可迭代对象拆分并迭代返回每个值作为实参传入函数。也可以在函数定义时,将多个元素收集起来并“凝聚”为单个元素。(参考章节 10.6 参数扩展与收集

2.2.1 函数调用时使用展开语法(扩展参数)

ES6 之前,假设我们需要将一个数组中的元素分别作为参数并调用一个函数,就必须依赖函数的 aplly() 方法。因为函数参数不需要传入一个数组,而是要分别传入数组中的元素

// 获取数组元素中的最大值,借助 Math.max() 但是它需要的参数不是一个数组
const values = [1, 5, 0, 1, 4, 6, 0, 5, 2, 8, 8];
console.log(Math.max(values)); // NaN
console.log(Math.max.apply(Math, values)); // 8
console.log(Function.prototype.apply.call(Math.max, Math, values)); // 8

// 或者使用 ES6 之后的反射 API ,但是不使用扩展操作符
console.log(Reflect.apply(Math.max, Math, values));

使用扩展操作符可以极为简洁地实现上述操作,对可迭代对象应用扩展操作符,并将其作为一个参数传入,等价于将可迭代对象拆分(展开)后将每个值单独作为参数传入

const values = [1, 5, 0, 1, 4, 6, 0, 5, 2, 8, 8];
console.log(Math.max(...values)); // 8

这只是一种新的结合使用了扩展操作符的传参形式。所以函数参数的所有特征都不会改变。例如 1、非箭头函数中的 arguments 对象依旧按照函数调用时传入的参数接收每一个值。2、命名参数和默认参数也依旧正常获取函数调用时传递的值。3、因为数组长度已知,所以在使用扩展操作符传参时,并不妨碍在其前面或者后面再传其他的参数值。

const values1 = [5, 7];
const values2 = [5, 7, 8];
function sum(arg1) {
    console.log(arg1, arguments[1]);
    const arr = [...arguments];
    return arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
}
const arrowFn = (arg1, arg2, arg3 = 'sum = ') => arg3 + (arg1 + arg2);

// 对于 arguments 对象而言,它并不知道扩展操作符的存在。
console.log(sum(...values2)); // 5 7;20
console.log(sum(2, ...values2, ...values1, 3)); // 2  5;37

// 扩展操作符用于命名参数和默认参数
console.log(arrowFn(...values1)); // 'sum = 12'
console.log(arrowFn(...values2)); // 20
2.2.2 函数定义时使用展开语法(收集参数)

函数定义时,可以使用扩展操作符将不定个数的独立参数组合为一个数组。有点类似 arguments 对象的构造机制,但是这种收集参数的结果是一个 Array 实例(arguments 对象只是一个类数组)。

function getSum(...rest) {
    console.log(rest); // [ 1, 2, 4 ]
    return rest.reduce((accumulator, currentValue) => accumulator + currentValue);
}
console.log(getSum(1, 2, 4)); // 7

const arrowSum = (initialValue, ...rest) => {
    console.log(rest); // [ 2, 4 ]
    return rest.reduce((accumulator, currentValue) => accumulator + currentValue, initialValue);
};
console.log(arrowSum(8, 2, 4)); // 14

这里充分利用了这门语言的弱类型及参数长度可变的特点。收集参数的前面如果还有其他命名参数,则只会收集其余的参数,没有则会得到空数组(又叫剩余参数 rest

// 写的时候代码编辑器也能检查都语法错误
((initialValue, ...rest, others) => console.log(others))(8, 2, 4);
// SyntaxError: Rest parameter must be last formal parameter

因为收集参数会收集所有后面传进的参数,所以只能把它作为最后一个参数(后面定义的参数都是无效的,因为调用时他们永远接收不到参数)

3 解构赋值

ES6 新增了对象解构语法可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构实现对象属性赋值。(参考章节 8.1.7 对象解构

// 一直敲控制台打印好烦,定义一个打印函数顶一下
const consoleFn = (...args) => args.map(item => console.log(item));

const user = {
    uname: '扬尘',
    age: 18,
    friends: ['小马', '老马'],
};

// 不使用解构初始化两个变量
let userName = user.uname,
    userAge = user.age;
consoleFn(userName, userAge); // '扬尘'  18

// 使用解构在声明变量的同时使用对象属性值对变量进行赋值(不一定与对象属性匹配,可以忽略某些值)
let { uname: userName, friends: userFriends, girlfriend: girlfriend } = user;
// '扬尘' true undefined
consoleFn(userName, userFriends === user.friends, girlfriend);

// 赋值结构使用 ES6 对象属性值简写的形式(需要初始化的变量名和对象属性的键名一样)
// 同时可以定义默认值
let { uname, age, girlfriend = '国家分配', friends = ['扬尘'] } = user;
// '扬尘' true '国家分配'
consoleFn(uname, friends === user.friends, girlfriend);

对象解构的理解:解释器根据左边的声明结构判断需要对右边的数据进行解构操作。解构后,根据左边声明的变量与对象属性的对应关系,使用对象的属性值给变量赋值。如果没有与声明对应的属性且左边结构给定变量默认值,则只进行变量声明而不进行赋值。但是一旦没有在左边结构给定默认值,则一定会进行赋值(此时为 undefined )。

let girlfriend = '国家分配',
    friends = ['扬尘'];
    
 // 这属于重新赋予新的值
({ girlfriend, friends } = user);
consoleFn(friends === user.friends, girlfriend); // true undefined

解构并不要求变量必须在解构表达式中声明。但是,如果是给实现已经声明的变量赋值,则赋值表达式必须包含在一对括号中。

解构在内部会使用函数 ToObject() 把元数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。也意味着根据 ToObject() 的定义,nullundefined 不能被解构,否则会抛出错误。而像 Number、String 等都可以解构,当然像 Array、MapSet 等集合引用类型对象也就更加可以运用解构。

// 基本包装类型的解构
let { length } = 'tkop';
let { constructor: numberC } = 404;
consoleFn(length, numberC === Number); // 4  true

// 数组类型解构(其他集合类型不常用,所以不去举例)
let { 1: index1Arg, length: arrLength } = ['401', '402', '403'];
let [arg0, arg1] = ['401', '402', '403'];
consoleFn(index1Arg, arrLength, arg0); // '402' 3 '401'

// undefined null 类型数据会抛出错误
let { _ } = undefined;
let { _ } = null;
// TypeError: Cannot destructure property '_' of 'undefined' as it is undefined.
// TypeError: Cannot destructure property '_' of 'null' as it is null.

3.1 嵌套解构和部分解构

1、解构对于引用嵌套的属性或者赋值目标没有限制。为此可以通过解构来复制对象。但是对于引用嵌套的属性,相当于将引用赋予目标对象对应的属性,所以相当于浅复制。

const tkop = {
    myName: 'tkop',
    age: 18,
    job: {
        title: 'Software engineer',
    },
};
const tkopCopy = {};
({ myName: tkopCopy.myName, age: tkopCopy.age, job: tkopCopy.job } = tkop);

// { myName: 'tkop', age: 18, job: { title: 'Software engineer' } }
console.log(tkopCopy);

// 浅复制
tkopCopy.job.title = '垃圾分类回收专业人员';
console.log(tkop.job.title); // '垃圾分类回收专业人员'

2、解构赋值可以使用嵌套解构,以匹配对象嵌套的属性但是无论源对象还是目标对象,在其外层属性没有定义的情况下不能使用嵌套解构

// 依旧使用上面的源对象,赋值结构嵌套匹配对象结构
let {
    job: { title: myJobTitle },
} = tkop;
consoleFn(myJobTitle); // 'Software engineer';

// 源对象外层属性为没有定义,无法解构
// TypeError: Cannot read property 'job' of undefined
({
    nonentity: { job: myJobTitle },
} = tkop);

// 目标对象外层属性为没有定义,无法解构
// TypeError: Cannot set property 'title' of undefined
const obj = {};
({
    job: { title: obj.job.title },
} = tkop);

3、多个属性的解构赋值是一个无关的顺序化操作,其实就是多个变量的顺序赋值。所以如果开始的赋值成功,而后面的赋值出错,则整个解构赋值依旧会完成前面的部分。

// 依旧使用上面的源对象,在给第二个变量赋值时出错
let myName, myJobTitle, myAge;
try {
    ({
        myName,
        myJob: { title: myJobTitle },
        age: myAge,
    } = tkop);
} catch (e) {}

// 'tkop' undefined undefined;
consoleFn(myName, myJobTitle, myAge);

3.2 函数参数列表中使用解构赋值

在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用的局部变量。

const consoleFn = (...args) => args.map(item => console.log(item));
const tkop = {
    myName: 'tkop',
    age: 18,
    job: {
        title: 'Software engineer',
    },
};

const arrowFn = (job, { myName, age }, height) => consoleFn(job.title, myName, age);
arrowFn(tkop.job, tkop, 165);
function fn({ myName: name, age }, height) {
    consoleFn(arguments, name, age, height);
}
fn(tkop, 165);

ppx1IiR.png

个人认为这样的编程方式主要有以下场景:

  1. 函数参数中包含一个对象,但是内部操作不需要标识这个对象(函数和该对象部分解耦)。不需要使用对象属性来获取参数,只需要操作变量参数。
  2. 函数在声明时还可以大概展现出对象参数的部分结构。
  3. 函数调用时,无需关注函数内部对对象参数的操作内容。例如需要的是哪些属性,或则和引用类型属性的修改问题。

使用时需要稍微注意一点,这里传递的对象参数已被解构并为参数赋值。以前在函数内部使用对象收集参数,并声明一个覆盖对象达到设置默认值的目的的操作。这是两个不同的概念,可千万别联系起来。

4 函数相关

ES6 对函数扩展的内容并不是很多。主要有箭头函数、new.target、函数尾调用优化和函数参数新的处理方式等内容。有关函数名的内容跳过,函数的扩展参数和剩余参数上面已经探讨了,所以在此也不再重复这些内容。(参考章节 10 函数

4.1 箭头函数

ES6 新增了使用胖箭头( => )语法定义函数表达式的能力。箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为基本相同。两者两大不同点是箭头函数内部没有默认的 arguments 对象及其内部的 this 对象的指向是静态的

1、箭头函数的基本使用。

// 箭头函数在没有形参或者多个形参时的写法。
const arrowFn0 = (arg1, arg2) => {
    return arg1 + arg2;
};
// 箭头函数只有一个形参时可以省略括号。
const arrowFn1 = arg1 => {
    return arg1 + '_arrowFn1';
};
// 函数体只有一行代码且返回值是其运行结果时,可以省略大括号。
const arrowFn2 = arg1 => arg1 + '_arrowFn2';

箭头函数省略大括号时会改变函数的行为。使用大括号说明包含函数体,可以包含多条语句,跟常规函数一样。如果省略大括号则箭头函数后面只能有一个表达式,箭头函数会隐式返回该表达式的值

2、箭头函数内部的 this 是静态的。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象( this 值)。这样只有在函数调用的时候才能确定,即在代码执行过程中可能会变。但是箭头函数中,this 引用的是定义箭头函数的上下文。定义的时候就已经确定,跟调用环境无关,所以是静态的。

const obj = {
    say: () => {
        console.log(this);
    },
};
const obj1 = {};
obj.say(); // Window
obj.say.call(obj1); // Window

箭头函数的这个特点使得它不能作为构造函数使用,也不能使用 supernew.target 。此外,箭头函数也没有 protorype 属性。

3、箭头函数内部没有 arguments 对象。

跟使用 function 关键字定义的标准函数不同,箭头函数内部没有提供 arguments 对象用以访问函数调用时传递的参数。而只能通过定义的命名参数访问传递给函数的参数。

function testArguments() {
    console.log(arguments);
    (function () {
        console.log(arguments);
    })('tkop', 'Tom');
    (() => {
        console.log(arguments);
    })('tkop_arrowFn', 'Tom_arrowFn');
}
testArguments('arguments0', 'arguments1');

ppOWFER.png

4.2 默认参数值相关内容

ES6 以之前,实现默认参数的常用方式是在函数体内判断调用时是否传入参数(值为 undefined 则没有),如果没有则将参数赋值为默认的值。

function fn(name) {
    name = typeof name === 'undefined' ? '某某' : name;
    console.log(name);
}
fn(); // '某某'
fn('我不叫某某'); // '我不叫某某'

1、基本使用。

ES6 支持显式定义默认参数,只需要在定义函数时在其参数列表中使用 “=” 为参数赋默认值即可

const getName = (name = '某某') => console.log(name);
getName(); // '某某'
getName('tkop'); // 'tkop'

2、arguments 对象的值不反映参数的默认值,只反映传给函数的参数(始终以调用传参为准)。

function fn(name = '某某') {
    // 其实相当于在函数最顶部进行如下操作
    // let name = '某某'
    console.log(arguments[0]);
}
fn(); // undefined
fn('tkop'); // 'tkop'

3、默认值可以是动态的值,例如另一个函数的返回值

const getArgs = name => 'name_' + name;
const fn = (defaultAgr = getArgs('tkop')) => console.log(defaultAgr);
fn(); // 'name_tkop'

函数的默认参数只有在函数被调用且未传相应的参数时才会被调用。

4、默认参数作用域与暂时性死区。

函数定义时,参数会被按照定义他们的顺序依次被初始化。可以依照如下示例想象一下这个过程。

// 示例 1
const fn = (age, name = '扬尘', height) => {};

// fn
() => {
    let age,
        name = '扬尘',
        height;
};

所以参数存在于自己的作用域中,也存在暂时性死区的特点。

const fn = (age, name = '扬尘', height) => {
    // 在箭头函数中这样声明不会报错,但是普通函数也会报错
    var height;
    console.log(height); // 165
};
fn(18, 'tkop', 165);

// 这样的函数在定义时就会出现暂时性死区的报错
const fn1 = (age, name = '扬尘', height) => {
    // SyntaxError: Identifier 'age' has already been declared
    let age;
};

看到这会有个疑问,那这样可不可以在示例 1 中的想象内容改为使用 var 来依次声明呢?答案是不能,因为又会与下面的示例相矛盾。所以上面只能视为帮助我们理解的想象,而不能视为内部实现。

// 因为参数是按顺序初始化的,所以后定义的参数默认值可以引用先定义的参数。
const fn = (n0 = '扬尘', n1 = n0) => {
    console.log(n1);
};
fn(); // '扬尘'

// 但是不能反过来使用
const fn1 = (n0 = n1, n1 = '扬尘') => {
    console.log(n0);
};
fn1('tkop'); // 'tkop'

// ReferenceError: Cannot access 'n1' before initialization
fn1();

带参数调用第二个函数时不会报错,因为此时参数 n0 不会使用默认参数,也就不会去访问 n1 所以不会出现暂时性死区。但是如果不带参数则会报错。

4.3 new.target

函数(非箭头函数)始终可以作为构造函数实例化一个新对象,也可以作为普通函数调用。ES6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用,则该属性的属性值为 undefined如果是使用 new 关键字调用,则 new.target 将引用被调用的构造函数

function Constr() {
    console.log(new.target);
}
Constr();
new Constr();

自己在实现 Promise 的过程中刚好就用到,所以感觉有必要学一学。

ppx6ezT.png

4.4 函数尾调用优化

ES6 新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件的情况下可以重用栈帧。这项优化非常适合“尾调用”,即外部函数的返回值是另一个函数的调用(返回值)。

'use strict';

const innerFn = (arg1, agr2) => arg1 + agr2;
const outerFn = (x, y) => innerFn(x, y);
console.log(outerFn(7, 5)); // 12

ES6 之前,执行 outerFn 会将第一个栈帧推到栈上。执行到 outerFn 的返回语句时,计算返回值就必须先执行 innerFn ,同样会将第二个栈帧推到栈上。 innerFn 执行完并返回结果,第二个栈帧弹出。outerFn 接收结果并返回,第一个栈帧弹出。

ES6 之后,在执行 innerFn 的返回语句时,JavaScript 引擎发现求值返回语句前必须执行 innerFn ,但是此时把第一个栈帧弹出栈外也没有问题,因为 innerFn 的返回值也是 outerFn 的返回值,且第一个栈帧里面已经没有第二个栈帧依赖的数据。它会在此时就将第一个栈帧弹出,然后推入第二个栈帧

引擎是在满足尾调用优化条件才会执行优化操作。这个条件就是外部栈帧真的没必要存在了。涉及的条件如下:

  • 代码在严格模式下执行(非严格模式下函数调用中允许使用 f.argumentsf.caller,而他们都会引用外部函数的栈帧)。
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包
'use strict';

// 无优化:没有返回
const innerFn = (arg1, agr2) => arg1 + agr2;
const outerFn0 = (x, y) => {
    innerFn(x, y);
};

// 无优化:没有直接返回
const outerFn1 = (x, y) => innerFn(x, y) + 9;
const outerFn2 = (x, y) => innerFn(x, y).toString();
const outerFn3 = (x, y) => {
    let r = innerFn(x, y);
    return r;
};

// 无优化:尾调用的是一个闭包
const outerFn4 = (x, y) => {
    const innerFn = () => x + y;
    return innerFn();
};

// 有优化
const innerFn0 = arg => arg.toString();
const outerFn5 = (x, y) => {
    return innerFn0(x + y);
};
const outerFn6 = (...rest) => {
    return rest.length === 2
        ? innerFn(...rest)
        : innerFn0(
              rest.reduce((sum, item) => sum + item),
              0
          );
};

尾调优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。虽然可以将递归改写成迭代循环的形式可以解决这个问题。不过也可以保持递归实现,将其重构为满足条件的形式。

'use strict';

// 没有优化的递归
function fib(n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(6));

// 优化:外部函数作为基础框架,内部函数执行递归。
const fib1 = n => recursion(0, 1, n);
function recursion(a, b, n) {
    return n === 0 ? a : recursion(b, a + b, n - 1);
}
console.log(fib1(6));

// 顺便复习一下迭代器的写法(直接返回可迭代对象内容,不依赖栈帧)
function fib2(n) {
    function* fn(n) {
        if (n < 2) {
            yield n;
        } else {
            yield* fn(n - 1);
            yield* fn(n - 2);
        }
    }
    let sum = 0;
    for (let i of fn(n)) {
        sum += i;
    }
    return sum;
}
console.log(fib2(6));

4.5 私有变量

严格来讲,JavaScript 没有私有成员的概念(ES10 类的概念中可以使用 # 来声明私有属性),所有对象属性都公有的。不过倒是有私有变量的概念。任何定义在函数或者块中的变量,都可以认为是私有的。这些变量无法在外部直接访问,但是可以通过闭包(特权方法)访问。私有变量包括函数参数、局部变量和函数内部定义的其他函数

function Fn(arg1, arg2) {
    let privateVariable = 18;
    function privateFn() {
        console.log('私有变量-内部定义的函数');
    }

    // 特权方法
    this.publicMethod = function () {
        console.log(privateVariable);
        privateFn();
    };
}

const f = new Fn('tkop', 165);
f.publicMethod();

示例中一个有四个私有变量,两个参数、内部定义一个变量和一个函数。最后是一个特权方法,由于特权方法是在函数内部定义返回的一个方法,所以产生闭包保存了私有变量,也就可以访问私有变量。

这里私有变量对于每个实例而言都是独一无二的,每次调用构造函数都会重新创建一套变量和方法。但是存在一个问题,必须通过构造函数来实现这种隔离,导致特权方法也会重新创建一边(无法公用)。可以使用静态私有变量实现特权方法,但是这样又不是同一个概念了。

(() => {
    let privateVariable = 18;
    function privateFn() {
        console.log('私有变量-内部定义的函数');
    }
    Fn = function (arg1) {
        privateVariable = arg1;
    };
    Fn.prototype.publicMethod = function () {
        console.log(privateVariable);
        privateFn();
    };
})();

const f = new Fn(17);
const f2 = new Fn(60);
f.publicMethod();

可以看到私有变量变成了静态变量(静态私有变量),可供所有实例使用。同样也是利用了闭包,但是此时 Fn 是一个全局构造函数,特权方法是其原型对象的方法,所有实例公用。

5 对象扩展

ES6 对象的扩展内容有增加类的概念、对象解构、语法增强和新增对象方法。其中类已单独专题学习,对象解构已在前面解构部分包含。所以在此主要学习增强的对象语法和新增的常用方法。(参考章节 8.1 理解对象MDN 文档

5.1 增强的对象语法

ES6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性没有改变现有引擎的行为,但使得处理对象更加方便。

1、属性值简写。

属性值支持简写。在使用某变量为对象添加的属性赋值时,如果属性名和变量名相同,只要使用变量名(省略冒号和属性键)就会自动被解释为 'variableName' : variable

let age = Math.floor(Math.random() * 100);
const p = {
    name: 'tkop',
    age,
};
console.log(p); // { name: 'tkop', age: 16 }

2、可计算属性。

在引入可计算属性之前,如果想要使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法单独添加属性。不能在对象字面量中直接动态命名属性。

const getKeys = () => 'key_' + Math.random().toString().slice(2, 7);
const p = {
    // 动态命名属性(不合语法)
    // getKeys(): 'randomKeys',
};

// 只能这样添加动态命名的属性。
p[getKeys()] = 'randomKeys';
console.log(p); // { key_46844: 'randomKeys' }

引入可计算属性后,就可以在对象字面量中完成动态属性的赋值。中括号包围的对象属性键会被解释为 JavaScript 表达式来求值,而不再是作为字符串来求值。

const getKeys = () => 'key_' + Math.random().toString().slice(2, 7);
const p = {
    [getKeys()]: 'randomKeys',
};

console.log(p); // { key_49187: 'randomKeys' }

3、简写方法名。

以前定义对象方法的方式是通过声明方法名、冒号,然后定义方法引用的函数表达式。ES6 引入简写方法的语法,只需要声明方法名、参数列表和方法体即可。

const methodKeys = 'methodKeys' + Math.random().toString().slice(2, 7);
const p = {
    say() {
        console.log('p_say()');
    },
    // 可计算属性名(方法名)
    [methodKeys]() {
        console.log(methodKeys);
    },
    // 获取函数和设置函数
    _name: '',
    get name() {
        return this._name;
    },
    set name(newValue) {
        this._name = 'tkop_' + newValue;
    },
};

// { say: [Function: say], methodKeys58099: [Function: methodKeys58099] }
console.log(p);
p.say(); // 'p_say()'
p[methodKeys](); // 'methodKeys58099'
p.name = 'p';
console.log(p.name); // 'tkop_p'

5.2 对象新增方法

ES6 对象新增方法和 ES5 之前的方法如下,可自行查看 MDN 文档详细用法和兼容性学习。这里不作详细记录。本想使用表格的形式列出来,但是感觉会更长,所以使用了注释的方式罗列一下。

// 星号越多个人认为越重要(纯个人看法,建议都了解看一遍)。

/*ES5 就已经提供的方法。
 **** Object.create(proto[, propertiesObject]);
 **** Object.getOwnPropertyNames(obj);
 **** Object.keys(obj);
 *** prototypeObj.isPrototypeOf(object);
 *** Object.getPrototypeOf(object);
 *** Object.prototype.hasOwnProperty(prop);
 *** Object.defineProperty(obj, prop, descriptor);
 ** Object.defineProperties(obj, props);
 ** Object.getOwnPropertyDescriptor(obj, prop);
 ** obj.toLocaleString();
 ** obj.toString();
 ** obj.valueOf();
 * Object.freeze(obj);
 * Object.isFrozen(obj);
 * Object.seal(obj);
 * Object.isSealed(obj);
 * Object.preventExtensions(obj);
 * Object.isExtensible(obj);
 * obj.propertyIsEnumerable(prop);
 */

/*ES6 提供的新方法
 **** Object.assign(target, ...sources);
 *** Object.entries(obj);
 *** Object.fromEntries(iterable);
 *** Object.is(value1, value2);
 ** hasOwn(instance, prop);
 ** Object.getOwnPropertySymbols(obj);
 ** Object.values(obj);
 ** Object.getOwnPropertyDescriptors(obj);
 ** Object.setPrototypeOf(obj, prototype);
 */

// 部分使用示范。
class C {
    constructor() {
        //实例成员
        this.name = 'tkop';
        this[Symbol.for('age')] = 18;
        this.instanceSay = function () {};
    }

    // 原型上的方法
    say() {
        console.log('say');
    }
}
let source = new C();

// 不可枚举属性
Object.defineProperty(source, 'id', {
    value: 0,
    enumerable: false,
});

// -> 自身可枚举类型的属性(含 Symbol)
let target = Object.assign({}, source);

// { name: 'tkop', [Symbol('age')]: 18, instanceSay: [Function (anonymous)] }
console.log(target);
Object.defineProperty(target, 'enumerable1', {
    value: 1,
    enumerable: false,
});

Object.defineProperty(target, Symbol.for('symbolEn'), {
    value: 2,
    enumerable: false,
});
target[Symbol.for('symbol')] = 3;

// [ 'name', 'instanceSay', 'enumerable1' ]
// -> 自身非Symbol(含不可枚举)
console.log(Object.getOwnPropertyNames(target));

// [ Symbol(age), Symbol(symbolEn), Symbol(symbol) ]
// -> 自身Symbol(含不可枚举)
console.log(Object.getOwnPropertySymbols(target));

// [ 'name', 'instanceSay' ] -> 自身可枚举非Symbol
console.log(Object.keys(target));

// [ 'tkop', [Function (anonymous)] ] -> 自身可枚举非Symbol
console.log(Object.values(target));

// true -> 自身所有类型
// console.log(Object.hasOwn(target, Symbol.for('symbolEn')));

// [ [ 'name', 'tkop' ], [ 'instanceSay', [Function (anonymous)] ] ]
// -> 自身可枚举非Symbol(fromEntries同理);
console.log(Object.entries(target));

// // -> 自身所有;
console.log(Object.getOwnPropertyDescriptors(target));

6 其他新增内容

ES6 数组和字符串新增的方法部分内容,某些 ES5 方法的参数在 ES6 才支持。但是这里统一归类为 ES5 方法。或者参数的类型不符合预期是在两个版本的表现不一致,具体内容看文档描述

6.1 数组 ES6 方法

这部分内容,个人看了几篇有关 ES6 新增方法的笔记文章。大部分都将 ES5 的方法视为了 ES6 的新增方法了,例如迭代方法。下面罗列的方法该了解的了解(有些直接可以不看),该掌握的掌握。然后可能还需大概了解一下稀疏数组的空槽在某些方法的行为

// 数组有关方法,ES5 部分已经全部掌握,ES6 部分需要加强。

// ES5 提供判断数据是否是数组引用类型的静态方法。
// Array.isArray(value);

// ES6 提供的静态访问器属性 [Symbol.species] 在类继承中已学习。
// Array[Symbol.species]; // [Function: Array]

// with 语句不常用,所以 Symbol 属性 @@unscopable 了解即可。
// Array.prototype[Symbol.unscopables];

// ES6 提供两个创建新数组的静态方法。
// Array.from(arrayLike[, mapFn, thisArg]);
// Array.of();

// 顺序可以大概反映重要程度,当然参考了他们的功能进行了排序。
/* ES5 提供的数组方法(实例方法)✔ --> 改变原数组。
 * concat(value1, value2);
 * push(element0, element1,  … , elementN); ✔
 * pop(); ✔
 * unshift(element0, element1,  … , elementN); ✔
 * shift(); ✔
 * reverse(); ✔
 * sort(compareFn); ✔
 * join(separator);
 *
 * slice(start, end);
 * splice(start, deleteCount, item1, item2, itemN); ✔
 *
 * forEach(callbackFn[, thisArg]);
 * every(callbackFn[, thisArg]);
 * some(callbackFn[, thisArg]);
 * filter(callbackFn[, thisArg]);
 * map(callbackFn[, thisArg]);
 * reduce(callbackFn[, initialValue]);
 * reduceRight(callbackFn[, initialValue]);
 *
 * indexOf(searchElement[, fromIndex]);
 * lastIndexOf(searchElement[, fromIndex]);
 */

/* ES6 数组新增方法(实例方法)
 * Array.prototype[@@iterator](); // 默认迭代器
 * includes(searchElement[, fromIndex]);
 * find(callbackFn[, thisArg]);
 * findIndex(callbackFn[, thisArg]);
 * findLast(callbackFn[, thisArg]);
 * findLastIndex(callbackFn[, thisArg]);
 *
 * entries();
 * copyWithin(target, start[, end]);
 * fill(value[, start, end]);
 * values();
 * keys();
 * at(index);
 * flat(depth);
 * flatMap(callbackFn[, thisArg]);
 */

6.2 字符串 ES6 方法

// ES5 提供的静态方法: String.fromCharCode()。

// ES6 提供的静态方法:String.fromCodePoint()。补充上述 ES5 方法。
//     String.raw(callSite, ...substitutions)下面模板字面量内容涉及

// 顺序可以大概反映重要程度,当然参考了他们的功能进行了排序。
/* ES5 提供的字符串方法(实例方法)
 * slice(beginIndex[, endIndex]);
 * substring(indexStart[, indexEnd]);
 * trim();
 *
 * concat(str2, [, ...strN]);
 * replace(regexp|substr, newSubStr|function);
 * toLowerCase();
 * toUpperCase();
 * toLocaleLowerCase([locale, locale, ...]);
 * toLocaleUpperCase([locale, locale, ...]);
 *
 * indexOf(searchString, position);
 * lastIndexOf(searchValue[, fromIndex]);
 * charAt(index);
 * charCodeAt(index);
 * search(regexp);
 * match(regexp);
 *
 * split([separator[, limit]]);
 * localeCompare(compareString[, locales, options]);
 */

/* ES6 新增字符串方法(实例方法)
 * String.prototype[@@iterator](); // 默认迭代器
 * repeat(count);
 * padEnd(targetLength, padString);
 * padStart(targetLength, padString);
 * trimEnd();
 * trimRight();
 * trimStart();
 * trimLeft();
 * replaceAll(regexp|substr, newSubstr|function);
 *
 * includes(searchString, position);
 * matchAll(regexp);
 * endsWith(searchString[, length]);
 * startsWith(searchString[, position]);
 * at(index);
 * codePointAt(index);
 *
 * normalize();
 */

6.3 模板字面量

ES6 新增了使用模板字面量定义字符串的能力(参考章节 3.4.6 String 类型)。

1、模板字面量。

因为模板字面量保留了换行字符、空格和制表符等,所以模板字面量可以跨行定义字符串。但这些也会造成格式正确的模板字符串看起来可能缩进不当的问题。

// 第三行虽然缩进看起来没有问题
// 但是由于保留前面的制表符,所以真实的字符串如下
const str = `窗前明月光,
疑是地上霜。
             举头望明月,
低头思故乡。`;
console.log(str);
// 窗前明月光,
// 疑是地上霜。
//              举头望明月,
// 低头思故乡。

2、字符串插值。

模板字面量最常用的一个特性是支持字符串插值。语法是通过在模板字面量中使用 ${ expression } 将该表达式的值(都会使用 toString() 强制转型)插入。

let str = `7 + 8 = ${7 + 8}`;
console.log(str); // '7 + 8 = 15'
const interpolation = { toString: () => 'hello world' };
console.log(`I say '${interpolation}'`); // I say 'hello world'

3、模板字面量标签函数。

带标签的模板是模板字面量的一种更高级的形式,它允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组,其余的参数与表达式相关。你可以用标签函数对这些参数执行任何操作,并返回被操作过的字符串(或者,也可返回完全不同的内容,见下面的示例)。用作标签的函数名没有限制。

// 自定义标签函数
function fn(stringArr, ...expressions) {
    console.log(stringArr);
    console.log(expressions);
    return (
        stringArr[0] +
        expressions
            .map((expression, i) => {
                return `${expression}${stringArr[i + 1]}`;
            })
            .join('')
    );
}
const o = {
    name: '扬尘',
    age: 18,
    job: '收破\n烂的',
};
with (o) {
    // 使用自定的标签函数
    console.log(fn`我叫${name},今年${age},是个${job}`);
}
// [ '我叫', ',今年', ',是个', '。' ] // 始终 n + 1 个
// [ '扬尘', 18, '收破烂的' ] // n 个插值
// 我叫扬尘,今年18,是个收破烂的。// 返回的字符串

标签函数的第一个参数并非是一个普通的数组,而是一个带有 raw 属性的可迭代对象,raw 里面保存的是模板字面量经切割后的原始内容。所以我们可以像下面一样使用标签函数。

function tagFn(stringObj, ...expressions) {
    console.log(stringObj.raw);
    stringObj.raw.forEach(element => {
        console.log(element);
    });
    stringObj.forEach(element => {
        console.log(element);
    });
}

const o = {
    name: '扬尘',
    age: 18,
    job: '收破烂的',
};
with (o) {
    tagFn`我叫${name}今年${age},是个${job}\u00A9。`;
}

ES6 中,内置对象 String 新增了一个静态方法 String.raw()它是一个标签函数,允许我们像上述自定义的标签函数一样使用。也允许我们使用函数和方法的形式调用

let codeType = 'Unicode';
let str = `版权符号的${codeType}为\u00A9`;
console.log(str); // '版权符号的Unicode为©'

// '©的Unicode为\u00A9'
console.log(String.raw`©的${codeType}为\u00A9`);


// 正常情况下,你也许不需要将 String.raw() 当作函数调用。
// 但是为了模拟 `t${0}e${1}s${2}t` 你可以这样做:
String.raw({ raw: 'test' }, 0, 1, 2); // 't0e1s2t'
// 注意这个测试,传入一个 string,和一个类似数组的对象
// 下面这个函数和 `foo${2 + 3}bar${'Java' + 'Script'}baz` 是相等的。
String.raw({
  raw: ['foo', 'bar', 'baz']
}, 2 + 3, 'Java' + 'Script'); // 'foo5barJavaScriptbaz'

标签函数 String.raw 返回的是原始的模板字面量内容(例如换行或者 Unicode 字符),而不是被转换后的字符表示。但是实际的换行符则不会被转换为转义序列的形式,类似于模板字面量上写的是 © 就不会转为 \u00A9

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值