复习ES(6-11)语法之ES6中篇

发布时间 2023-07-04 23:55:25作者: 小风车吱呀转

类是对象的模版,定义了同一组对象共有的属性和方法

ES5中的类与继承

  • 定义类

ES5其实并没有类的概念,是通过function 构造函数来模拟一个类。在构造函数中,通常会将不变的方法直接定义在prototype对象上,这样所有对象实例就可以共享这些方法,节省内存。

// 类
function People(name, age) {
  this.name = name;
  this.age = age;
}
People.prototype.showName = function () {
  console.log("我的名字是" + this.name);
};
let p1 = new People("zzz", 18);
console.log(p1);
p1.showName();
let p2 = new People("小明", 20);
console.log(p2);
p2.showName();

  • 定义静态属性

静态属性就是直接在定义在类上的属性,使用时直接通过类调用它

// 类
function People(name, age) {
  this.name = name;
  this.age = age;
  People.count++;
}
People.count = 0;
let p1 = new People("zzz", 18);
let p2 = new People("小明", 20);
console.log(People.count) // 2
  • 静态方法

静态方法跟静态属性一样,也是定义在类上,使用时直接通过类调用它。在静态方法中,this指向构造函数,所以无法通过this获取实例属性

People.getCount = function () {
  console.log(this); // 指向构造函数
  console.log("当前共有" + People.count + "个人");
};
People.getCount();
  • 继承

①构造函数继承,可以继承构造函数里面的方法和属性,但是不继承原型链

// 父类
function Animal(name) {
  this.name = name;
}
Animal.prototype.showName = function () {
  console.log("名字是" + this.name);
};

// 子类
function Dog(name, color) {
  Animal.call(this, name); //继承属性
  this.color = color;
}
let d1 = new Dog("wangcai", "white");
console.log(d1); //Dog { name: 'wangcai', color: 'white' }

②原型链继承,只能继承原型链上的方法,无法继承构造函数里面的方法和属性

将子类的prototype对象指向父类的实例化对象,并且当前Dog原型的构造函数需要再指回到Dog上。

// 父类
function Animal(name) {
  this.name = name;
}
Animal.prototype.showName = function () {
  console.log("名字是" + this.name);
};

// 子类
function Dog(name, color) {
  //   Animal.call(this, name); //继承属性
  this.color = color;
}
Dog.prototype = new Animal(); // Animail.prototye
Dog.prototype.constructor = Dog;
let d1 = new Dog("wangcai", "white");
console.log(d1); //Dog {  color: 'white' }
d1.showName(); //名字是undefined

③组合式继承,就是将构造函数继承和原型链继承一起使用,既可以继承构造函数里面的方法和属性,又可以继承原型链上的方法。

// 父类
function Animal(name) {
  this.name = name;
  Animal.count++;
}
Animal.prototype.showName = function () {
  console.log("名字是" + this.name);
};

// 子类
function Dog(name, color) {
  Animal.call(this, name); //继承属性
  this.color = color;
}
Dog.prototype = new Animal(); // Animail.prototye
Dog.prototype.constructor = Dog;
let d1 = new Dog("wangcai", "white");
console.log(d1); //Dog { name: 'wangcai', color: 'white' }
d1.showName(); //名字是wangcai

ES6中的类与继承

  • 类的定义

在ES6中提供了一个关键字Class可以用来声明一个类,在类中有constructor构造方法可以初始化属性,实例方法直接写在class里

class People {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  showName() {
    console.log(this.name);
  }
}
let p1 = new People("zzz", 18);
p1.showName();
  • 继承

在ES6中可以使用extendssuper关键字实现对类的继承,其中super是用来在constructor中继承父类的属性

class Coder extends People {
  constructor(name, age, company) {
    super(name, age); // 继承父类的name和age属性
    this.company = company;
  }
  showCompany() {
    console.log(this.company);
  }
}
let c1 = new Coder("小明", 18, "路人甲公司");
console.log(c1);
c1.showName();
c1.showCompany();
  • 访问器

