ECMAScript2015-2017

大家常说的ES6相对应的版本是ECMAScript2015, 但是由于该版本影响较大,所以ES6也成为往后的版本的一个统称,以下将着重列出2015-2017版本的变动

ECMAScript

ECMAScript2015

let

  • 先来一个例子
// 只会输出一轮
for (var i = 0; i < 3; i++) {
    for (var i = 0; i < 3; i++) {
        console.log(i);
    }
    console.log("内层结束i=" + i);
}
// 结果
// 0
// 1
// 2
// 内层结束i=3
  • 使用let之后 会输出三轮,并且外部的i那里会输出 0 1 2
for (let i = 0; i < 3; i++) {
    for (let i = 0; i < 3; i++) {
        console.log(i);
    }
    console.log("内层结束外部的i=" + i);
}
  • 再来一个例子

var elements = [{}, {}, {}];
for (var i = 0; i < elements.length; i++) {
    elements[i].onclick = function () {
        console.log(i);
    };
}
// 这里不管调用第几个元素的点击事件输出的i都是3
elements[1].onclick(); //

  • 如果想拿到每个元素对应的点击事件,以前我们建立一个闭包就可以解决这个问题
for (var i = 0; i < elements.length; i++) {
    elements[i].onclick = (function (i) {
        return function () {
            console.log(i);
        };
    })(i);
}
elements[1].onclick();
  • 使用了let之后,让i只在块级作用域内生效
for (let i = 0; i < elements.length; i++) {
    elements[i].onclick = function () {
        console.log(i);
    };
}
elements[1].onclick();

  • 再来一个例子就更理解let了
for (let i = 0; i < 3; i++) {
    let i = "foo";
    // 这里会输出3个foo
    console.log(i);
}
  • 以下代码同样会输出3个foo,内部是独立的let,但外部是共用的一个let
let i = 0;
if (i < 3) {
    let i = "foo";
    console.log(i);
}
i++;
if (i < 3) {
    let i = "foo";
    console.log(i);
}
i++;
if (i < 3) {
    let i = "foo";
    console.log(i);
}

案例总结:

  • 案例中循环体中的i是内层独立的作用域

  • 外层是for循环本身的作用域

  • let的声明不会出现提升

  • 声明提前的情况大家应该都知道,下面举个小例子

// 用var不会报错,只会输出一个undefined
console.log(foo); // undefined
var foo = "es6";

// let就会报错, 会报错 Cannot access 'foo' before initialization
console.log(foo);
let foo = "es6";

常量const

  • 如果你把const这么用,那么就会报错了
const name="const"
name = "const声明的重新赋值会报错"

const name
name = 'const声明和赋值必须一起'
  • 接下来这段代码会报错吗???
const obj = {}
obj.name = 'jack'

答案是不会报错! 为什么呢?

  • const声明的成员不能被修改的理解是:

  • 不允许在声明过后重新指向一个新的内存地址,并不是不允许更改内部的成员属性

  • 所以,这里如果给obj重新赋值一个空对象,那么不出意外会报错,因为改变了obj的内存指向

const obj = {}
obj.name = 'jack'
obj = {}

*其他特性就跟let一样了

*建议大家以后在写代码的过程中能做到不用var,默认全部使用const,对于要修改的值才使用let

解构

数组解构
  • 不使用解构的话想要拿到指定位置的值,需要拿对应的下标把值放在一个变量中
const arr = [1, 2, 3]
const a1 = arr[0]
const a2 = arr[1]
const a3 = arr[2]
  • 如果使用了数组解构,内部会根据变量名出现的位置,分配数组中对应的位置
const arr = [1, 2, 3]
const [a1, a2, a3] = arr
  • 如果只想获取其中某个位置的成员(例如第三个),那么需要用 , 作为占位符来对其前面的成员进行占位
const arr = [1, 2, 3]
const [, , a3] = arr
  • …表示从当前位置开始往后的所有成员 注意:这种用法只能在解构位置的最后一个成员上使用
const arr = [1, 2, 3]
const [a1, ...rest] = arr
// rest会拿到[2,3]
  • 解构也支持给提取的成员设置默认值
  • 如果给a2也设置一个值的话,取到的是数组中的值,而不是设置的默认值22
