for循环中设置循环变量的部分是一个父作用域,循环体部分是一个子作用域
var a = [];
for (var i = 0; i < 10; i++) {
//如果这里用var来定义i 那么全局就只有这一个i
a[i] = function () {
console.log(i);
};
}
//循环体外调用console的时候,全局的i已经是10了
a[6](); // 10
如果使用let
定义i
就可以避免这个问题,因为每次循环定义的i
都是新的作用域
变量提升:变量可以在var定义之前就使用,不过值为undefined
let
定义过一个变量,那么改变量将无视外部影响(调用或修改),减少运行时的报错。var tmp = 123
if(true){
tmp = '123' //referenceError 死区
let tmp
tmp = '123' //123
}
如果在暂时性死区内使用typeof 操作符,则会报错refenerceError。如果单纯typeof一个未定义变量,会返回undefined
禁止重复声明
let禁止在同一块级作用域中重复声明参数
const
const用于定义常量,不允许修改,在定义时就必须赋值。其性质与let相同。在定义基本数据类型时不可变,定义对象的时候,由于储存的是对象的引用,因此对象可以进行修改。如果希望定义的对象也无法修改,可以使用object.freeze(obj)
在ES5中,只有全局作用域与函数作用域,这会有一些问题。
var tmp = new Date();
function f() {
//变量提升导致这里打印了函数内部作用域的tmp = undefined
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
let
生成的块级作用域可以任意嵌套,父层级无法读取子层级的变量,相反可以。
?块级作用域的出现,可以替代立即执行函数表达式。
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
块级作用域中尽量使用函数表达式定义函数,而不是函数声明的方式,因为在浏览器的运行环境中,为了兼容性规定:
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
此时函数声明当做var
定义的undefined
变量,并将其提升到函数作用域与全局作用域的头部,导致报错xxx is not a function
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
function f() { console.log('I am inside!'); }
}
f();// Uncaught TypeError: f is not a function
}());
///等同于
function f() { console.log('I am outside!'); }
(function () {
var f = undefined
if (false) {
function f() { console.log('I am inside!'); }
}
f();// Uncaught TypeError: f is not a function
}());
浏览器:
window
node:global
Web Worker:self
ES2020,引入globalThis
来获取顶层对象
在ES5中 var、function
定义的全局变量会被当做是顶层对象的属性,而使用let、const、class
定义的变量将不会当做顶层对象的属性。
var a = 1;
window.a // 1
let b = 1;
window.b // undefined
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计缺陷,这将导致编译时无法判断一个变量是未声明还是错误输入,因为全局对象可能是顶层对象的属性,而属性随时都可以动态添加。
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
写法的本质上就是格式匹配。
let [a, b, c] = [1, 2, 3];
如果格式匹配不上,结构的变量值为undefined
let [bar, foo] = [1];// foo = undefined
只要数据结构具有Iterator接口,就可以进行解构,否则会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
//以上都报错
解构可以指定默认值,但只有被解构的数组元素严格等于undefined
,默认值才会生效
let [a=1] = [] //a=1
let [a=1] = [null] //a=null
默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [a=1,b=a]=[] // a=1 b=1
let [a=b,b=1]=[] // ReferenceError: b is not defined
与数组不同,对象的解构赋值不是按照位置顺序,而是根据对象属性名称来匹配
let {a,b} = {b:1,a:2}
//等价于
let {a:a,b:b} = {b:1,a:2} //b=1,a=2
当属性名称无法对应到被解构对象中时,解构失败,变量值为undefined
。如果确定要赋值某个属性,也可以通过key:value来解构。
let {c} = {a:1,b:2} // c=undefined
let {a:c} = {a:1,b:2} //c=1
对象的解构赋值也可以用于嵌套结构
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
//这里,第一个loc是变量可以赋值,后面的都只是标识结构
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
对象的解构赋值可以取到继承的属性
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);//定义obj1继承自obj2
const { foo } = obj1;
foo // "bar"
对象结构可以制定默认值,与数组相同,如果默认值要生效,则被解构的对象对应属性必须严格等于undefined
注意,如果将已声明的变量用于结构复制,避免将
{}
用于行首,因为 JavaScript 引擎会将{}
理解成一个代码块,从而发生语法错误。
let x;
{x} = {x: 1}; // SyntaxError: Unexpected token '='
({x} = {x: 1}); //x=1
只要等号右边的值不是对象或数组,就先将其转为对象
//string
const [a, b, c, d, e] = 'hello';
let {length : len} = 'hello'; //去了类数组的length对象
len // 5
//number
let {toString: s} = 123;
s === Number.prototype.toString // true
//boolean
let {toString: s} = true;
s === Boolean.prototype.toString // true
// undefined与null无法转化为对象
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
//这样的写法是为x和y定义初始值,并设置参数初始值为{} 如果没有传入,就会按初始处理
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
====================
//这种写法并没有初始化xy的值,而是设置一个初始参数来对xy进行解构赋值
//如果传入的参数不包含xy,则无法解构对应参数,返回undefined
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
${}
0xFFFF
的数值for ... of
String.fromCodePoint()
用于返回Unicode码点对应的字符。对应ES5String.fromCharCode()
。能够识别超过0xFFFF
的数值。String.raw()
处理模板字符串,获取原始字符串。占位符${}
中的内容会被计算出来,但是转义符```不会生效,而是原样输出。// 一般用法
String.raw`hello\nw${0}rld` //hello\nworld
// 也可以当做函数使用,第一个参数为占位符分割的字符串,剩余参数为内嵌表达式的值
String.raw({raw:['hello\nw','rld']},0)
codePointAt()
(实例方法)用于返回Unicode 编码点值的非负整数。能能够正确识别4字节储存的字符。参数是字符在字符串中的位置(从 0 开始)。let s = '';
s.charCodeAt(0) // 55362 只能返回前两个字节对应数值
s.codePointAt(0) // 134071 正确返回完整数值
JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于Unicode 码点大于
0xFFFF
的字符需要4个字节来储存
normalize()
(实例方法)规范字符串编码includes()
(实例方法)判断是否包含字符串,传入两个参数,查找的字符串和开始的位置。返回布尔值startsWith()
(实例方法)判断字符串开头,传入两个参数,查找的字符串和开始的位置。返回布尔值endsWith()
(实例方法)判断字符串结尾,传入两个参数,查找的字符串和前几个字符。返回布尔值repeat()
(实例方法),返回重复n次的字符串,n参数,n>-1。padStart()
用于头部补全,padEnd()
用于尾部补全(实例方法),接受两个参数,第一个是补全后的最大长度,第二个是补全的内容。// 补全字符串到指定位数
'12'.padStart(10, '0') // "0000000012"
// 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
trimStart()
消除字符串头部的空格,trimEnd()
消除尾部的空格。返回新字符串。replaceAll()
一次替换所有匹配的字符。返回新字符串'aabbcc'.replace('b', '_') // aa_bcc
'aabbcc'.replaceAll('b', '_') // aa__cc
at()
接受一个整数参数,返回指定位置的字符,支持负值(倒数)数字中可以通过_
来对位数进行分隔。不能放在开头或末尾,对于 JavaScript 内部数值的存储和输出,并没有影响。
12345_00 === 123_4500 // true
Number.isFinite()
用来检查一个数值是否有限Number.isNaN()
用来检查值是否为NaNNumber.parseInt()
将参数转化为整数Number.parseFloat()
将参数转化为浮点数Number.isInteger()
判断一个数是否为整数,小数点较多超出16位时,会误判。Number.EPSILON
代表1 与大于 1 的最小浮点数之间的差,用于标识JavaScript 能够表示的最小精度。Number.isSafeInteger()
判断一个整数是否在JS能够识别的范围内,-253到253由于JS浮点数是取了近似值,因此在计算的时候会出现误差,当误差小于Number.EPSILON时,JS就认为不存在误差了。
ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。
为了识别超出JS范围的大数,使用数值后面加n的方式来表示
42n === 42 // false
typeof 123n // 'bigint'
-42n // 正确
+42n // 报错
// BigInt()函数将其他数据类型转换为BigInt
BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n
ES6以后,可以对函数参数制定默认值,指定方式类似TS,直接在参数后加=
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a, b = 1, c) {}).length // 1
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
并且length属性不会计入rest参数
var x = 1;
// 这里参数y默认值为x,生成单独的作用域
function f(x, y = x) {
console.log(y);
}
//这时生成的作用域中x=2,如果作用域中找不到x的值,才会去外层找
f(2) // 2
f() // 1
用于获取函数的多余参数,将多余的参数作为数组传入。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5) // [1,2,3,4,5]
返回函数名
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
this
对象,普通函数this
在运行时制定,箭头函数this
就是定义时上层作用域的this
new
命令arguments
对象。可用rest
代替yield
命令,因此箭头函数不能用作Generator
函数下面两种情况不能使用箭头函数
- 如果一个对象属性定义的函数中需要使用this,则不能使用箭头函数来定义该属性,因为对象不够成单独的作用域。
- 如果需要动态获取this的时候
const cat = {
lives: 9,
//这里的箭头函数this会指向全局作用域
jumps: () => {
console.log(this.lives);
}
}
cat.jump() // undefined
var button = document.getElementById('press');
//这里如果使用箭头函数,则this会指向全局对象,无法获取到当前点击的对象
button.addEventListener('click', () => {
this.classList.toggle('on');
});
尾调用就是某个函数的最后一步仅仅是另一个函数的调用。
function f(x) {
if (x > 0) {
//不一定是函数末尾,但在逻辑上是最后一步
//仅仅是调用,没有任何其他操作
return m(x)
}
return n(x);
}
即只保留内层函数的调用帧,只能在严格模式下使用。
尾调用是函数的最后一步操作,如果内层函数是本次函数调用栈的尾调用,可以直接用内层函数的调用帧,取代外层函数的调用帧。引擎不需要对下一个函数调用创建一个新的栈帧,只需复用已有的栈帧。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误。如果我们将递归修改为尾调用的形式,只存在一个调用帧,就可以避免报错。
//非尾调用
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
//尾调用
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
扩展运算符(spread)是三个点...
。类似rest的逆运算,是吧数组作为按逗号分割的参数序列。
console.log(...[1, 2, 3]) //1,2,3
const a2 = [...a1]; // 复制数组 浅拷贝
[...arr1, ...arr2, ...arr3] // 合并数组 浅拷贝
let [arr1, ... arr2] = [1,2,3] //结构赋值时,只能放在最后
[...'hello'] // [ "h", "e", "l", "l", "o" ] 直接将字符串转换为数组
任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
可以将一个类数组对象或者是定义了遍历器(Iterator)接口的结构转换为新的数组
类数组对象,就是指有length属性的对象,典型的有NodeList对象和arguments
有三个参数,第一个为传入要转化的结构,第二个是一个函数,类似于map,可以对每个元素进行处理,第三个参数可以绑定传入第二个函数中的this
Array.from(string).length // 和扩展预算符一样,可以用作将字符串转化为数组
Array.from({ length: 2 }, () => 'jack') // 这样的写法可以控制函数参数执行的次数
用于将一组值,转换为数组。
由于构造函数Array()
在传入一个参数的时候会将参数识别为数组长度,为了解决这个冲突,就定义了Array.of()
定义数组
Array(3) // [, , ,]
Array.of(3) // [3]
实例方法,在当前数组内部,将指定位置的成员复制到其他位置,它接受三个参数。
[1, 2, 3, 4, 5].copyWithin(0, 3) //[4,5,3,4,5]
用于找出数组中第一个符合条件的元素。
接受一个参数,是一个函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
返回数组中第一个符合条件的元素的位置。
用于填充数组,接收三个参数,第一个是用于填充的值,后两个是填充起始与终止位置(非必传),如果第一个参数为对象,那么填充的只是对象的引用(浅拷贝)
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
实例方法,返回数组的一个遍历器对象,用于for .. of
循环
返回布尔值,表示数组是否包含某个值。接受两个参数,第一个为查找的值,第二个为查找的起始位置(支持负值)。
相比indexOf方法(===
判断),可以正确识别NaN
[1, 2, NaN].includes(NaN) // true
实例方法,用于打平多维数组。返回一个新的数组。接受一个参数,表示打平的层数,如果打平全部层数可以传入Infinity
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
实例方法,相当于map()+flat()
先对数组中的每一项进行处理,再打平一次。传入一个函数作为参数。
const tree = [ 1, [2, 3]];
tree.flatMap(x=>x+1)
// 先进行x+1再打平数组
// [2, '2,31']
实例方法,接受一个整数作为参数,返回对应位置的成员,支持负索引。
ES6有5种遍历方式
for in
用于遍历对象自身和继承的可枚举属性(不含 Symbol 属性)。Object.keys()
返回对象自身可枚举属性(不含Symbol属性)的key组成的数组。Object.getOwnPropertyNames()
返回对象自身所有属性(不含Symbol属性)的key值组成的数组。Object.getOwnPropertySymbols()
返回对象自身所有Symbol属性key值的数组。Reflect.ownKeys(obj)
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。遵守同样的属性遍历的次序规则。
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
其中的enumerable
表示该属性是否可枚举,如果该值为false,则下面四个操作会忽略该属性
1.for…in循环:只遍历对象自身的和继承的可枚举的属性。
2.Object.keys():返回对象自身的所有可枚举的属性的键名。
3.JSON.stringify():只串行化对象自身的可枚举的属性。
4.Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
对象中的函数this
总是指向当前对象,ES6新增super
关键字用来指向当前对象的原型对象。但是必须用在对象的方法之中(必须是直接声明的方法,不能通过字面量赋值的方式)。
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
//制定proto对象为obj对象的原型对象
Object.setPrototypeOf(obj, proto);
//由于原型对象中的foo()方法使用了this,在调用的时候是在子对象中,因此输出的是world
obj.foo() // "world"
扩展运算符用于对象中时,不能复制继承自原型对象的属性。
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined 因为y是在构造函数中定义的,不是对象实例自身的属性
z // 3
解构赋值中使用扩展运算符
let { ...x, y, z } = someObject; // 句法错误
Object.is(a,b)
判断两个值是否相等,相比===,能够准确判断NaN、-0、+0
// ES5 实现
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
Object.assign()
合并对象,接受多个对象作为参数,将源对象(非第一个参数)自身的可枚举属性合并到目标对象中(第一个参数)。如果传入非对象值作为参数,则会先转换为对象。
//完整拷贝一个对象
function clone(origin) {
//获取传入对象的原型对象
let originProto = Object.getPrototypeOf(origin);
//通过原型对象创建一个新对象,然后在合并传入对象
return Object.assign(Object.create(originProto), origin);
}
Object.assign()
中的对象复制是浅拷贝,并且无法复制对象属性的特性。
const source = {
foo:1
};
//这里修改source对象foo属性的特性为不可修改
Object.defineProperty(source,'foo',{writable:false})
const target1 = {};
// 将source对象拷贝到target1中
Object.assign(target1, source);
console.log(Object.getOwnPropertyDescriptor(source,'foo'))
console.log(Object.getOwnPropertyDescriptor(target1,'foo'))
//{value: 1, writable: false, enumerable: true, configurable: true}
//拷贝后foo属性的writable又被初始化为true,相当于重新定义
//{value: 1, writable: true, enumerable: true, configurable: true}
Object.assign()
只能进行值的复制,而不会拷贝它背后的赋值方法或取值方法。因此访问器属性会被转换成数据属性,再进行复制。
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
Object.getOwnPropertyDescriptors()
ES5 的Object.getOwnPropertyDescriptor()
方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了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.assign()无法正确拷贝get属性和set属性的问题。
// 优化后的合并函数
//通过Object.defineProperties来为目标对象绑定属性
const shallowMerge = (target, source) => Object.defineProperties(
target,
//获取源对象内部可遍历属性
Object.getOwnPropertyDescriptors(source)
);
另一个用处,是配合Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
//这种写法也可以实现对象的继承
const obj = Object.create(
prot,// 父对象
Object.getOwnPropertyDescriptors({
foo: 123,
})
);
Object.values()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值value。忽略symbol值属性
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
Object.entries()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名与键值的二维数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
该方法可以用于将对象转化为Map结构
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
Object.fromEntries()
该方法为Object.entries()
的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
__proto__
属性,用来读取或设置当前对象的原型对象。该属性必须是在支持的运行环境中才能使用,有兼容性风险。因此ES6中新增了Object.setPrototypeOf()
和Object.getPrototypeOf()
方法来操作原型对象。
Object.setPrototypeOf()
用来设置一个对象的原型对象(prototype),返回参数对象本身。
// 将A对象的原型设置为B
const a = Object.setPrototypeOf(A, B);
Object.getPrototypeOf()
用于读取一个对象的原型对象
Object.getPrototypeOf(obj);
**
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
?.
用于判断左侧的对象是否为null或undefined。如果是,就不再往下运算,直接返回undefined。
obj?.prop // 对象属性是否存在
obj?.[expr] // 同上
func?.(...args) // 函数或对象方法是否存在
??
它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。
||=、&&=、??=
先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。为属性设置默认值
// 老的写法
user.id = user.id || 1;
user.id || (user.id = 1);
// 新的写法
user.id ||= 1;
属于JS基本数据类型之一,表示独一无二的值。
let simple = symbol()
typeof simple
// "symbol"
可以接收一个字符串作为该Symbol值的描述。通过description
可以取得描述值
let s1 = Symbol('foo');
s1.description // 'foo'
// Symbol值可以转化为字符串或是boolean类型
String(s1) or s1.toString() //'Symbol(foo)'
Boolean(s1) // true
由于Symbol值是不重复的,用于对象的属性名就可以避免属性名出现重名的情况。
Symbol 值作为对象属性名时,不能用点运算符。
在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
let mySymbol = Symbol();
let a = {
[mySymbol]: 'Hello'
};
a[mySymbol]// 'Hello'
可以用于消除魔术字符串(将函数中有逻辑耦合的字符串提取到外部,用一个变量代替),由于只需要保证提取的字符串唯一,就可以在对象中将该字符串属性设置为一个Symbol值。
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of
循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()
返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
Symbol.for()
和Symbol.keyFor()
Symbol.for()
可以获取到同一个Symbol值。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
let s3 = Symbol('bar')
let s4 = Symbol('bar')
s1 === s2 // true
s3 === s4 // false
Symbol.for()
与Symbol()
这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。
Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
const s = new Set();
// 去除数组的重复成员
[...new Set(array)]
Set.prototype.constructor
:构造函数,默认就是Set函数。Set.prototype.size
:返回Set实例的成员总数。Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear()
:清除所有成员,没有返回值。Set.prototype.keys()
:返回键名的遍历器。Set.prototype.values()
:返回键值的遍历器。由于Set对象的键名和键值是同一个值,因此所以keys方法和values方法的行为完全一致。Set.prototype.entries()
:返回键值对的遍历器。for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
Set.prototype.forEach()
:使用回调函数遍历每个成员。let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有一些区别。
- WeakSet 的成员只能是对象,而不能是其他类型的值。
- WeakSet 成员是对象的弱引用,当JS垃圾回收机制回收这些对象时,不会检测到这里的调用。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。
- 由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定
WeakSet 不可遍历。
const a = [[1, 2], [3, 4]];
//可以接受一个数组作为参数来初始化,但是数组中的成员必须都为对象,否则会报错
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
WeakSet 结构有以下三个方法。
WeakSet.prototype.add(value)
:向 WeakSet 实例添加一个新成员。WeakSet.prototype.delete(value)
:清除 WeakSet 实例的指定成员。WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。Map结构类似于对象,但是其键名不局限为一个字符串,还可以是对象等其他数据类型。
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。用来生成一个Map。
如果对同一个键多次赋值,后面的值将覆盖前面的值。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。
const map = new Map();
map.set(['a'], 555);
//这里set和get中的['a']虽然写法完全一样,但却是两个独立的数组
map.get(['a']) // undefined
size
属性返回 Map 结构的成员总数。Map.prototype.set(key, value)
设置键名key的值为valuie,然后返回整个 Map 结构,如果key已经有值,则键值会被更新,否则就新生成该键。可以采用链式写法。Map.prototype.get(key)
获取键名为key的值,如果找不到key,返回undefined。Map.prototype.has(key)
返回一个布尔值,表示某个键是否在当前 Map 对象之中。Map.prototype.delete(key)
删除某个键,返回true。如果删除失败,返回false。Map.prototype.clear()
清除所有成员,没有返回值。遍历方法,返回遍历器,可用for of
拿到具体的值
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
类似WeakSet,只能接受对象作为键名,WeakMap的键名所指向的对象,不计入垃圾回收机制。可以防止内存泄漏。
WeakSet 和 WeakMap 是基于弱引用的数据结构,ES2021 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。
let target = {};
let wr = new WeakRef(target);//定义一个对象的弱引用对象,垃圾回收机制不会计入这个引用。
该实例对象有一个方法deref()
,用于返回原始引用的对象。如果原始对象已经被垃圾回收机制清除,该方法返回undefined
。
let target = {};
let wr = new WeakRef(target);
console.log(wr.deref() === target); // true
标准规定,一旦使用WeakRef()创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除。
proxy可以拦截对对象的访问,可以对外界的访问进行过滤和改写。
//作为构造函数,Proxy()接收两个参数,第一个是拦截的目标对象,第二个是描述代理的操作的对象
var proxy = new Proxy({}, {
//这里拦截了读取属性的操作,传入两个参数,第一个是拦截的目标对象,第二个是要访问的属性。
get: function(target, propKey) {
//无论读取什么属性,都返回一个固定值35
return 35;
}
});
proxy.time // 35
proxy.name // 35
用Proxy代理的对象,其内部的this指向会指向proxy代理的对象
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Reflect
对象和Proxy
对象都是ES6为了操作对象提供的新API。主要目的为
Object
上的属于语言内部的方法(比如Object.defineProperty
)转移到Reflect
上。目前这两个关键字都可以调用这些方法,后续新方法将只部署在Reflect对象上。Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
Object
上的操作都变成函数式,比如name in obj
和delete obj[name]
,可以写作Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
Proxy
对象中的方法一一对应。配合使用可以在不影响对象原本的行为的情况下,部署额外的功能。比如观察者模式// 一个观察者模式的实例,如果观察的对象有变化,就执行打印操作
const queueObserve = new Set();
//定义一个observe方法,用于将监听变化后的执行的操作添加到一个Set结构里
function observe(fn){
queueObserve.add(fn)
}
//observable方法对监听对象的set方法进行拦截
function observable (obj){
return new Proxy(obj,{
set:function(target, name, value, receiver) {
//在不修改原本操作的基础上
const result = Reflect.set(target, name, value, receiver);
// 执行所有添加的操作
queueObserve.forEach(observer => observer());
return result
}
})
}
// observable函数创建一个被观察的对象
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
// observe添加对象变化后的打印方法
observe(print);
person.name = '李四';
// 李四,20