在类中可以使用 get xxx 或者set xxx创建访问器方法,用来访问/返回值或者设置某一个属性的值。

class Coder extends People {
  constructor(name, age, company) {
    super(name, age); // 继承父类的name和age属性
    this.company = company;
  }
  get sex() {
    return "female";
  }
  showCompany() {
    console.log(this.company);
  }
}
let c1 = new Coder("小明", 18, "路人甲公司");
console.log(c1.sex); // female

如果使用get创建了属性,那么直接修改这个属性的值是无效的,还需要定义set方法来修改,在使用set时需要创建一个额外的属性来存储属性值,否则每次使用set修改属性会一直死循环set方法。

class Coder extends People {
  constructor(name, age, company) {
    super(name, age); // 继承父类的name和age属性
    this.company = company;
    this._sex = -1;
  }
  get sex() {
    return this._sex;
  }
  set sex(val) {
    this._sex = val;
  }
  showCompany() {
    console.log(this.company);
  }
}
let c1 = new Coder("小明", 18, "路人甲公司");
c1.sex = "male";
console.log(c1.sex); // male
  • 静态方法

在ES6中使用static关键字定义静态方法,使用时直接通过类调用,子类可以继承父类的静态方法。

//在父类定义静态方法
class People {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static getCount() {
    return 6;
  }
  showName() {
    console.log(this.name);
  }
}

console.log(People.getCount()); // 6
console.log(Coder.getCount()); // 6
  • 静态属性

ES6明确规定,class内部只能定义静态方法,不能定义静态属性。可以同ES5方式一样通过类名.xxx定义静态属性,因为类本质是构造函数的语法糖

People.count = 9;
console.log(typeof People); // function
console.log(People.count);

新的原始数据类型

ES5的原始数据类型有 undefinednullnumberstringboolean、和引用类型Object。ES6新增了一种新的原始数据类型SymbolSymbol在ES6表示独一无二的意思

  • 声明方式

无描述值,即使创建的2个Symbol都无描述值,它们依然不相等

let s1 = Symbol();
let s2 = Symbol();
console.log(s1); // Symbol()
console.log(s2); // Symbol()
console.log(s1 === s2); // false

//打印描述值
console.log(s1.description); // undefined

有描述值,把字符串作为参数传到Symbol当描述

let s1 = Symbol("s1");
let s2 = Symbol("s2");
console.log(s1); // Symbol(s1)
console.log(s2); // Symbol(s2)
console.log(s1 === s2); //false

//打印描述值
console.log(s1.description); // s1  

③如果传入的参数是一个对象,Symbol会自动调佣对象的toString方法

const obj1 = {
  name: "obj1",
};
const obj2 = {
  name: "obj2",
  toString() {
    return this.name;
  },
};
let s1 = Symbol(obj1);
let s2 = Symbol(obj2);
console.log(s1); // Symbol([object Object])
console.log(s2); // Symbol(obj2)

Symbol.for(),通过这种方式创建的Symbol值,相当于在全局环境中定义了,每次创建时都会在全局查找是否有相同的描述值的Symbol。而Symbol()每次都会创建一个新值。

let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo");
console.log(s1 === s2); // true

function bar(){
    return Symbol.for('bar')
}
const x = bar()
const y = Symbol.for('bar')
console.log(x === y) // true

Symbol.keyFor方法返回一个已经登记的Symbol类型值的key。

let s1 = Symbol("foo");
let s2 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // undefined
console.log(Symbol.keyFor(s2)); // foo
  • 应用场景

①利用Symbol来作为对象的属性名

如果 一个年级里有2个同名的同学,那么不使用Symbol的情况下无法进行区分,前一个同学的信息被后一个同学覆盖。

