JavaScript简明教程-对象

207 阅读12分钟

基本概念

对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合(数组是有序的复合数据集合)

属性及方法的简写

进行对象定义时可以使用简写方式,优化一部分写法

let name = 'rede';
let age = 3;
// 属性的简写,把变量name当作key值,变量值当做value
let foo = {name, age};
foo; // {name: "rede", age: 3}
// 方法的简写
foo = {name, age, sayHi () {console.log('hi')}};

对属性的读取有两种方法:点运算符、方括号运算符

let info = {
	name: 'rede'
}
info.name; // rede
info['name']; // rede

使用括号形式如果不是使用字符串形式,会先按变量处理

let val = 'name';
info[val]; // rede
// 可以直接对读取的属性进行赋值,如果存在会进行覆盖,如果不存在则会新建相关属性
info.name = 'info';
info.foo = 'foo';
info; // {name: "info", foo: "foo"}
// Object.keys可以查看所有可以遍历的属性
Object.keys(info); // ["name", "foo"]
// delete可以用于删除对象的属性,这个方法,如果去删除一个不存在的属性一样也是返回true
delete info.foo; // true
delete info.age; // true

方法的name属性

const person = {
	sayName () {...}
}
person.sayName.name; // sayName

如果方法使用了取值函数(getter) 和存值函数(setter),则name 属性存在于方法属性的描述对象的get和set属性上,返回值是方法名前加get和set

const obj = {
  get foo() {},
  set foo(x) {}
};

Object.getOwnPropertyDescriptor(obj, 'foo').get.name; // get foo
Object.getOwnPropertyDescriptor(obj, 'foo').set.name; // set foo

bind 方法创造的函数,name 属性返回 bound 加原函数名

let doSomeThing = () => {
	conole.log('do some thing');
};
doSomeThing.bind().name; // bound doSomeThing

Function 构造函数,name 属性返回anonymous

(new Function()).name;

如果对象的方法是一个Symbol值,那么name 属性返回的是这个Symbol值的描述

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // "" key2没有描述也就没有返回值

属性的可描述对象(Descriptor)

每个属性都有一个描述对象,用来控制属性的行为,Object.getOwnPropertyDescriptor 可以获取属性的描述对象

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo');
// {value: 123, writable: true, enumerable: true, configurable: true}
  • value: 属性值
  • writable: 是否可修改
  • enumerable: 是否可遍历

有四个操作会忽略enumerable为false的属性

* for..in: 只遍历对象自身的和继承的可枚举的属性
* Object.keys: 返回对象自身的所有可枚举的属性的键名
* JSON.stringify: 只串行化对象自身的可枚举的属性
* Object.assign: 只拷贝对象自身的可枚举的属性
  • configurable: 能否使用delete、能否需改属性特性、或能否修改访问器属性

扩展运算符

  • 对象的扩展运算符用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
let z = { a: 3, b: 4 };
let n = { ...z };
n; // { a: 3, b: 4 }
  • 对象的扩展运算符等同于使用Object.assign() 方法
let aClone = { ...a };
let aClone = Object.assign({}, a);
  • 扩展运算符可以用于合并两个对象
let ab = { ...a, ...b};
let ab = Object.assign({}, a, b);
  • 对象的扩展运算符属性会覆盖位于扩展运算符前面的同名属性
let a = {x: 4, y: 4};
let aWithDefaults = { x: 1, ...a, y: 2 };
{x: 4, y: 2}
  • 对象的扩展运算符可以跟表达式
const x = 2;
const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};
obj;
// {a: 1, b: 2}
  • 扩展运算符的参数对象中,如果有取值函数get,这个函数会执行
let run = {
  ...{
    get x() {
      console.log('x会执行')
    }
  }
};
// x会执行

使用对象直接量的形式创建对象,不要使用new Object的形式

in操作符和for...in

  • in操作符返回一个布尔值,表示一个对象是否具有某个属性
  • for...in获得对象的所有可遍历属性
let info = {name: 'rede'};
'name' in info; // true
'toString' in info; // true toString是继承来的属性,但是in运算符不会区分,只检查是否存在
...
let foo = Object.create(info, {
	age: {value:2, enumerable: false} // 设定age是不可遍历属性
})
// in运算符同时也可以判断出不可遍历属性是否存在
'age' in foo; // true
...
// for...in是循环可遍历属性
foo.foo = 'foo'
for (attr in foo) {console.log(attr)}; // foo name; age属性是不可遍历的,所以循环不出
...
// 配合hasOwnProperty可以只循环出自身对象的属性
for (attr in foo) {
	if (foo.hasOwnProperty(attr)) {
		console.log(attr);
	}
}; // foo name是继承的属性,age是不可遍历属性
// 如果只查看当前对象的属性可以使用Object.keys
Object.keys(foo); // ["foo"]

super关键字

super 指向当前对象的原型对象,相当于是在对象内部调用Object.getPrototypeOf(this) ,这个关键字目前只能用在对象方法的简写形式中,用在其他地方会报错

const obj = {
  find: function () {
    return super.foo
  }
}; // Uncaught SyntaxError: 'super' keyword unexpected here
...
const obj = {
  find() {
    return super.foo
  }
}; // 正常
const proto = {
  foo: 'hello'
};

Object.setPrototypeOf(obj, proto);
obj.find(); // hello 

Object相关方法

  • Object.is: 同值相等,用来比较两个值是否严格相等,与严格运算符===行为基本一致,但是这个能更好的区分+0与-0,以及NaN的问题
+0 === -0; // true
NaN === NaN; // false

Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
  • Object.assign: 方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象
const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2); // 第一个是目标对象,后面都是源对象
target // {a:1, b:2, c:3}

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
const source = {source1, source2};

Object.assign(target, source); // {a: 1, source1: {…}, source2: {…}}
source1.b = 4; 
target.source1.b; // 4 修改源对象的值,浅拷贝对象对应的值也同步更新

如果源对象和目标对象有同名属性,会以最后的源对象为准

const target = { a: 1 };
const source1 = { b: 2, a: 2 };
const source2 = { c: 3, a: 3 };
Object.assign(target, source1, source2); // {a: 3, b: 2, c: 3}
...
// 如果是数组这个方法一样可用
Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3] 

如果要进行对象克隆,建议使用对象展开符,避免使用Object.assign的形式

// very bad,在从复制出的对象上删除某个值,但实际上原始对象的值也会被影响
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 });
delete copy.a; // original: { b: 2, c: 3 } copy: { b: 2, c: 3 }

// bad,可以达到预期目的,但在写法上可读性不好
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }
delete copy.a; // original: { a: 1, b: 2 } copy: { b: 2, c: 3 }

// good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }
delete copy.a; // original: { a: 1, b: 2 } copy: { b: 2, c: 3 }
  • Object.keys: 方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名
  • Object.getOwnPropertyNames: 接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名,使用方式与Object.keys基本一致,区别在于这个方法可以返回不可枚举属性
// 数组是一个有序的数据的集合,其中length属性是不可枚举的属性
let a = ['Hello', 'World'];
Object.keys(a); //  ["0", "1"]
Object.getOwnPropertyNames(a); // ["0", "1", "length"]
  • Object.values: 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
  • Object.entries: 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组

Object.entries和Object.values行为一致,区别就是Object.entries返回的键值对

let info = {name: 'rede', ted: 'ted'};
let foo = Object.create(info, {
	foo: {value: 'foo', enumerable:true},
	age: {value:2, enumerable:false}
});
Object.values(foo); // ["foo"] age是不可遍历属性,name, ted是继承属性
Object.entries(foo); // [Array(2)] ['bar', 'foo']
Object.entries(info); // [Array(2), Array(2)] ['name', 'rede']['ted', 'ted']
  • Object.fromEntries: 用于将一个键值对数组转为对象
Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
]);
// {foo: "bar", baz: 42}
  • Object.getPrototypeOf: 返回对象的原型
// 这个判断只对对象有效或者可以转为对象的类型有效
Object.getPrototypeOf(null); // Uncaught TypeError: Cannot convert undefined or null to object
// 如果是可以转为对象类型的一样可以正常判断
Object.getPrototypeOf(2); // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, toString: ƒ, …}
// 如果使用这个方法,可以看到NaN会返回相关信息,能明显确定为数据类型
Object.getPrototypeOf(NaN); // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, toString: ƒ, …}
  • Object.getOwnPropertySymbols: 返回一个给定对象自身的所有 Symbol 属性的数组
let obj = {};
let a = Symbol('a');
obj[a] = 'local';
Object.getOwnPropertySymbols(obj);
// [Symbol(a)]

这个方法只能针对实例中那样,部署了Symbol才会有返回值,否则这个方法只会返回一个空数组

  • Object.setPrototypeOf: 方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象
let a = {};
let b = {x: 1};
Object.setPrototypeOf(a, b); // 指定a对象的原型是b对象,此时a对象可以共享b对象的属性
a.x; // 1
b.y = 2;
a.y; // 2 b对象更新,对应a对象也一起更新
let c = {};
Object.setPrototypeOf(c, a); // 依然可以指定c对象的原型是a,此时c一样共享a对象属性
b.z = 3;
c.z; // 3
  • Object.getOwnPropertyDescriptor: 获取某个属性的描述对象
let info = {name: 'rede'};
Object.getOwnPropertyDescriptor(info, 'name');
// {value: "rede", writable: true, enumerable: true, configurable: true}
  • Object.getOwnPropertyDescriptors: 返回指定对象所有自身属性(非继承属性)的描述对象
let info = {name: 'rede', age: 2}
Object.getOwnPropertyDescriptors(info); // {name: {…}, age: {…}}
  • Object.defineProperty:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
let info = {name: 'rede', ted: 'ted'};
// 这里对属性的修改是指定多少修改多
Object.defineProperty(info, 'ted', {
	value : 'foo'
});
Object.getOwnPropertyDescriptors(info); // enumerable,configurable,writable未发生变化
// 如果是新属性相关值和create一样默认为false
...
Object.defineProperty(info, 'ted', {
	value : 'foo',
	enumerable: false
});
Object.keys(info); // ["name"] ted属性变成不可遍历
  • Object.defineProperties:直接在一个对象上定义新的属性或修改现有属性,并返回该对象

功能和Object.defineProperty一致,区别在于这个方法可以一次修改多个值

let info = {name: 'rede', ted: 'ted'};
Object.defineProperties(info, {
	name: {value:'name'},
	foo: {value:'foo'}
});
Object.keys(info); // ["name", "ted"] foo是新建属性,默认为不可遍历
  • Object.preventExtensions:方法让一个对象变的不可扩展,也就是永远不能再添加新的属性
  • Object.isExtensible:判断对象是否可扩展
let info = {name: 'rede', ted: 'ted'};
let foo = Object.create(info);
Object.preventExtensions(foo); // foo被设为不可扩展了,只有继承属性
info.age = 2;
foo.age; // 2
foo.age = 3;
foo.age; // 2 
Object.isExtensible(foo); // false foo不可以扩展
  • Object.seal:阻止添加新属性并将所有现有属性标记为不可配置
  • Object.isSealed:判断一个对象是否为不可配置
let info = {name: 'rede', ted: 'ted'};
Object.seal(info);
Object.getOwnPropertyDescriptors(info); // 两个属性的configurable值为false
Object.isSealed(info); // true info已经被封闭不可删除相关属性
  • Object.freeze:不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性
  • Object.isFrozen:判断一个对象是否被冻结
let info = {name: 'rede', ted: 'ted'};
Object.freeze(info); //属性的writable和configurable值均修改为false
Object.isFrozen(info); // true
  • Object.create:该方法可以指定原型对象和属性,返回一个新的对象

Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法