const arr = [1,2,3]
const [a1, a2=22, a3, a4 = 4] = arr
// a2输出数组中第二个成员2, a4输出默认值4,数组中没有第四个成员,所以取默认值
对象解构
  • 对象解构和数组解构不同的是,对象解构是根据属性名匹配的,而不是位置(因为对象没有下标)
    上代码
const obj = { name: 'jack', age: 18 }
const { name,sex } = obj
// 花括号内的变量,也是提取出来的数据所存放的变量名
// 这里解构的name相当于把obj对象中name的值放到了name变量中
// 没有匹配的成员所以,sex会输出undefined
  • 有一种情况需要注意
const obj = { name: 'jack', age: 18 }
const name = 'bob'
const { name } = obj
  • 上面的代码中出现了两个name变量,命名出现了冲突,这种冲突在工作中多半都会发生,我们可以使用重命名的方式来解决冲突
  • 冒号左边还是匹配对象中的属性名,冒号右边则是提取到的值所放入到的变量名(解决命名冲突)
  • 如果还要加默认值,那么在后面再给上一个=去赋值即可
    代码如下
const obj = { name: "jack", age: 18 };
const name = "bob";
const { name: objName = "tom" } = obj;
  • 其他特性和数组解构一致

模板字符串

const str = `我是模板字符串`;
console.log(str);
  • 插值表达式${}
  • 插值表达式内部可以使用任何标准的js语句
const name = "jack";
const str2 = `hello,${name} --- ${1 + 2} --- ${Math.random()}`;
console.log(str2);
  • 模板字符串可以使用标签
  • 标签实际上是一个特殊的函数
console.log`hello world`; // 会输出[hello world]
  • 要使用标签函数就要先定义标签函数
  • 标签函数接收一个数组参数,这个数组参数中的内容是模板字符串中按照插值表达式分割之后的结果
  • 除了第一个参数这个分割数组以外,后面的参数可以依次获取到插值表达式内的值
  • 标签函数的返回值return什么就是什么
const name = "jack";
const gender = true;
function myTagFunc(strings, name, gender) {
    // 因为返回什么就是什么了,所以要将原值返回的话需要内部处理
    return strings[0] + name + strings[1] + gender + strings[2];
}
const result = myTagFunc`hey, ${name} is a ${gender}.`;
// 返回值hey, jack is a true.
console.log(result);

字符串扩展方法

const str = "hello, world";
// 判断字符串是以什么开头
console.log(str.startsWith("h"));
// 判断字符串是以什么结尾
console.log(str.endsWith("d"));
// 判断字符串中是否包含某些字符
console.log(str.includes(","));

参数默认值

  • 正常情况下不传参会打印undefined
  • 加入一个默认值之后不传参会输出默认值
function fn(flag = true) {
    console.log(flag);
}
fn();
// 但是要注意的是,带有默认值的参数必须在最后,否则会被前面传入的参数值给替代
function fn(flag = true, str) {
    console.log(flag);
    console.log(str);
}
fn();

剩余参数

  • 以前获取剩余参数都是使用arguments,但是arguments是一个伪数组
  • es2015使用…变量名可以获取到剩余所有参数
  • 必须写在参数中最后一个且只能写一次
function foo(...args) {
    console.log(arguments);
    console.log(args);
}
foo(1, 2, 3, 4);

展开运算符

  • 和剩余参数的运算符写法一致,但是这里表示的展开数组,拿到数组内所有的值(不能展开对象)
let names = ["jack", "tom"];
console.log(...names);

箭头函数

  • 箭头函数并且没有自己的this,arguments,super或new.target
  • 箭头函数表达式更适用于那些本来需要匿名函数的地方
  • 箭头函数它不能用作构造函数。
function fn(num) {
    return num + 1;
}
const fn = (num) => {
    console.log(num);
    return num;
};
// 如果只有一个表达式可以省略return
const fn = (num) => num + 1;
console.log(fn(5));
  • 极大的简化回调函数的编写
const arr = [1, 2, 3, 4, 5];
console.log(arr.filter((item) => item % 2));
  • 普通写法 这里的this指向的是调用say方法的对象