const grade = {
  李四: { address: "zzz", tel: "111" },
  李四: { address: "yyy", tel: "222" },
};
console.log(grade); // { '李四': { address: 'yyy', tel: '222' } }

利用Symbol来作为对象的属性名,可以进行区分,因为它形成的是独一无二的key,无法被覆盖。

const stu1 = Symbol("李四");
const stu2 = Symbol("李四");

const grade = {
  [stu1]: { address: "zzz", tel: "111" },
  [stu2]: { address: "yyy", tel: "222" },
};
console.log(grade);
console.log(grade[stu1]);
console.log(grade[stu2]);

②定义某个类的私有属性或方法

定义一个登录类,通过Symbol将密码设为类似私有属性的属性,不允许对外访问。

// a文件
const password = Symbol('pwd');
class Login {
  constructor(username, pwd) {
    this.username = username;
    this[password] = pwd;
  }
  checkPassword(pwd) {
    console.log(this[password]);
    return this[password] === pwd;
  }
}

export default Login;

// b文件
const login = new Login("admin", "123");
console.log(login.checkPassword("123"));

login.password        // 无法访问
login[password]       // 无法访问
login["password"]     // 无法访问



for (let key in login) {
  // 只能取到普通属性
  console.log(key); // username
}
for (let key of Object.keys(login)) {
  // 只能取到普通属性
  console.log(key); // username
}
for (let key of Object.getOwnPropertySymbols(login)) {
  // 只能取到Symbol属性
  console.log(key); // Symbol(pwd)
}
for (let key of Reflect.ownKeys(login)) {
  // 既取到普通属性又能取到Symbol属性
  console.log(key); // 输出username和Symbol(pwd)
}

  • 消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量替代。

const shapeType = {
  triangle: Symbol(),
  circle: Symbol(),
};

function getArea(shape) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = 1;
      break;
    case shapeType.circle:
      area = 2;
      break;
  }
  return area;
}
console.log(getArea(shapeType.triangle)); // 1

新的数据结构Set

Set是一组唯一值集合,每个值只能在Set中出现一次,Set可以容纳任何数据类型的值

  • 常用方法

①使用 new 关键字和 Set 构造函数可以创建一个集合:

let s = new Set([1, 2, 3, 2, "2"]);
console.log(s); // Set(4) { 1, 2, 3, '2' }

②add(): 添加元素

s.add("4").add("zzz");

③has(): 查询Set实例是否存在某元素(返回布尔值)

console.log(s.has("zzz")); // true

④delete(): 删除Set实例中某个元素(返回布尔值)

s.delete("zzz");

⑤clear(): 清空Set实例

s.clear();
console.log(s); // Set(0) {}

⑥size: 获取Set实例的元素个数

console.log(s.size)

⑦Set实例转数组

通过扩展运算符或者Array.from可以将Set实例转数组

let s = new Set([1, 2, 3, 4]);
console.log([...s]); // 转换为数组
console.log(Array.from(s)); // 转换为数组
  • 遍历

通过遍历可知,Set这种数据结构,它的key和value都是完全一样的值。

s.forEach((item) => console.log(item));

for (let item of s) {
  console.log(item);
}

for (let item of s.keys()) {
  console.log(item);
}

for (let item of s.values()) {
  console.log(item);
}

for (let item of s.entries()) {
  console.log(item);
}


  • 应用场景

①数组去重

// 数组去重
let arr = [1, 2, 3, 4, 2, 3];
let s = new Set(arr);
console.log(s);

②合并去重

//合并去重
let arr1 = [1, 2, 3, 4];
let arr2 = [2, 3, 4, 5, 6];
let s = new Set([...arr1, ...arr2]);
console.log(s); // Set(6) { 1, 2, 3, 4, 5, 6 }

③交集

//交集
let arr1 = [1, 2, 3, 4];
let arr2 = [2, 3, 4, 5, 6];
let s1 = new Set(arr1);
let s2 = new Set(arr2);
let result = new Set(arr1.filter((item) => s2.has(item)));
console.log(result); // Set(3) { 2, 3, 4 }

