ECMAScript 6 入门 (Day08)
8.1 Object 数据结构的改变
对象(object)是 JavaScript 最重要的数据结构。ES6 对object数据结构进行了重大升级
8.1.1 属性的简洁表示
1、属性
const foo = 'bar';
const obj = {foo}; // {foo: "bar"}
// 等同于
const obj = {foo: foo}; // {foo: "bar"}
2、方法
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
3、实际例子
1.定义对象
let birth = '2000/01/01';
const Person = {
name: '张三',
birth, //等同于birth: birth
hello() { console.log('我的名字是', this.name); } // 等同于hello: function ()...
};
2.导出变量
let ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
8.1.2 属性名表达式
JavaScript 定义对象的属性,有两种方法。
1、用标识符作为属性名
obj.foo = true;
2、用表达式作为属性名
obj['a' + 'bc'] = 123;
但是,如果使用字面量(使用大括号)定义对象,在ES5中只能使用标识符定义属性。
ES6中允许字面量定义对象时,使用表达式作为对象的属性名,把表达式放在方括号[]
内。
let objKey='isOK';
let obj={
[objKey]:true,
['a'+'bc']:123
};
表达式还可以用于定义方法名。
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
8.1.3 属性的可枚举性
1、属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。
Object.getOwnPropertyDescriptor(object,prop)
方法:获取该属性的描述对象
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable
属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable为false的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。
Object.keys()
:返回对象自身的所有可枚举的属性的键名。
JSON.stringify()
:只串行化对象自身的可枚举的属性。
Object.assign()
: ES6 新增,忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
其中,只有for...in
会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。
另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in
循环,而用Object.keys()
代替。
8.1.4 属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性。
1、for…in
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
2、Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
3、Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
4、Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol 属性的键名。
5、Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性
8.2 ES5 中新增的方法
1、Object.keys()
Object.keys(obj)
用于获取对象自身(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名,返回一个由属性名组成的数组。- 类似
for...in
var fruit = {
name : '苹果',
desc : '红富士'
};
console.log('获取对象自身(不含继承的)所有可枚举属性',Object.keys(fruit));
// ["name", "desc"]
2、Object.defineproperty()
Object.defineproperty(obj,prop,describer)
定义对象中的新属性或修改原有的属性。
obj
:必需,目标对象prop
:必需,需定义或修改的属性名describer
:必需,目标属性所拥有的特性
其中第三个参数 describer
为一个对象{}
- value:设置属性的值,默认为undefined
- writable:值是否可以重写。true | false , 默认为false
- enumerable:目标属性是否可以被枚举。true | false , 默认为false
- configurable:目标属性是否可以被删除或再次修改特性。true | false , 默认为false
- get: 属性的 getter 函数,如果没有 getter,默认为 为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象该函数的返回值会被用作属性的值。
- set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
var fruit = {
name : '苹果',
desc : '红富士'
};
Object.defineProperty(fruit,'desc',{
enumerable : false,
get(){
return descValue
},
set(newVal){
console.log(newVal)
descValue=newVal
}
});
console.log('修改属性描述符为不可枚举',Object.keys(fruit));
// 输出:
// 修改属性描述符为不可枚举 ["name"]
下面是 vue2.* 响应式数据的原理
let state = { count:0 }; //1.这是一个对象数据,将它变成响应式的数据
let active = null; // 3.定义一个中间变量,用来记录 当前的 watcher 订阅函数
//2.创建一个方法 将 state 变成响应式的数据
function defineReative(obj){
for(let key obj){//循环对象的所有属性
let value=obj[key]; //对象的属性对应的值 ,state.count的值为0
let dep=[];// 4.定义一个数组(相当于一个队列),用来记录所有的订阅函数
//Object.defineProperty直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
//所有的属性都会执行object.defineProperty方法
Object.defineProperty(obj,key,{
get(){//在取值时调用,将当前的watcher订阅函数push到 队列 dep中去
if(active){ //如果state.count值没改变时,即当active为null时,不用重新收集依赖
dep.push(active) //收集依赖
}
return value;
},
set(newVal){//在 state.count 改变时,将newVal赋值给value 触发更新
value=newVal;
dep.forEach(item=>item();) //此时的item为 watcher 函数
}
})
}
}
defineReative(state) //将state传进去,重新修改对象的属性
const watcher=(fn)=>{ //定义一个订阅 函数
active=fn //将当前的 fn 赋值给active
fn(); //执行fn函数
active=null //执行完后将active置null
}
//订阅一个,将state.count的值渲染到id为app的 div中
watcher(()=>{
document.getElementById('#app').innerHTML=state.count; //取值
})
//再订阅一个,打印最新的值
watcher(()=>{
console.log(state.count);//打印最新的值
})
state.count++
如果数据不需要响应式,可以用Object.freeze()
冻结对象,那样这个对象就用不了Object.defineProperty()
数组响应式原理
- 使用数据劫持的方式,重写数组的方法(push、pop、shift、unshift、splice、sort、reverse七个方法)
- Vue 将data中的数组进行了原型链重写,指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新。
- 如果数组中包含着引用类型,会对数组中的引用类型再次进行监控。
- 如果修改数组的索引和长度是无法监控到的。需使用Vue.$set() 来进行处理,其核心内部使用的是splice方法
let state=[1,2,3]
let originalArray=Array.prototype; //复制一份 数组原来的方法
let arrayMethods=object.create(originArray) //浅拷贝 得到一个新的对象
function defineReactive(obj){
//函数劫持
arrayMethods.push=function(...args){
originalArray.call(this,...args)
render();//更新视图
}
obj.__proto__=arrayMethods; js中的原型链
}
defineReactive(state);
function render(){
app.innerHTML=state
}
render();
setTimeout(()=>{
state.push(4)
},1000)
vue2.0中使用object.defineProperty
的弊端
object.defineProperty
将对象的每个属性都重新定义了一个,性能不好。- 对象嵌套,比如
a={b:{c:0}}
,这时需要进行递归循环
才能将对象中的所有属性都涉及到,层级过深,性能就会差 - 不需要响应的数据也放到了里面(性能优化问题)
所以vue3.0用proxy
代替了object.defineProperty
,proxy的小弊端在于兼容性不好
3、Object.isExtensible() | Object.preventExtensions()
Object.isExtensible(obj)
判断一个对象是否可扩展,返回 true | false
Object.preventExtensions(obj)
阻止对象扩展,阻止后对象
- 不能添加属性。
- 可以修改属性的值。
- 可以删除属性。
- 可以修改属性描述符
var fruit = {
name : '苹果',
desc : '红富士'
};
console.log('isExtensible',Object.isExtensible(fruit)); // true
Object.preventExtensions(fruit); //{name: "苹果", desc: "红富士"}
console.log('isExtensible',Object.isExtensible(fruit)); // false
Object.defineProperty(fruit,'price',{
value : 6
});
console.log('为对象fruit添加属性',Object.keys(fruit));
4、Object.isSealed() | Object.seal()
Object.isSealed(obj)
判断一个对象是否被密封,返回 true | false
Object.seal()
将对象密封,对象被密封后
- 不能添加属性。
- 不能删除属性。
- 可以修改属性。
- 不能修改属性描述符。(会抛异常)
5、Object.isFrozen() | Object.freeze()
Object.isFrozen(obj)
判断一个对象是否被冻结,返回 true | false
Object.freeze()
将对象冻结,对象被冻结后
- 不能添加属性。
- 不能删除属性。
- 不能修改属性。(赋值)
- 不能修改属性描述符。(会抛异常)
var fruit = {
name : '苹果',
desc : '红富士'
};
console.log('isFrozen',Object.isFrozen(fruit)); // false
Object.freeze(fruit);
console.log('isFrozen',Object.isFrozen(fruit)); // true
delete(fruit.desc);
console.log('删除属性',fruit); //删除不成功
6、拓展、密封、冻结对象三者区别
8.3 ES6 中新增的方法
1、Object.is()
ES5 比较两个值是否相等,只有两个运算符,可是它们都有缺点:
- 相等运算符(
==
)会自动转换数据类型。 - 严格相等运算符(
===
),NaN
不等于自身,+0
等于-0
。
Object.is(value1,value2)
采用ES6 提出“Same-value equality”(同值相等)算法,用来比较两个值是否严格相等,与严格比较运算符(===
)的行为基本一致。不同之处只有两个:一是+0
不等于-0
,二是NaN
等于自身。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
2、Object.assign ()
Object.assign(target, source1, source2)
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign(target, source1, source2)
方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
- 如果只有一个参数,Object.assign会直接返回该参数。
const obj = {a: 1};
Object.assign(obj) === obj // true
- 如果该参数不是对象,则会先转成对象,然后返回。
typeof Object.assign(2) // "object"
- 由于
undefined
和null
无法转成对象,所以如果它们作为参数,就会报错。如果undefined
和null
不在首参数,就不会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
- 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
- 除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
- 只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。
Object(true) // {[[PrimitiveValue]]: true}
Object(10) // {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
Object.assign()
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
上面代码中,Object.assign
要拷贝的对象只有一个不可枚举属性invisible
,这个属性并没有被拷贝进去。
- 属性名为 Symbol 值的属性,也会被
Object.assign
拷贝。
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
Object.assign()
的常见用途
- 为对象添加属性
- 为对象添加方法
- 克隆对象
- 合并多个对象
- 为属性指定默认值
//为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
//为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
//克隆对象
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
//合并多个对象
const merge = (...sources) => Object.assign({}, ...sources);
//为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
3、Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptor()
:会返回某个对象属性的(descriptor)描述对象(ES5)。
Object.getOwnPropertyDescriptors()
:返回指定对象所有自身属性(非继承属性)的描述对象。(ES2017)
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.getOwnPropertyDescriptors()
方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。
Object.getOwnPropertyDescriptors()
方法配合Object.create()
方法,将对象属性克隆到一个新对象,实现浅拷贝。
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
// 或者
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj), //获取Object的原型对象
Object.getOwnPropertyDescriptors(obj) //返回 obj 所有自身属性(非继承)的描述对象
);
4、Object.setPrototypeOf(),Object.getPrototypeOf(),__proto__属性
JavaScript 语言的对象继承是通过原型链实现的。
__proto__
属性(前后各两个下划线):用来读取或设置当前对象的原型对象(prototype)
Object.setPrototypeOf()
(写操作):用来设置一个对象的原型对象(prototype),返回参数对象本身。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
该方法等同于下面的函数。
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
Object.getPrototypeOf()
(读操作):用于读取一个对象的原型对象
Object.getPrototypeOf(obj);
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
如果参数不是对象,会被自动转为对象。
// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo') // String {length: 0, [[PrimitiveValue]]: ""}
// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true) // Boolean {[[PrimitiveValue]]: false}
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
如果参数是undefined或null,它们无法转为对象,所以会报错。
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object
Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object
Object.create()
(生成操作):用于创建一个新对象,使用现有的目标对象来提供新创建的对象的__proto__
,返回一个新对象,带着指定的原型对象和属性。
const person = {
isHuman: false,
printIntroduction: function() {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten
me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"
5、Object.keys(),Object.values()
Object.keys(obj)
:返回一个成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组
Object.values(obj)
:返回一个成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值的数组
6、Object.entries(),Object.fromEntries()
Object.entries(obj)
:返回一个成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
Object.entries
方法的另一个用处是,将对象转为真正的Map结构。
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
自己实现Object.entries
方法,非常简单。
// Generator函数的版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// 非Generator函数的版本
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
Object.fromEntries()
:Object.entries()
的逆操作,用于将一个键值对数组
转为对象
。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
该方法的一个用处是配合URLSearchParams对象,将查询字符串转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
参考链接: