Object是在javascript中一个被我们经常使用的类型,js中几乎所有的对象都是继承自Object对象,虽然我们平时只是简单的使用Object对象来存储数据,但事实上,Object对象是javascript中的核心内容。Object对象其实包含了很多有用的属性和方法,深入理解它的属性和方法有利于在工作中更好且更优雅的操作对象。诸如原型/原型链,对象深拷贝/浅拷贝等知识点也是面试过程中常会考察的点。
Object对象的创建
Object对象的创建常见的有两种方式:对象字面量和Object构造函数的方式。
// 1. 对象直面量
var obj = {
name: 'zhang',
age:18
}
console.log(obj.constructor === Object); // true
// 2.Object构造函数
var obj1 = new Object();
obj1.name = 'zhang';
obj1.age = 18;
console.log(obj1.constructor === Object); // true
无论是通过对象字面量的方式还是Object构造函数的方式创建的Object对象都具有实例属性constructor,该属性指向创建实例对象的构造函数的引用,可以看到两者的constructor属性都是Object。Object对象实例的另一个属性是__proto __ 属性, __ proto __ 属性是一个访问器属性,通过它可访问对象的内部的[[Prototype]], __ proto __ 属性也可以在对象定义时通过对象[[Prototype]]来创建。
let Circle = function() {};
let p = {
a: function() {
console.log('aaa');
}
};
// 通过__proto__设置对象的原型链引用
Circle.prototype.__proto__ = p;
let shape = new Circle();
shape.a(); // aaa
console.log(Circle.prototype === shape.__proto__); // true
Object对象的方法
Object对象的方法分为构造函数方法和实例方法。
构造函数方法
-
Object.assign():将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象。
语法:Object.assign(target, ...sources)
target是目标对象,sources源对象。返回值为目标对象,如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖,后面的源对象属性将类似地覆盖前面的源对象的属性。并且其只会拷贝源对象自身的并且可枚举的属性到目标对象。
// 1.复制一个对象
const obj = {a: 1};
const copy = Object.assign({}, obj);
console.log(copy); // {a: 1}
// 2.深拷贝问题,如果源对象的属性值是一个对象的引用,它仅仅会复制其引用值
function test() {
let obj1 = {a: 0, b: {c: 0}};
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2)); // {a: 0, b: {c: 0}}
obj1.a = 1;
console.log(JSON.stringify(obj1)); // {a: 1, b: {c: 0}}
console.log(JSON.stringify(obj2)); // {a: 0, b: {c: 0}}
obj1.b.c = 2;
console.log(JSON.stringify(obj1)); // {a: 1, b: {c: 2}}
console.log(JSON.stringify(obj2)); // {a: 0, b: {c: 2}}
// Deep Clone
obj1 = {a: 0, b: {c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 3;
obj1.b.c = 3;
console.log(JSON.stringify(obj3)); // {a: 0, b: {c: 2}}
}
test();
// 3.合并对象,相同属性会被覆盖
const o1 = {a: 1, b: 1, c: 1};
const o2 = {b: 2, c: 2};
const o3 = {c: 3};
const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // {a: 1, b: 2, c: 3}
// 4.继承属性和不可枚举属性不能被拷贝
const obj = Object.create({foo: 1}, { // foo为原型上的属性,属于继承属性
bar: {
value: 2 // bar是个不可枚举属性,enumerable默认为false
},
baz: {
value: 3,
enumerable: true
}
});
const copy = Object.assign({}, obj);
console.log(copy); // {baz: 3}
-
Object.create():创建一个新对象,使用现有的对象提供新对象的__ proto __ 。
语法:Object.create(proto, propertiesObject)
proto指定新创建对象的原型对象,propertiesObject为新创建的对象添加指定的属性值和对应的属性描述符。返回值为带有指定的原型对象和属性的新对象。
const o1 = {a:1, say: function(){console.log(this.a)}}
const o2 = Object.create(o1, {
foo: {
writeable: true,
configurable: true,
value: 'hello'
},
bar: {
configurable: false,
get: function() {return 10},
set: function(value) {console.log('Setting o.bar to ', value)}
}
});
console.log(o2.a); // 1
console.log(o2.say()); // 1
console.log(o2.bar); // 10
o2.bar = 20; // Setting o.bar to 20
-
Object.defineProperties():在一个对象上定义新的属性或修改现有属性,并返回该对象。
语法:Object.defineProperties(obj, props)
obj为在其上定义或修改属性的对象,props为定义了可枚举属性或修改的属性描述符的对象。描述符有几个键:configurable只有为true时该属性才可以从对应对象修改或删除,enumerable在枚举相应对象上的属性时该属性显现,value与属性关联的值,默认为undefined,writable只有为true该属性相关联的值才能被赋值运算符改变,get作为该属性的getter函数,函数的返回值被用作属性的值,set作为属性的setter函数,函数仅接受参数赋值给该属性的新值。
const obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});
console.log(obj); // {property1: true, property2: 'Hello'}
obj.property1 = false;
obj.property2 = 'World';
console.log(obj); // {property1: false, property2: 'Hello'}
-
Object.defineProperty():直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。
语法:Object.defineProperty(obj, prop, desciptor)
obj为要定义属性的对象,prop为要定义或修改的属性的名称或Symbol,descriptor为要定义或修改的属性描述符。
属性描述符有两种:数据描述符和存取描述符。数据描述符是具有值的属性,存取描述符是由getter和setter函数所描述的属性。数据描述符不能拥有get和set键值,存取描述符不能拥有value和writable键值。
// 创建属性
const o = {};
// 添加一个数据描述符属性
Object.defineProperty(o, 'a', {
value: 37,
writable: true,
enumerable: true,
configurable: true
});
// 添加一个存取描述符属性
var bValue = 38;
Object.defineProperty(o, 'b', {
get(){return bValue;},
set(newValue){ bValue = newValue;},
enumerable: true,
configurable: true
});
console.log(o); // {a: 37, b:38}
bValue = 39;
console.log(o.b); // 39
-
Object.entries():返回一个给定对象自身可枚举属性的键值对数组。
语法:Object.entries(obj)
obj为可以返回其可枚举属性的键值对的对象,返回值为给定对象自身可枚举属性的键值对数组。
const obj = {foo: 'bar', baz: 42};
console.log(Object.entries(obj)); // [['foo', 'bar'],['baz', 42]]
// 类数组对象
const obj = {0: 'a', 1: 'b', 2: 'c'};
console.log(Object.entries(obj)); // [['0','a'],['1','b'],['2','c']]
// Object转换为Map
const obj = {0: 'a', 1: 'b', 2: 'c'};
const map = new Map(Object.entries(obj));
console.log(map); // Map {'0': 'a', '1': 'b', '2': 'c'}
-
Object.freeze():冻结一个对象,被冻结的对象不能被修改,添加或删除属性,对象的原型也不能被修改。
语法:Object.freeze(obj)
obj为要被冻结的对象,返回值为被冻结的对象。
const obj = {
prop: function(){},
foo: 'bar'
};
Object.freeze(obj);
obj.foo = 'baz';
console.log(obj.foo); // 'bar' 此时修改属性等操作都失效
// 浅冻结,冻结对象的属性若为对象或数组,仍可添加删除
const obj1 = {
internal: {},
arr: []
};
Object.freeze(obj1);
obj1.internal.a = 'aValue';
obj1.arr.push(1);
console.log(obj1); // {internal: {a: 'aValue'}, arr: [1]}
-
Object.fromEntries():将键值对列表转换为一个对象
语法:Object.fromEntries(iterable)
iterable为类似Array、Map或其他可迭代对象,返回值为新对象。
// Map转化为Object
const map = new Map([['foo', 'bar'],['baz', 42]]);
const obj = Object.fromEntires(map);
console.log(obj); // {foo: 'bar', baz: 42}
// Array转化为Object
const arr = [['0', 'a'], ['1', 'b'], ['2', 'c']];
const obj = Object.fromEntries(arr);
console.log(obj); // {0: 'a', 1: 'b', 2: 'c'}
-
Object.getOwnPropertyDescriptor():返回指定对象上一个指定属性对应的属性描述符
语法:Object.getOwnPropertyDescriptor(obj, prop)
obj为需要查找的目标对象,prop为目标对象内的属性名称,返回值为指定属性的属性描述符对象,不存在该属性则返回undefined。
const o = {
get foo(){return 17;}
};
const d = Object.getOwnPropertyDescriptor(o, 'foo');
console.log(d); // {configurable: true, enumerable: true, get: foo(), set: undefined}
const o1 = {
bar: 42
};
const d1 = Object.getOwnPropertyDescriptor(o1, 'bar');
console.log(d1); // {value: 42, writable: true, enumerable: true, configurable: true}
-
Object.geOwnPropertyNames():返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
语法:Object.getOwnPropertyName(obj)
obj为一个对象,其自身的可枚举和不可枚举属性的名称被返回。
// 数组
const arr = ['a', 'b', 'c'];
console.log(Object.getOwnPropertyNames(arr).sort()); // ['0','1','2','length']
// 类数组对象
const obj = {0: 'a', 1: 'b', 2: 'c'};
console.log(Object.getOwnPropertyNames(obj).sort()); // ['0', '1', '2']
-
Object.getPrototyopeOf():返回指定对象的原型
语法:Object.getPrototypeOf(object)
obj为要返回其原型的对象,返回值为给定对象的原型,如果没有则返回null。
const proto = {};
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
-
Object.is():判断两个值是否为同一个值。
语法:Object.is(value1, value2)
value1为被比较的第一个值,value2为被比较的第二个值。返回值为一个boolean值,表示两个参数是否是同一个值。
Object.is('foo', 'foo'); // true
Object.is(window, window); // true
Object.is('foo', 'bar'); // false
Object.is([], []); // false
var test = { a: 1 };
Object.is(test, test); // true
Object.is(null, null); // true
// 特例,Object.is不做类型转换
Object.is(+0, -0); // false
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true
-
Object.keys():返回一个由一个给定对象的自身可枚举属性组成的数组。
语法:Object.keys(obj)
obj为要返回其枚举自身属性的对象,返回值为所有可枚举属性键值组成的字符串数组。
// 数组
const arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // ['0','1','2']
// 类数组对象
const obj = {0: 'a', 1: 'b', 2: 'c'};
console.log(Object.keys(obj)); // ['0','1','2']
-
Object.seal():封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。
语法:Object.seal(obj)
obj为将要被密封的对象,返回值为密封的对象。属性不可配置的效果是不能添加或删除属性,不能重新定义为访问器属性,但属性的值仍然可以修改,并且不会影响从原型链上继承的属性。
const obj = {
prop: function(){},
foo: 'bar'
};
Object.seal(obj);
obj.foo = 'baz';
console.log(obj); // {foo: "baz", prop: ƒ}
delete obj.foo;
console.log(obj); // {foo: "baz", prop: ƒ}
-
Object.setPrototypeOf():设置一个指定的对象的原型到另一个对象
语法:Object.setPrototypeOf(obj, prototype)
obj为要设置其原型的对象,prototype为该对象的新原型。
// 向一个原型附加一个链
function Mammal() {
this.isMammal = 'yes';
}
-
Object.values():返回一个给定对象自身的所有可枚举属性值的数组。
语法:Object.values(obj)
obj为被返回可枚举属性值的对象,返回值为一个包含对象自身的所有可枚举属性值的数组。
// 数组
const arr = ['a', 'b', 'c'];
console.log(Object.values(arr)); // ['a','b','c']
// 类数组对象
const obj = {0: 'a', 1: 'b', 2: 'c'};
console.log(Object.values(obj)); // ['a','b','c']
// 随机键的类数组对象
const obj = {100: 'a', 2: 'b', 7: 'c'};
console.log(Object.values(obj)); // ['b','c','a']
实例方法
-
hasOwnProperty():返回一个布尔值,指示对象自身属性中是否具有指定的属性。
语法:obj.hasOwnProperty(prop)
prop为要检测的属性的string形式的名称,或者Symbol,返回值为某个对象是否含有指定的属性的布尔值。
// 1.判断属性是否存在
const o = new Object();
o.hasOwnProperty('prop'); // false
o.prop = 'exists';
o.hasOwnProperty('prop'); // true
delete o.prop;
o.hasOwnProperty('prop'); // false
// 2.区别自身属性和继承属性
const o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop'); // true
o.hasOwnProperty('toString'); // false
o.hasOwnProperty('hasOwnProperty'); // false
-
isPrototypeOf():测试一个对象是否存在于另一个对象的原型链上。
语法:prototypeObj.isPrototypeOf(obj)
obj为原型链上搜寻的目标对象,返回值为一个布尔值,表示目标对象是否在另一个对象的原型链上。
function Foo(){};
function Bar(){};
function Baz(){};
Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);
const baz = new Baz();
console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true
-
toString():返回一个表示该对象的字符串
语法:obj.toString()
// object
const o = {a: 1, b: 2};
console.log(o.toString()); // '[object Object]'
// Array
const arr = [1,2,3];
console.log(arr.toString()); // '1,2,3'
-
valueOf():返回指定对象的原始值
语法:obj.valueOf()
// Array:返回数组对象本身
var array = ["ABC", true, 12, -5];
console.log(array.valueOf() === array); // true
// Date:当前时间距1970年1月1日午夜的毫秒数
var date = new Date();
console.log(date.valueOf()); // 1617097249869
// Number:返回数字值
var num = 15;
console.log(num.valueOf()); // 15
// 布尔:返回布尔值true或false
var bool = true;
console.log(bool.valueOf() === bool); // true
// Function:返回函数本身
function foo(){}
console.log( foo.valueOf() === foo ); // true
var foo2 = new Function("x", "y", "return x + y;");
console.log( foo2.valueOf() );
// Object:返回对象本身
var obj = {name: "张三", age: 18};
console.log( obj.valueOf() === obj ); // true
// String:返回字符串值
var str = "http://www.xyz.com";
console.log( str.valueOf() === str ); // true
toString()方法和valueOf()方法以及Symbol.toPrimitive方法
-
valueOf()方法和toString()
js中每个对象都有一个toString()方法和valueOf()方法,其中toString()方法返回一个表示该对象的字符串,valueOf()方法返回该对象的原始值。
不同类型的对象的valueOf()方法的返回值
对象 返回值 Array 返回数组对象本身 Boolean 布尔值 Date 返回的时间是从1970年1月1日午夜开始计时的毫秒数UTC Function 函数本身 Number 数字值 Object 对象本身 String 字符串 Math和Error对象没有valueOf()方法 /* 数组 */ var arr = [1,2,3]; console.log(typeof(arr.toString())); // string console.log(arr.toString()); // '1,2,3' console.log(typeof(arr.valueOf())); // object console.log(arr.valueOf()); // [1,2,3] /* Boolean */ var bool = true; console.log(typeof(bool.toString())); // string console.log(bool.toString()); // 'true' console.log(typeof(bool.valueOf())); // boolean console.log(bool.valueOf()); // true /* Date */ var date = new Date(); console.log(typeof(date.toString())); // string console.log(date.toString()); // 'Sat Sep 19 2020 16:36:32 GMT+0800 (中国标准时间)' console.log(typeof(date.valueOf())); // number console.log(date.valueOf()); // 1600504592998 /* Function */ var fun = function(a){console.log(a)}; console.log(typeof(fun.toString())); // string console.log(fun.toString()); // 'function(a){console.log(a)}' console.log(typeof(fun.valueOf())); // function console.log(fun.valueOf()); // ƒ (a){console.log(a)} /* Number */ var num = 123; console.log(typeof(num.toString())); // string console.log(num.toString()); // '123' console.log(typeof(num.valueOf())); // number console.log(num.valueOf()); // 123 /* Object */ var obj = {a:1}; console.log(typeof(obj.toString())); // string console.log(obj.toString()); // '[object Object]' console.log(typeof(obj.valueOf())); // object console.log(obj.valueOf()); // {a: 1} /* String */ var str = "hello"; console.log(typeof(str.toString())); // string console.log(str.toString()); // 'hello' console.log(typeof(str.valueOf())); // string console.log(str.valueOf()); // 'hello' -
Symbol.toPrimitive
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转化为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
Symbol.toPrimitive被调用时,会接受一个字符串参数hint,表示当前运算的模式,有三种模式:
- number: 数字模式,该场合需要转成数值
- string: 字符串模式,该场合需要转成字符串
- defalut: 默认模式,该场合可以转成数值,也可以转成字符串
对于大多数标准对象,数字模式有以下优先级排序:
- 调用valueOf()方法,如果结果为原始值,则返回。
- 否则,调用tosSring()方法,如果结果为原始值,则返回。
- 如果再无可选值,则抛出错误。
同样,对于大多数标准对象,字符串模式有以下优先级排序:
- 调用toString()方法,如果结果为原始值,则返回。
- 否则,调用valueOf()方法,如果结果为原始值,则返回。
- 如果再无可选值,则抛出错误。
在大多数情况下,标准对象会将默认模式按数字模式处理(除了Date对象,在这种情况下,会将默认模式按字符串模式处理),如果自定义Symbol.toPrimitive方法,则可以覆盖这些默认的强制转换特性。
// 没有Symbol.toPrimitive属性的对象
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // [object Object]
console.log(obj1+""); // [object Object]
// 有Symbol.toPrimitive属性的对象
let obj2 = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 10;
}
if (hint === 'string') {
return 'hello';
}
return true;
}
}
console.log(+obj2); // 10
console.log(`${obj2}`); // hello
console.log(obj2+""); // true
// 有Symbol.toPrimitive属性的对象
let obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
console.log('Number模式');
return 123;
}
if (hint === 'string') {
console.log('String模式');
return 'str';
}
if (hint === 'default') {
console.log('Default模式');
return 'default';
}
}
}
console.log(2*obj); // Number模式 246
console.log(3+obj); // Default模式 3default
console.log(obj+""); // Default模式 default
console.log(String(obj)); // String模式 str
由上总结,一般情况下,+连接运算符传入的参数是default,乘法等算数运算符传入的是number,对于String(str),${str}等情况,传入的参数是string。
当然也可以根据不同模式的优先级顺序重写toString和valueOf方法。
let obj = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
rteturn 2;
}
}
console.log(1+obj); // 3
console.log('1'+obj); // 12
可以看到,Symbol.toPrimitive方法在转换基本类型的时候优先级最高。
Object.create与new的区别
我们知道Object.create和new都可以创建一个对象,那他们所创建的对象的差别在哪呢?
首先我们知道new创建一个实例对象的步骤是:1.新建一个新对象并将隐式原型指向构造函数的原型;2.将构造函数的作用域赋值给新对象;3.执行构造函数的代码;4.返回新对象。可以看出通过new创建的对象其原型对象为Object.prototype。而Object.create()创建新对象时,指定了新对象的原型为第一个参数。Object.create()用第二个参数来创建非空对象的属性描述默认为false,而构造函数或字面量创建的对象属性描述默认为true。
// js实现一个Object.create
function myCreate(obj, propertiesObject) {
function F(){}
F.prototype = obj;
const newObj = new F();
if(propertiesObject){
Object.defineProperties(newObj, propertiesObject);
}
return newObj;
}
const hh = myCreate({a: 11}, {mm: {value: 10}});
console.log(hh);
原型与原型链
// js实现一个Object.create
function myCreate(obj, propertiesObject) {
function F(){}
F.prototype = obj;
const newObj = new F();
if(propertiesObject){
Object.defineProperties(newObj, propertiesObject);
}
return newObj;
}
const hh = myCreate({a: 11}, {mm: {value: 10}});
console.log(hh);
原型与原型链
我们知道在js中一切皆对象,而对象又分为普通对象和函数对象,所有的函数对象在创建后都有一个prorotype属性,这个属性指向函数的原型对象,原型对象默认拥有一个constructor属性,指向了它的那个构造函数。普通对象虽然没有prototype属性,但有__ proto __属性,指向了该对象隐藏的属性[[prototype]],即创建该对象的构造函数的原型对象。
function Dog(name, color) {
this.name = name;
this.color = color;
}
const dog1 = new Dog('dog1','black');
console.log(Dog.prototype === dog1.__proto__); // true
由上可以知道,所有的构造函数都拥有一个prototype属性,指向其原型对象,而所有的实例对象都具有一个__ proto __ 属性,指向了创建该实例对象的构造函数的原型对象,这样就形成了一个链,所有的对象之间通过__ proto __ 连接起来,也就是原型链。当搜索当前的对象不存在的属性时,会通过原型链层层往上找,直到最上层Object对象。需要注意的是,所有的函数都能通过__ proto __ 找到Function构造函数的原型,所有的对象都能通过__ proto __ 找到Object构造函数的原型,而Object构造函数原型对象的 __ proto __指向null。
function Foo(){}
Foo.prototype.x = 10;
Foo.prototype.calculate = function() { return this.x + this.y;}
const b = new Foo();
b.y = 20;
const c = new Foo();
c.y = 30;
console.log(b.calculate()); // 30
console.log(c.calculate()); // 40
对象的浅拷贝与深拷贝
浅拷贝
浅拷贝是指只复制一层对象,当对象的属性是引用类型时,实质复制的是其引用
function shallowClone(initalObj){
var obj={};
for(var i in initalObj){
obj[i]=initalObj[i];
}
return obj;
}
Es6的方法
let newObj=Object.assign({},obj);
ES6的对象扩展
let newObj={...obj}
深拷贝
将一个对象从内存中拷贝一份,从堆内存中开辟一个新的区域存放新对象,且改变新对象不会影响原对象
基础版
function deepClone(initalObj){
if(typeof initalObj ==='object'){
let newObj={};
for(var i in initalObj){
newObj[i]=deepClone(initalObj[i]);
}
return newObj;
}else{
return initalObj;
}
}
考虑数组
function deepClone(initalObj){
if(typeof initalObj ==='object'){
//let newObj=Object.prototype.toString.call(inital)==='[Object Array]'
let newObj=Array.isArray(initalObj)?[]:{};
for(var i in initalObj){
newObj[i]=deepClone(initalObj[i]);
}
return newObj;
}else{
return initalObj;
}
}
循环利用
循环利用会因为递归导致陷入死循环,解决循环利用死循环的问题,可以开辟一个新的内存来存储当前对象与拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝这个对象,如果有直接返回,如果没有继续拷贝
Map结构的键可以是任意值,包括函数,对象等引用类型。
function deepClone(initalObj,map=new Map()){
if(typeof initalObj==='object'){
let newObj=Array.isArray(initalObj)?[]:{};
if(map.get(initalObj)){
return map.get(initalObj);
}
map.set(initalObj,newObj);
for(const i in initalObj){
newObj[i]=deepClone(initalObj[i]);
}
return newObj;
}else{
return initalObj;
}
}