④差集

// 差集
let arr1 = [1, 2, 3, 4];
let arr2 = [2, 3, 4, 5, 6];
let s1 = new Set(arr1);
let s2 = new Set(arr2);

let arr3 = new Set(arr1.filter((item) => !s2.has(item)));
let arr4 = new Set(arr2.filter((item) => !s1.has(item)));
console.log([...arr3, ...arr4]); // [ 1, 5, 6 ]
  • WeakSet

WeakSet的使用其实和Set比较类似,他们的区别主要有两个:

  1. WeakSet的成员只能是对象,而不是能是别的类型的值
  2. WeakSet的对象都是弱引用,WeakSet没有size属性,不能遍历
const ws = new WeakSet();
const obj1 = {
  name: "zzz",
};
const obj2 = {
  name: "xxx",
};
ws.add(obj1);
ws.has(obj2);
//ws.delete(obj1);

WeakSet中的对象都是弱引用:WeakSet 中对对象的引用不会被考虑进垃圾回收机制,这些值不属于正式的引用,不会阻止垃圾回收,即只要没有其他的对象引用该对象,则该对象就会被回收,而不管它在不在 WeakSet。

因此, WeakSet 适用于临时存放一组对象,以及存放和对象绑定的信息,只要这些对象在外部消失,那么它在 WeakSet 里面的引用也会自动消失。

Map

Map是ECMAScript 6 的新增特性,是一种新的集合类型,为javascript带来了真正的键/值存储机制。

①Map 对象存有键值对,其中的键可以是任何数据类型。

②Map 对象记得键的原始插入顺序。

③Map 对象具有表示映射大小的属性。

  • 常用方法

①使用 new 关键字和 Map 构造函数创建一个映射

let m1 = new Map();
//如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。
//可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中
let m2 = new Map([["key1", "val1"]]);

②set():添加键/值对

let m = new Map([["key1", "val1"]]);
let obj = {
  name: "zzz",
};
m.set(obj, "es");
console.log(m); // Map(2) { 'key1' => 'val1', { name: 'zzz' } => 'es' }

③get():获取 Map 对象中键的值

console.log(m.get(obj)); // es

③has():查询键存在于 Map 中,返回 布尔值。

console.log(m.has("key1")); // true
console.log(m.has("key2")); // false

④delete():删除一个键/值对

m.delete("key1");
console.log(m); // Map(1) { { name: 'zzz' } => 'es' }

⑤clear():清除这个映射实例中的所有键/值对

console.log(m); // Map(0) {}

⑥size属性:返回 Map 元素的数量。

console.log(m.size) // 0
  • 遍历
let map = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"],
]);
map.forEach((value, key) => console.log(value, key));
for (let [key, value] of map) {
  console.log(key, value);
}
for (let key of map.keys()) {
  console.log(key);
}
for (let value of map.values()) {
  console.log(value);
}
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
  • 应用场景

Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。

使用 Map

①储存的键不是字符串/数字/或者 Symbol 时,选择 Map,因为 Object 并不支持

②储存大量的数据时,选择 Map,因为它占用的内存更小

③需要进行许多新增/删除元素的操作时,选择 Map,因为速度更快

④需要保持插入时的顺序的话,选择 Map,因为 Object 会改变排序

⑤需要迭代/遍历的话,选择 Map,因为它默认是可迭代对象,迭代更为便捷

使用 Object

①只是简单的数据结构时,选择 Object,因为它在数据少的时候占用内存更少,且新建时更为高效

②需要用到 JSON 进行文件传输时,选择 Object,因为 JSON 不默认支持 Map

③需要对多个键值进行运算时,选择 Object,因为句法更为简洁

④需要覆盖原型上的键时,选择 Object

虽然 Map 在很多情况下会比 Object 更为高效,不过 Object 永远是 JS 中最基本的引用类型,它的作用也不仅仅是为了储存键值对。

  • WeakMap