const person = {
    name: "jack",
    say: function () {
        console.log(`hi,my name is ${this.name}`);
    },
    sayAsync: function () {
        const _this = this;
        // 普通函数的写法要通过闭包的方式来改变this的指向
        setTimeout(function () {
            console.log(_this.name);
        });
    },
};
person.say(); // 输出 hi,my name is jack
person.sayAsync(); // 输出 jack

// 箭头函数写法
const person2 = {
    name: "tom",
    say: () => console.log(`hi,my name is ${this.name}`),
    // this指向当前作用域中的this
    sayAsync: function () {
        setTimeout(() => {
            console.log(this.name);
        });
    },
};

person2.say(); // 输出 hi,my name is undefined'
person2.sayAsync(); // 输出 tom

对象字面量增强

  • 如果变量名和要添加到对象中的属性名一致可以省略 :变量名
  • 对象内添加普通方法也可能直接省略: function
  • 可以使用表达式的返回值做属性名(计算属性名)
    • 计算属性名要用[ ]包裹起来
    • [ ]内部可以使用任意表达式
age = 18;
const obj = {
    age,
    name: "jack",
    method1() {
        console.log(this); // 指向obj
    },
    [Math.Random]: "随机属性",
};
obj.method1();

Object扩展方法

Object.assign 方法
  • 将多个源对象中的属性赋值到一个目标对象中
const source1 = {
    a: 123,
    b: 123,
};
const source2 = {
    c: 123,
};
// 目标对象
const target = {
    a: 456,
    d: 456,
};
// 返回的结果和目标对象一致(会改变目标对象)
const result = Object.assign(target, source1, source2);
console.log(result === target); // true
console.log(target); // { a: 123, d: 456, b: 123, c: 123 }

// 函数内部修改对象的值,将源对象的属性放到一个空对像中,修改不会影响源对象
function fn(obj) {
    const fnObj = Object.assign({}, obj);
    fnObj.name = "fn obj";
    console.log(fnObj); // { name: 'fn obj' }
}
const obj = { name: "jack" };
fn(obj);
console.log(obj); // { name: 'jack' }
Object.js
// 在比较之前会转换类型
console.log(0 == false); // true
// 不会转换类型,严格对值进行比较
console.log(0 === false); // false
// 三等对于数字0正负无法区分
console.log(+0 === -0); // true
// NaN是非数字,有无限种可能,所以是false
console.log(NaN === NaN); // false

// 在es2015中 NaN要完全相等, +0 -0不相等
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true

*大多数情况下还是建议使用=== 严格相等运算符

Proxy

  • es2015出现了Proxy 代理器 Vue3.0中已经使用了proxy实现内部数据响应
  • 代理可以理解为门卫,不管是要进去,还是要出去,都必须要经过他
  • proxy接收两个参数 1.代理的目标对象, 2.代理的处理对象
  • 处理对象中的常用方法
    • get 监视属性访问
      • 参数:代理的属性对象/ 外部访问的属性名
    • set 监视设置属性
      • 参数:代理的属性对象/ 外部访问的属性名/ 要赋的值
const person = {
    name: "jack",
    age: 18,
};

const personProxy = new Proxy(person, {
    get(target, property) {
        // 判断代理目标对象中是否存在要访问的属性
        target[property] ? target[property] : "没有定义";
        return target[property];
    },
    set(target, property, value) {
        // 内部可以做数据校验,检验age是不是整数
        if (property === "age") {
            if (!Number.isInteger(value)) {
                throw new TypeError(`${value} is not an int`);
            }
        }
        target[property] = value;
    },
});
// 通过代理对象写入sex属性和值
personProxy.sex = "man";
personProxy.age = 11;
console.log(personProxy.name); // jack
console.log(personProxy.sex); // man
console.log(personProxy.age); // 11
  • deleteProperty 监控删除对象内某个属性
const proxy = new Proxy(person, {
    deleteProperty(target, property) {
        console.log("delete", property);
        // 内部这里不删除外部操作就算使用delete也是无法删除的
        delete target[property];
    },
});
// 外部操作,内部监控
delete proxy.age;
console.log(proxy); // { name: 'jack', sex: 'man' }
  • proxy对数组的监视
const list = [];
const listProxy = new Proxy(list, {
    set(target, property, value) {
        console.log("set", property, value);
        target[property] = value;
        // 对象不return不会报错,数组监视set不return会报错TypeError:
        return true; // 表示设置成功
    },
});
listProxy.push(100);
// set 0 100
// set length 1

