复习ES(6-11)语法之ES6中篇
类
类是对象的模版,定义了同一组对象共有的属性和方法
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中可以使用extends
和super
关键字实现对类的继承,其中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的原始数据类型有 undefined
、null
、number
、string
、boolean
、和引用类型Object
。ES6新增了一种新的原始数据类型Symbol
。Symbol
在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比较类似,他们的区别主要有两个:
- WeakSet的成员只能是对象,而不是能是别的类型的值
- 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^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。
Math.pow(2, 53) // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
ES6 引入了Number.MAX_SAFE_INTEGER
和Number.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 obj
和delete 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); //不可删除
}