WeakMap是一种弱映射的集合类型,它与Map最大的不同在于,WeakMap的键只能是引用数据类型。

let wm = new WeakMap();
wm.set([1], 2);
wm.set(
  {
    name: "zzz",
  },
  "es"
);
console.log(wm);

WeakMap的常见方法有四个:set()get()delete()has()

WeakMap没有size属性,不仅不能使用clear()也不能进行遍历

WeakMap的key对对象的引用是弱引用,如果没有其他引用引用这个对象,那么垃圾回收机制(GC)可以将这个对象回收。

字符串的扩展

  • 字符的Unicode表示法

在ES6中可以采用\uxxxx的形式表示一个字符,其中xxxx表示字符的 Unicode 码点,码点范围为0000~ffff

例:?的Unicode码点表示是\u20BB7 ,它超出码点范围,会被JS理解成\u20BB+7,由于\u20BB是一个不可打印字符,所以只会显示一个奇怪符号,后面跟着一个7

console.log("\u20BB7"); // ₻7

ES6 对这一点做出了改进,只要将码点放入大括号{},就能正确解读该字符。

console.log("\u{20BB7}"); // ?

有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。

'\z' === 'z'  // true
'\172' === 'z' // true 八进制
'\x7A' === 'z' // true 十六进制
'\u007A' === 'z' // true 
'\u{7A}' === 'z' // true
  • 字符串的遍历器接口

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历。

for (let codePoint of "apple") {
  console.log(codePoint);
}
  • 模板字符串