Reflect静态类

const obj = {
    name: "jack",
    age: 18,
};
// console.log(Object.keys(obj));
// console.log("name" in obj);
// console.log(delete obj.age);

console.log(Reflect.ownKeys(obj));
console.log(Reflect.has(obj, "name"));
console.log(Reflect.deleteProperty(obj, "age"));

class 类

  • es2015之前使用构造函数
function Person(name) {
    this.name = name;
}
// 在这个类型实例间共享成员需要通过原型对象property
Person.property.say = function () {
    console.log(`my name is ${this.name}`);
};
let p = new Person("jack");
p.say();
  • 使用class关键词创建一个类
class Person {
    // 构造器
    constructor(name) {
        this.name = name;
    }
    say() {
        console.log(`my name is ${this.name}`);
    }
     // 静态方法
    static create(name) {
        console.log(this); //this指向当前的类型Person
        return new Person(name);
    }
}
let p = new Person("jack");
p.say(); // my name is jack
// 对静态方法的引用
let tom = Person.create("tom");
tom.say(); // my name is tom
继承
  • 关键字extends继承某个父类
  • 关键字 super可以继承来自父类的属性方法
class Student extends Person {
    constructor(name, number) {
        // 继承父类的属性
        super(name);
        // 子类内部重新定义的属性
        this.number = number;
    }
    // 重新定义的方法hello
    hello() {
        // 内部调用父类的方法say
        super.say();
        console.log(`my number is ${this.number}`);
    }
}
let stu = new Student("bob", 20);
console.log(stu); // Student { name: 'bob', number: 20 }
stu.hello(); // my number is 20

数据结构

Set
  • Set是一个类型
  • Set集合中的值是不重复的
const s = new Set();
// add方法会返回集合对象本身
s.add(1).add(2).add(3).add(2).add(4).add(2);
// 遍历获取值
s.forEach((i) => console.log(i));
for (let i of s) {
    console.log(i);
}
// size和length获取长度
console.log(s.size); // 4
// has判断集合中是否存在某个值
console.log(s.has(2)); // true
// delete删除集合中某个值
console.log(s.delete(3)); //true
// clear清除集合中所有值
s.clear();
console.log(s);
  • 使用Set去重 返回值是一个集合,需要做处理才能拿到数组
const arr = [1, 2, 3, 2, 1, 4, 3];
let result = new Set(arr);
let result2 = Array.from(new Set(arr));
let result3 = [...new Set(arr)];
console.log(result); // Set { 1, 2, 3, 4 }
console.log(result2); // [ 1, 2, 3, 4 ]
console.log(result3); // [ 1, 2, 3, 4 ]
Map
  • 在不使用Map之前,如果对象的键不是字符串,则内部会对键值toString变成字符串
const obj = {};
obj[true] = "true";
obj[123] = "123";
obj[{ name: "jack" }] = "value";

// 所以以下的输出结果就是
console.log(Object.keys(obj)); // [ '123', 'true', '[object Object]' ]
// 如果将对象作为键,对象toString之后的值是一样的,那么就无法区分
console.log(obj[{}]); // value
console.log(obj["[object Object]"]); // value
  • 如果使用map则可以避免这一情况的发生
// 创建一个map集合
const m = new Map();
const jack = { name: "jack" };
// 调用set方法存储数据,键可以是任何数据
m.set(jack, 18);
console.log(m); // Map { { name: 'jack' } => 18 }
// 调用get方法获取数据
console.log(m.get(jack)); // 18
// 调用has方法判断某个键是否存在
console.log(m.has(jack)); // true
// forEach方法第一个参数是值, 第二个参数是键
m.forEach((value, key) => {
    console.log(value); // 18
    console.log(key); // { name: 'jack' }
});
// delete删除某个键和其对应的值
console.log(m.delete(jack)); // true
// clear清空整个集合
m.clear();

Symbol

  • 一种全新的原始数据类型
  • 创建一个独一无二的值