let info = {name: 'rede'};
let foo = Object.create(info);
info.name; // rede
foo.name; // rede
// 通过Object.create新建对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上
info.name = 'foo';
foo.name; // foo

Object.create方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性

let info = {name: 'rede'};
let foo = Object.create(info, {
	foo: {
    value: 'foo',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});
foo.name; // "rede"
foo.foo; // "foo"

// 参数设定的值中
// 如果省略这些值,这些值会默认为false
enumerable: 是否可遍历
	有四个操作会忽略enumerable为false的属性
	-for..in: 只遍历对象自身的和继承的可枚举的属性
	-Object.keys: 返回对象自身的所有可枚举的属性的键名
	-JSON.stringify: 只串行化对象自身的可枚举的属性
	-Object.assign: 只拷贝对象自身的可枚举的属性
	
writable: 是否可修改
configurable: 能否使用delete、能否需改属性特性、或能否修改访问器属性
  • Object.getPrototypeOf:获取对象的Prototype对象

定义在prototype的方法是所有实例对象都继承了这些方法

下面方法统一使用的变量

let info = {name: 'rede'};

  • Object.prototype.valueOf:返回当前对象对应的值,默认情况下就是返回对象本身
info.valueOf(); // {name: 'rede'}
// valueOf方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法(是方法而不是属性)
1 + info; // "1[object Object]"
info.valueOf = '2';
1 + info; // "1[object Object]"
// 定义valueOf方法覆盖Object.prototype.valueOf的方法
info.valueOf = () => 3
1 + info; // 4
  • Object.prototype.toString:返回当前对象对应的字符串形式
info.toString(); // "[object Object]"
// 通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式
info + ' foo'; // "[object Object] foo"
// 定义覆盖方法
info.toString = () => 'rede';
info + ' foo'; // "rede foo"
// 该方法借助call改变this指向可以判断使用值的的数据类型,这比typeof判断更准确
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(info); // "[object Object]"
  • Object.prototype.toLocaleString:返回当前对象对应的本地字符串形式

这个方法和toString使用一致,都是返回一个值的字符串形式,与toString的主要区别是这个方法是留出接口让各个对象可以返回特定值

// 虽然阮一峰老师教程中说了Array、Number、Date都有自己的实现,不过试后发现只有Date有特定值
let date = new Date();
date.toString(); // "Tue Jul 03 2018 15:32:28 GMT+0800 (中国标准时间)"
date.toLocaleString(); // "2018/7/3 下午3:32:28"
// 这个方法不能像toString方法用来判断数据类型
// 有意思的地方如果使用Object.prototype.toLocaleString.call这时的返回值和date.toString()一致
Object.prototype.toLocaleString.call(date); // "Tue Jul 03 2018 15:32:28 GMT+0800 (中国标准时间)"
  • Object.prototype.hasOwnProperty:判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性
info.hasOwnProperty('name'); // true info对象有name属性
info.hasOwnProperty('toString'); // false toString是从Object继承的属性
  • Object.prototype.isPrototypeOf:判断当前对象是否为另一个对象的原型
let foo = Object.create(info);
info.isPrototypeOf(foo); // true info是在foo的原型
  • Object.prototype.propertyIsEnumerable:判断某个属性是否可枚举
  • Object.prototype.__proto__: 实例对象的__proto__属性(前后各两个下划线),返回该对象的原型。该属性可读写,而且如果使用的话需要浏览器支持才可以(通过caniuse来看,基本都支持)

不过Object.getPrototypeOf是标准写法,建议使用这个方法获取原型

// 这个方法和Object.getPrototypeOf效果一致,都是返回对象的原型
info.__proto__ === Object.getPrototypeOf(info); // true
// 但是Object.getPrototypeOf是只读,不可进行赋值
Object.getPrototypeOf(info) = {}; // Uncaught ReferenceError...
// __proto__是可读写的
info.__proto__ = {};
Object.getPrototypeOf(info); // {} 就是上文定义的原型