模板字符串(template string)是增强版的字符串,用反引号 (`) 标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

//不使用模板字符串实现三行字符串需要添加换行符\n
const str1 = "第一行\n" + "第二行\n" + "第三行";
console.log(str1);

//使用模板字符串实现多行字符串
const str = `第一行
第二行
第三行`;
console.log(str);


//不使用模板字符串涉及到数值运算时,会被当成字符串进行拼接,除非添加括号
const a = 10;
const b = 8;
const str2 = "我的年龄是:" + a + b;
const str3 = "我的年龄是:" + (a + b);
console.log(str2); // 我的年龄是:108
console.log(str3); // 我的年龄是:18

//使用模板字符串
const a = 10;
const b = 8;
const str5 = `我的年龄是:${a + b}`;
console.log(str5) // 我的年龄是18

模板字符串甚至还能嵌套。

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;
  • String.fromCodePoint():返回使用指定的 Unicode 编码位置创建的字符串

ES5提供的String.fromCharCode不能识别大于ffff,而ES6提供的String.fromCodePoint()可以识别正确识别。

console.log(String.fromCharCode(0x20bb7)); // ஷ
console.log(String.fromCodePoint(0x20bb7)); // ?
  • String.prototype.includes():返回布尔值,表示是否找到了参数字符串。支持第二个参数,表示开始搜索的位置。
const str = "apple";
console.log(str.includes("pp")); // true
console.log(str.includes("pp", 2)); // false
  • String.prototype.startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。支持第二个参数,表示开始搜索的位置。
console.log(str.startsWith("p")); // false
console.log(str.startsWith("p", 2)); // true
  • String.prototype.endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。支持第二个参数,表示开始搜索的位置。
console.log(str.endsWith("e")); // true
console.log(str.endsWith("a", 1)); // true
  • String.prototype.repeat():返回一个新字符串,表示将原字符串重复n次。
const newStr = str.repeat(3);
console.log(newStr); // appleappleapple

正则的扩展

  • y修饰符

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

const str = "aaa_aa_a";
const reg1 = /a+/g; // 每次匹配剩余的
const reg2 = /a+/y; // 剩余的第一个开始匹配
console.log(reg1.exec(str));
console.log(reg2.exec(str));

console.log(reg1.exec(str)); // aa
console.log(reg2.exec(str)); // null

console.log(reg1.exec(str));
console.log(reg2.exec(str));
  • u修饰符

ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符

const str = "\uD842\uDFB7"; // 表示一个字符
console.log(/^\uD842/.test(str)); // es5 true
console.log(/^\uD842/u.test(str)); // es6 false

点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。

const s = '?';

/^.$/.test(s) // false
/^.$/u.test(s) // true

ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('?') // true

使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。

/?{2}/.test('??') // false
/?{2}/u.test('??') // true

数值的扩展

  • 二进制0B 八进制0O

在ES5中十进制转为二进制,二进制转为十进制可以这样写

// 十进制 -> 二进制
const a = 5; // 101
console.log(a.toString(2));

// 二进制 -> 十进制
const b = 101;
console.log(parseInt(b, 2));

在ES6中用0B表示二进制,0O表示八进制

const a = 0b0101;
console.log(a); // 5

const b = 0o777;
console.log(b); // 511
  • Number.isFinite(),Number.isNaN()

Number.isFinite()判断值是否有限的。如果传入的值不是Number类型的一律认为是false

console.log(Number.isFinite(5)); // true
console.log(Number.isFinite(0.5)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite("zzz")); // false
console.log(Number.isFinite(true)); // false

Number.isNaN()判断值是不是NaN

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("z" / 3)); // true
console.log(Number.isNaN(15)); // false

它们与传统的全局方法isFinite()isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效。

  • Number.parseInt(),Number.parseFloat()

ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。

// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

  • Number.isInteger()

Number.isInteger()用来判断一个数值是否为整数。如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

console.log(Number.isInteger(5)); // true
console.log(Number.isInteger(5.5)); // false
console.log(Number.isInteger(3.00000000000000000002)); // true
  • 0.1+0.2 === 0.3 ???

js使用 Number 类型来表示数字(整数或浮点数),遵循IEEE 754 标准(双精度标准),通过 64 位来表示的一个number类型变量。

对于整数来说,十进制的35会被存储为: 00100011 其代表 2^5 + 2^1 + 20

对于纯小数来说,十进制的0.375会被存储为: 0.011 其代表 1/22 + 1/23 = 1/4 + 1/8 =0.375

对于像0.1这样的数值用二进制表示你就会发现无法整除,最后算下来会是 0.000110011..由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值。

例如:

console.log(0.1000000000000001); // 0.1000000000000001
console.log(0.10000000000000001); // 0.1

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。

  • 安全整数和Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1  // true

ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true
Number.MIN_SAFE_INTEGER === -9007199254740991  // true

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
  • Math新增方法

Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法将其先转为数值。对于空值和无法截取整数的值,返回NaN

Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0

Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN
Math.trunc(undefined) // NaN

Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0;
  • 其他值,返回NaN
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN

如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN

Math.sign('')  // 0
Math.sign(true)  // +1
Math.sign(false)  // 0
Math.sign(null)  // 0
Math.sign('9')  // +1
Math.sign('foo')  // NaN
Math.sign()  // NaN
Math.sign(undefined)  // NaN

Math.cbrt()方法用于计算一个数的立方根。对于非数值,Math.cbrt()方法内部也是先使用Number()方法将其转为数值。

Math.cbrt('8') // 2
Math.cbrt('hello') // NaN

Proxy

Proxy在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy(target, handler);
  • get():get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

下面是对一个数组拦截读取操作,超出索引范围的就返回"error"。

let arr = [7, 8, 9];
arr = new Proxy(arr, {
  get(target, prop) {
    return prop in target ? target[prop] : "error";
  },
});

console.log(arr[1]); // 8
console.log(arr[3]); // error
  • arr():set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

下面是对一个数组拦截设置操作,只能存放number类型的元素。

let arr = [];
arr = new Proxy(arr, {
  set(target, prop, val) {
    if (typeof val === "number") {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  },
});
arr.push(1);
console.log(arr);
  • has():has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

下面是一个定义区间范围的对象,可以通过has判断某个数是否在这个区间范围内。

let range = {
  start: 1,
  end: 5,
};
range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  },
});
console.log(20 in range); // false
console.log(2 in range); // true

注意:has()拦截只对in运算符生效,对for...in循环不生效

  • ownKeys():ownKeys()方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

Object.getOwnPropertyNames()

Object.getOwnPropertySymbols()

Object.keys()

for...in循环

假设在对象里使用下划线,表示当前的属性是一个私有属性,对象遍历属性时拦截私有属性。

let userInfo = {
  username: "zzz",
  role: "admin",
  _password: "123456",
};
userInfo = new Proxy(userInfo, {
  ownKeys(target) {
    return Object.keys(target).filter((item) => !item.startsWith("_"));
  },
});
console.log(Object.getOwnPropertyNames(userInfo)); // [ 'username', 'role' ]
console.log(Object.keys(userInfo)); // [ 'username', 'role' ]
  • apply():用于拦截函数调用以及call、apply的操作

例子:有一个不定参数求和的函数,当它被调用或者进行call和apply操作时,先求和再将值乘以2。

let sum = (...args) => {
  let num = 0;
  args.forEach((item) => {
    num += item;
  });
  return num;
};

sum = new Proxy(sum, {
  apply(target, ctx, agrs) {
    return target(...agrs) * 2;
  },
});
console.log(sum(1, 2)); // 6
console.log(sum.call(null, 1, 2, 3)); // 12
console.log(sum.apply(null, [1, 2, 3])); // 12
  • construct():用于拦截new命令
// construct -> new
let User = class {
    constructor(name) {
        this.name = name
    }
}
User = new Proxy(User, {
    //construct(目标函数,构造函数的参数列表,创建实例时new命令作用的构造函数)
    construct(target, args, newTarget) {
        console.log('construct')
        // construct必须返回一个对象
        return new target(...args)
    }
})
console.log(new User('zzz')) // User {name: "zzz"}

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

① 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。

例如:Object.defineProperty可用Reflect.defineProperty代替

let obj = {};
let newVal = "";
Reflect.defineProperty(obj, "name", {
  get() {
    console.log("get");
    return newVal;
  },
  set(val) {
    console.log("set");
    newVal = val;
  },
});
obj.name = "zzz";

console.log(obj.name);

②修改某些Object方法的返回结果,让其变得更合理。

比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) { // boolean
  // success
} else {
  // failure
}

③ 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

Reflect对象的方法与Proxy对象的方法一一对应

// 拦截第一个字符为下划线的属性名;
let userInfo = {
  username: "zzz",
  role: "admin",
  _password: "123456",
};
userInfo = new Proxy(userInfo, {
  get(target, prop) {
    if (prop.startsWith("_")) {
      throw new Error("不可访问");
    } else {
      // return target[prop]
      return Reflect.get(target, prop);
    }
  },
  set(target, prop, val) {
    if (prop.startsWith("_")) {
      throw new Error("不可访问");
    } else {
      // target[prop] = val
      Reflect.set(target, prop, val);
      return true;
    }
  },
  deleteProperty(target, prop) {
    // 拦截删除
    if (prop.startsWith("_")) {
      throw new Error("不可删除");
    } else {
      // delete target[prop]
      Reflect.deleteProperty(target, prop);
      return true;
    }
  },
  ownKeys(target) {
    // return Object.keys(target).filter((item) => !item.startsWith("_"));
    return Reflect.ownKeys(target).filter((item) => !item.startsWith("_"));
  },
});
console.log(Object.keys(userInfo)); // [ 'username', 'role' ]
try {
  console.log(userInfo._password);
} catch (e) {
  console.log(e.message); // 不可访问
}
try {
  delete userInfo._password;
} catch (e) {
  console.log(e.message); //不可删除
}

参考链接:https://es6.ruanyifeng.com/#docs/number