const s = Symbol();
console.log(s); // Symbol()
// 通过类型判断可以判断出是一个symbol类型
console.log(typeof s); // symbol
// 每一个Symbol都是独一无二的
console.log(Symbol() === Symbol()); // false
// 可以通过添加字符串来给Symbol添加标识 区分Symbol
console.log(Symbol("jack")); // Symbol(jack)
console.log(Symbol("tom")); // Symbol(tom)
// 即便传入的标识是一样的Symbol函数的结果也是全新的值
console.log(Symbol("bob") === Symbol("bob"));
  • 对象可以使用symbol的值作为属性名
const name = Symbol();
const obj = {
    // 计算属性名的方式
    [name]: "jack",
    say() {
        console.log(this[name]);
    },
};
obj.say();
console.log(obj[name]);
  • 如果要在全局复用一个Symbol的值可以使用for方法
  • 接收一个字符串作为参数,相同的字符串就一定会有相同的结果
const s1 = Symbol.for("foo");
const s2 = Symbol.for("foo");
// 如果传入的不是字符串则方法内部会将传入值转换为字符串
console.log(Symbol.for(true) === Symbol.for("true")); // true
console.log(s1 === s2); // true

// 在Symbol类型中有很多内置的常量,作为内部方法的标识

const o = {
    // Symbol内置的常量toStringTag
    [Symbol.toStringTag]: "XObject",
    [Symbol()]: "symbol value",
    name: "bob",
};
// 对象的toString标签
console.log(o.toString()); // [object XObject]

// 以下遍历都不能拿到Symbol类型的属性名
for (let key in o) {
    console.log(key); // name
}
console.log(Object.keys(o)); // [ 'name' ]
console.log(JSON.stringify(o)); // {"name":"bob"}
// 所以Symbol类型的属性适合作为对象的私有属性

// Object.getOwnPropertySymbols()获取到的是所有Symbol类型的属性名
// Object.keys()获取到的是所有字符串类型的属性名

for…of

  • for…in 适合遍历键值对
  • for 适合遍历普通数组
  • 新的遍历方式for…of 基本用法
const arr = [1, 2, 3, 4];
for (let value of arr) {
    console.log(value);
    if (value > 2) {
        // 可以跳出循环
        break;
    }
}
const s = new Set([1, 2, 3, 4]);
for (let value of s) {
    console.log(value);
}
const m = new Map();
m.set("jack", 123);
m.set("tom", 456);
for (const [key, value] of m) {
    // 遍历map得到的还是一个数组
    // 因为遍历的是一个键值结构,所以拿到是一个数组
    // 可以通过数组结构拿到对应的键值
    console.log(key, value);
    // ["jack", 123]
    // [("tom", 456)];
}

const obj = {
    name: "jack",
    age: 18,
};
for (const item of obj) {
    console.log(item); // TypeError: obj is not iterable
}

为什么不能遍历普通对象呢???

迭代器
  • iterable 可迭代接口
  • 实现了统一接口,可以理解为统一的规格标准,
  • 之所以那些可以被for…of循环的数据类型,是因为内部实现了iterator这个接口
  • for…of循环工作的原理调用next方法就可以实现对内部数据的遍历
const array = [1, 2, 3, 4];
const iterator = array[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
  • 通过以上的了解我们知道了 对象内部没有实现iterator这个接口所以无法遍历
  • 那么我们可以给对象内部加上这个接口
// 可迭代接口对象 Iterable
const obj = {
    arr: [1, 2, 3, 4],
    [Symbol.iterator]: function () {
        let index = 0;
        const self = this;
        // 返回一个 迭代器接口(Iterator) 的对象
        return {
            next: function () {
                const result = {
                    value: self.arr[index],
                    done: index++ >= self.arr.length,
                };
                // 迭代结果接口对象 IterationResult
                return result;
            },
        };
    },
};
for (const item of obj) {
    console.log(item);
    // 1
    // 2
    // 3
    // 4
}

*由此可见对象内部加入了迭代器接口,该对象就成了可迭代对象

  • 迭代器设计模式开发一个任务清单应用
const todoS = {
    life: ["吃饭", "睡觉", "敲代码"],
    work: ["喝茶", "吃水果"],
    // each方法遍历对象内两个数组的值,不使用迭代器时
    each: function (callback) {
        const arr = [...this.life, ...this.work];
        for (const item of arr) {
            callback(item);
        }
    },
    // 迭代器设计模式, 使用迭代器
    [Symbol.iterator]: function () {
        const arr = [...this.life, ...this.work];
        let index = 0;
        // 返回一个 迭代器的对象
        return {
            next: function () {
                const result = {
                    value: arr[index],
                    done: index++ >= arr.length,
                };
                return result;
            },
        };
    },
};
// 调用each方法
todoS.each((item) => {
    console.log(item);
});

// 使用for...of
for (let item of todoS) {
    console.log(item);
}

*迭代器的核心是对外提供统一遍历接口

Generator生成器函数

function* fn() {
    console.log("aaa", 1);
    return 123;
}
const g = fn();
console.log(g, 2); // Object [Generator] {}
// 生成器对象也实现了迭代器接口
console.log(g.next(), 3);
  • 生成器函数要搭配关键词yield
  • 生成器函数会返回一个生成器对象
  • 调用这个对象的next方法才会让这个函数的函数体开始执行
  • 遇到yield关键词函数执行就会暂停下来,yield后面的值会作为next的结果返回
function* foo() {
    console.log(1111);
    yield 100;
    console.log(2222);
    yield 200;
    console.log(3333);
    yield 300;
}
const generator = foo();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
  • 简单实现一个发号器
function* creatIdMaker() {
    let id = 1;
    while (true) {
        yield id++;
    }
}
const inMaker = creatIdMaker();
console.log(inMaker.next().value); // 1
console.log(inMaker.next().value); // 2
console.log(inMaker.next().value); // 3
  • 使用generator函数实现iterator方法
const todoS = {
    life: ["吃饭", "睡觉", "敲代码"],
    work: ["喝茶", "吃水果"],
    // each方法遍历对象内两个数组的值,不使用迭代器时
    each: function (callback) {
        const arr = [...this.life, ...this.work];
        for (const item of arr) {
            callback(item);
        }
    },
    // 使用生成器实现iterator
    [Symbol.iterator]: function* () {
        const arr = [...this.life, ...this.work];
        for (const item of arr) {
            yield item;
        }
    },
};
for (let item of todoS) {
    console.log(item);
}

ECMAScript 2016 新增

includes 判断数组中是否存在指定的值
const arr = ["jack", 18, NaN, false];
// indexOf是查找下标,includes是判断是否存在,并且indexOf不能查找NaN
console.log(arr.indexOf(NaN)); // -1
console.log(arr.includes(NaN)); //true
指数运算符 **
console.log(Math.pow(2, 10)); // 1024
// 和Math.pow作用一样,但是简化了写法
console.log(2 ** 10); // 1024

ECMAScript 2017 新增

Object.values 返回对象中所有值的数组
const obj = {
    name: "jack",
    age: 18,
};
console.log(Object.values(obj)); // [ 'jack', 18 ]
Object.entries 以数组的形式返回对象中的键值对
for (let [key, value] of Object.entries(obj)) {
    console.log(key, value);
}
// 将一个对象转换为Map类型的对象
console.log(new Map(Object.entries(obj)));

const p1 = {
    firstName: "Li",
    lastName: "Wang",
    // 向外界提供一个只读属性fullName
    get fullName() {
        return this.firstName + this.lastName;
    },
};
// assign在赋值的时候将fullName当做一个普通的属性去复制
const p2 = Object.assign({}, p1);
p2.firstName = "zce";
console.log(p2); // { firstName: 'zce', lastName: 'Wang', fullName: 'LiWang' }
使用Object.getOwnPropertyDescriptors 获取对象属性中完整的描述信息
const descriptors = Object.getOwnPropertyDescriptors(p1);
console.log(descriptors);
const _p2 = Object.defineProperties({}, descriptors);
_p2.firstName = "zce";
console.log(_p2); // { firstName: 'zce', lastName: 'Wang', fullName: [Getter] }
字符串填充方法
  • 用给定字符串填充目标字符串开始或结束位置,如果目标字符串超出给定长度,则默认显示完整目标字符串
    • padStart
    • padEnd
const books = {
    html: 5,
    css: 16,
    javascript: 123,
};
for (let [key, value] of Object.entries(books)) {
    console.log(`${key.padEnd(12, "-")}|${value.toString().padStart(3, 0)}`);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值