js对象总结
- js对象
- 一、理解对象
- 二、定义,创建
- 三、增
- 四、删
- 三、改
- 五、查
- 六、遍历
- 七、合并
- 八、其他
- 1.冻结对象[`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
- 2.返回新对象[`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)
- 3.返回`prototype`指定对象的[`Object.getPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf)
- 4.[`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
- 5.[`Object.isExtensible()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible)
- 6.[`Object.isFrozen()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen)
- 7.[`Object.isSealed()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed)
- 8.[`Object.preventExtensions()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions)
- 9.[`Object.setPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)
js对象
一、理解对象
1.理解对象
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如之后的创建
2.属性的类型
- 数据属性
要修改属性的默认特性,就必须使用 Object.defineProperty()
方法
- 访问器属性
// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
3.属性值简写
为此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同名的属性键。如果没有找到同名变量,则会抛出 ReferenceError。
let name = 'Matt';
let person = {
name
};
console.log(person); // { name: 'Matt' }
4.可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。换句话说,不能在对象字面量中直接动态命名属性。比如:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为 JavaScript 表达式而不是字符串来求值:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。
5.简写方法名
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
新的简写方法的语法遵循同样的模式,但开发者要放弃给函数表达式命名(不过给作为方法的函数命名通常没什么用)。相应地,这样也可以明显缩短方法声明。
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
简写方法名对获取函数和设置函数也是适用的:
let person = {
name_: '',
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
}
};
person.name = 'Matt';
person.sayName(); // My name is Matt
简写方法名与可计算属性键相互兼容:
const methodKey = 'sayName';
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt
6.对象解构
ECMAScript 6
新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
// 不使用对象解构
let person = {
name: 'Matt',
age: 27
};
let personName = person.name, personAge = person.age;
console.log(personName); // Matt
console.log(personAge); // 27
然后,是使用对象解构的:
// 使用对象解构
let person = {
name: 'Matt',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。如果想让变量直接使用属性的名称,那么可以使用简写语法,比如:
let person = {
name: 'Matt',
age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined:
let person = {
name: 'Matt',
age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
也可以在解构赋值的同时定义默认值,这适用于前面刚提到的引用的属性不存在于源对象中的
情况:
let person = {
name: 'Matt',
age: 27
};
let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer
解构在内部使用函数 ToObject()
(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()
的定义)null和 undefined 不能被解构,否则会抛出错误。
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:
let personName, personAge;
let person = {
name: 'Matt',
age: 27
};
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给 personCopy,所以修改
// person.job 对象的属性也会影响 personCopy
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer
在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样:
let person = {
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
- 部分解构
需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:
let person = {
name: 'Matt',
age: 27
};
let personName, personBar, personAge;
try {
// person.foo 是 undefined,因此会抛出错误
({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined
7.参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在
函数签名中声明在函数体内使用局部变量:
let person = {
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
二、定义,创建
对象是JavaScript的一个基本数据类型,是一种复合值,它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。即属性的无序集合。
1.工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。(本书后面还会讨论其他设计模式及其在 JavaScript 中的实现。)下面的例子展示了一种按照特定接口创建对象的方式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
2.构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在这个例子中,Person()构造函数代替了createPerson()
工厂函数。实际上,Person()内部的代码跟 createPerson()
基本是一样的,只是有如下区别。
-
没有显式地创建对象。
-
属性和方法直接赋值给了 this。
-
没有 return。
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
上一个例子的最后,person1 和 person2 分别保存着 Person 的不同实例。这两个对象都有一个constructor 属性指向 Person,如下所示:
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
constructor 本来是用于标识对象类型的。不过,一般认为 instanceof
操作符是确定对象类型更可靠的方式。前面例子中的每个对象都是 Object 的实例,同时也是 Person 的实例,如下面调用instanceof
操作符的结果所示:
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处
let Person = function(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数:
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。比如,前面的例子中定义的 Person()可以像下面这样调用:
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到 window 对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
这样理解这个构造函数可以更清楚地知道,每个 Person 实例都会有自己的 Function 实例用于显示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,
要解决这个问题,可以把函数定义转移到构造函数外部
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在这里,sayName()
被定义在了构造函数外部。在构造函数内部,sayName
属性等于全局 sayName()
函数。因为这一次 sayName
属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2
共享了定义在全局作用域上的 sayName()
函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。
3.原型模式
原型对象:共享方法
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
使用函数表达式也可以:
let Person = function() {};
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
1.理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。对前面的例子而言,Person.prototype.constructor 指向 Person
。然后,因构造函数而异,可能会给原型对象添加其他属性和方法。
在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承Object。
每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome会在每个对象上暴露__proto__
属性,通过这个属性可以访问对象的原型。
关键:关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
2.isPrototypeOf()
虽然不是所有实现都对外暴露了[[Prototype]],但可以使用isPrototypeOf()
方法确定两个对象之间的这种关系。本质上,isPrototypeOf()
会在传入参数的[[Prototype]]指向调用它的对象时返回 true,如下所示:
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
这里通过原型对象调用 isPrototypeOf()
方法检查了 person1 和 person2
。因为这两个例子内部都有链接指向 Person.prototype
,所以结果都返回 true。
3.getPrototypeOf()
返回参数的内部特性[[Prototype]]的值。例如:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
4.setPrototypeOf()
可以向实例的私有特性[[Prototype]]写入一个新值。这样就可以重写一个对象的原型继承关系:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
警告
Object.setPrototypeOf()
可能会严重影响代码性能。Mozilla 文档说得很清楚:“在所有浏览器和 JavaScript 引擎中,修改继承关系的影响都是微妙且深远的。这种影响并不仅是执行Object.setPrototypeOf()
语句那么简单,而是会涉及所有访问了那些修改过[[Prototype]]的对象的代码。”
为避免使用 Object.setPrototypeOf()
可能造成的性能下降,可以通过 Object.create()
来创建一个新对象,同时为其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
5.原型层级
前面提到的 constructor 属性只存在于原型对象,因此通过实例对象也是可以访问到的。
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值
6.原型和in操作
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上。来看下面的例子:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
console.log("name" in person1); // true
console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false
console.log("name" in person2); // true
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
在上面整个例子中,name 随时可以通过实例或通过原型访问到。因此,调用"name" in persoon1
时始终返回 true,无论这个属性是否在实例上。如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用 hasOwnProperty()
和 in 操作符:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
只要通过对象可以访问,in 操作符就返回 true,而hasOwnProperty()
只有属性存在于实例上时才返回 true。因此,只要 in 操作符返回 true 且 hasOwnProperty()
返回 false,就说明该属性是一个原型属性。来看下面的例子
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person = new Person();
console.log(hasPrototypeProperty(person, "name")); // true
person.name = "Greg";
console.log(hasPrototypeProperty(person, "name")); // false
在这里,name 属性首先只存在于原型上,所以 hasPrototypeProperty()
返回 true。而在实例上重写这个属性后,实例上也有了这个属性,因此 hasPrototypeProperty()
返回 false。即便此时原型对象还有 name 属性,但因为实例上的属性遮蔽了它,所以不会用到。
4.对象迭代
1.constructor
指向问题
有读者可能注意到了,在前面的例子中,每次定义一个属性或方法都会把Person.prototype
重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法,如下面的例子所示:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
在这个例子中,Person.prototype
被设置为等于一个通过对象字面量创建的新对象。最终结果是一样的,只有一个问题:这样重写之后,Person.prototype
的 constructor 属性就不指向 Person了。在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。虽然 instanceof
操作符还能可靠地返回值,但我们不能再依靠 constructor 属性来识别类型了,如下面的例子所示:
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
这里,instanceof
仍然对Object和Person都返回true。但constructor属性现在等于Object而不是 Person 了。如果 constructor 的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值:
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
这次的代码中特意包含了 constructor 属性,并将它设置为 Person,保证了这个属性仍然包含恰当的值。但要注意,以这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的。因此,如果你使用的是兼容 ECMAScript
的JavaScript 引擎,那可能会改为使用 Object.defineProperty()
方法来定义 constructor 属性:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
2.原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。下面是一个例子:
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
friend.sayHi(); // "hi",没问题!
以上代码先创建一个 Person 实例并保存在 friend 中。然后一条语句在 Person.prototype
上添加了一个名为 sayHi()
的方法。虽然 friend 实例是在添加方法之前创建的,但它仍然可以访问这个方法。之所以会这样,主要原因是实例与原型之间松散的联系。在调用 friend.sayHi()
时,首先会从这个实例中搜索名为 sayHi
的属性。在没有找到的情况下,运行时会继续搜索原型对象。因为实例和原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到 sayHi
属性并返回这个属性保存的函数。
虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两回事。实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针。来看下面的例子:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); // 错误
在这个例子中,Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()
的时候,会导致错误。这是因为 firend
指向的原型还是最初的原型,而这个原型上并没有 sayName
属性。
3.原生对象原型
原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法数组实例的 sort()方法就是 Array.prototype
上定义的,而字符串包装对象的 substring()方法也是在 String.prototype
上定义的,如下所示:
console.log(typeof Array.prototype.sort); // "function"
console.log(typeof String.prototype.substring); // "function"
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。比如,下面的代码就给 String原始值包装类型的实例添加了一个 startsWith()
方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
4.原型的问题
原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。来看下面的例子:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
这里,Person.prototype
有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个 Person 的实例。person1.friends
通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype
而非 person1
上,新加的这个字符串也会在(指向同一个数组的)person2.friends
上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
1、对象直接量/字面量:
var obj = {
name: 'zsn',
age: 18
}
console.log(obj.name);//zsn
2、构造函数:
5.object.create()
Object.create(原型)
; 用指定的原型对象和属性创建一个新对象。
关于此方法的一些事项:
(1)、若传参为Object.prototype
,则创建的原型为Object.prototype
,和 new Object()
创建的对象是一样的
Object.create(Object.prototype) <==> new Object();
(2)、若传参为空 或者 null,则创建的对象是没有原型的, 导致该对象是无法用document.write()
打印会报错,因为document.write(
)打印的原理是调用Object.prototype.toString()
方法,该对象没有原型,也就没有该方法,所以document.write()
无法打印
由此延伸的知识点: 引用值都也是算作是对象,所以都可以用
document.write()
打印;原始值numebr, boolean, string
都有自己对象的包装类,借助此机制也是可以用document.write()
打印出的;但undefined 和 null既不是引用值,也没有对应的包装类,所以应该无法打印的,但大家会发现这两个值也是可是用document.write()
打印的,因为这两个值被设定为特殊值,document.write()
打印其是不用调用任何方法的,而是之直接打印其值。
构造函数:
1.系统自带的, ps: new Object(), Array(), Number(), Boolean(), Date()...
var obj = new Object();
obj.name = 'zsn';
console.log(obj.name);
2.自定义的: 为了和普通函数区分,首字母大写,采用大驼峰写法;
function Obj(name){
this.name = name;
this.age = 18
}
var obj = new Obj('zsn');
console.log(obj.name);//zsn
console.log(obj.age);//18
自定义构造函数的基本构造原理:
关键是有没有new这个操作符,不用new,Obj(‘zsn’)就是一个函数的正常执行,没有返回值,则默认返回undefined;
而是用new操作符后js引擎就会将该函数看作构造函数看待,返回值就是一个对象了。demo如下:
function Obj(){
this.age = 18;
}
//不用new
console.log(Obj());//undefined
//用new
console.log(new Obj());//Obj {age: 18}
用new和不用new不同的原因:
不用new,函数内的this指向的是window,所以this.xxx
定义的变量都是window上的属性,但为什么使用new后其中的this就不是window对象了呢?
那是因为用new后,js引擎会在函数上进行两步隐式操作(假设构造函数名为Person):
第一步, var this = Object.create(Peson.prototype);
(也是创建对象的一种方法,下边会讲到) 隐式的改变函数内this的含义,现在函数内的this是一个原型为Person.prototype
, 构造函数为Person的对象(其实此过程就将想要的对象基本创造成功了,只是差些属性而已,从此可是看出构造函数创建对象的最根本原理是借用Object.create()
方法来实现的,只不过被封装功能化了);
第二步, 在创建的对象设置完所需要的属性后,隐式的将创建的对象this通过return返回 return this;
通过代码的展现:
构造函数得原型:
Person.prototype = {
say: function(){
console.log('I am saying');
}
}
// 构造函数
function Person(){
// 隐士操作
//var this = Object.create(Person.prototype);
//返回对象属性的设置
this.name = "zsn";
this.age = 18
// 隐士操作
//return this;
}
var person1 = new Person();
console.log(person1.name); //zsn
person1.say(); //I am saying
上述两步理论的验证:
第一步:现在函数内的this是一个原型为Person. prototype, 构造函数为Person的对象
// 构造函数的原型
Person.prototype = {
say: function (){
console.log('I am saying');
}
}
// 构造函数
function Person(){
this.name ='zsn';
this.age = 18;
// 打印this对象的原型
console.log(this.__proto__);
// 验证this是否是Person构造函数的实例
console.log(this instanceof Person);//true
}
new Person();//打印结果如下
// Object say: ()__proto__: Object
// true
Person();//打印结果如下
// Window
// false
第二步:隐士的将创建的对象this通过return返回
// 构造函数的原型
Person.prototype = {
say: function (){
console.log('I am saying');
}
}
// 构造函数
function Person(){
var that = Object.create(Person.prototype);
that.name ='zsn';
that.age = 18;
return that;
//提前返回that导致return this无法执行而失效
}
var person = new Person();
//此处不用new也是可以成功返回一个满足条件的对象,因为显示的返回了that
console.log(person.name); //zsn
person.say();//I am saying
关于显示返回that的问题,当我们用new生成对象,若我们显示return的是一个对象 / 引用值,则会导致return this失效,若返回的是原始值,则return this不会失效
三、增
1.简单增加
字面量直接添加
在对象直接量中,属性名与属性值之间通过冒号分隔,冒号左侧是属性名,右侧是属性值,名值对(属性)之间通过逗号分隔。
var obj = {
x : 1,
y : function () {
return this.x + this.x;
}
}
方式一:在定义对象时,直接添加属性和方法
function Person(name,age) {
this.name = name;
this.age = age;
this.say = function() {
alert(name + ':::' + age);
}
}
var person = new Person('张三', 24);
person.say();
方式二:通过"对象.属性名"的方式添加
function Person() {}
var person = new Person();
person.name = '张三';
person.say = function() {alert(this.name)};
person.say();
方式三:通过prototype(原型)属性添加
function Person() {}
var person = new Person();
Person.prototype.name = '张三';
Person.prototype.say = function() {alert(this.name)};
person.say();
2.增加属性
1.Object.defineProperty()
使用
Object.defineProperty()
函数可以为对象添加属性,或者修改现有属性。如果指定的属性名在对象中不存在,则执行添加操作;如果在对象中存在同名属性,则执行修改操作。
语法:
Object.defineProperty(object, propertyname, descriptor);
参数说明如下:
- object:指定要添加或修改属性的对象,可以是 JavaScript 对象或者 DOM 对象。
- propertyname:表示属性名的字符串。
- descriptor:定义属性的描述符,包括对数据属性或访问器属性。
返回值:Object.defineProperty
返回值为已修改的对象。
示例3
下面示例先定义一个对象直接量 obj,然后使用 Object.defineProperty() 函数为 obj 对象定义属性,属性名为 x,值为 1,可写、可枚举、可修改特性。
var obj = {};
Object.defineProperty(obj, "x", {
value : 1,
writable : true,
enumerable : true,
configurable : true
});
console.log(obj.x); //1
2.Object.defineProperties
使用 Object.defineProperties()
函数可以一次定义多个属性。具体用法如下:
object.defineProperties(object, descriptors);
参数说明如下:
- object:对其添加或修改属性的对象,可以是本地对象或 DOM 对象。
- descriptors:包含一个或多个描述符对象,每个描述符对象描述一个数据属性或访问器属性。
示例
在下面示例中,使用 Object.defineProperties()
函数将数据属性和访问器属性添加到对象 obj 上。
var obj = {};
Object.defineProperties(obj, {
x : { //定义属性x
value : 1,
writable : true, //可写
},
y : { //定义属性y
set : function (x) { //设置访问器属性
this.x = x; //改写obj对象的x属性的值
},
get : function () { //设置访问器
return this.x;
},
}
});
obj.y = 10;
console.log(obj.x); //10
四、删
1. delete
使用 delete 运算符可以删除对象的属性。但它的工作比其“替代”设置慢100倍 object[key] = undefined
var obj = {x : 1}; //定义对象
delete obj.x; //删除对象的属性x
console.log(obj.x); //返回undefined
当删除对象属性之后,不是将该属性值设置为 undefined,而是从对象中彻底清除属性。如果使用 for/in 语句枚举对象属性,只能枚举属性值为 undefined 的属性,但不会枚举已删除属性。
2. 设置为undefined
这个选择不是这个问题的正确答案!但是,如果你小心使用它,你可以大大加快一些算法。如果您delete在循环中使用并且在性能方面存在问题,请阅读详细解释
var obj = {
field: 1
};
obj.field = undefined;
可以删除其他东西吗
1.变量
var name ='zs' //已声明的变量
delete name //false
console.log(typeof name) //String
age = 19 //未声明的变量
delete age //true
typeof age //undefined
this.val = 'fds' //window下的变量
delete this.val //true
console.log(typeof this.val) //undefined
已声明的变量windows下的变量可以删除, 未声明的变量不可删除
2.函数
var fn = function(){} //已声明的函数
delete fn //false
console.log(typeof fn) //function
fn = function(){} //未声明的函数
delete fn //true
console.log(typeof fn) //undefined
3.数组
var arr = ['1','2','3'] ///已声明的数组
delete arr //false
console.log(typeof arr) //object
arr = ['1','2','3'] //未声明的数组
delete arr //true
console.log(typeof arr) //undefined
var arr = ['1','2','3'] //已声明的数组
delete arr[1] //true
console.log(arr) //['1','empty','3']
4.对象
var person = {
height: 180,
long: 180,
weight: 180,
hobby: {
ball: 'good',
music: 'nice'
}
}
delete person ///false
console.log(typeof person) //object
var person = {
height: 180,
long: 180,
weight: 180,
hobby: {
ball: 'good',
music: 'nice'
}
}
delete person.hobby ///true
console.log(typeof person.hobby) //undefined
三、改
var obj = {
name : "zsn"
};
console.log(obj.name); //zsn
obj.name = 'obj';
console.log(obj.name); //obj
const json = JSON.parse(JSON.stringify(options).replace(/name/g,"label"));
注:
1、options是需要更改属性的对象
2、replace(/name/g,"label") ,将对象里所有属性为name的都修改成label
五、查
1. 使用点语法
var obj = { //定义对象
x : 1
}
console.log(obj.x); //访问对象属性x,返回1
obj.x = 2; //重写属性值
console.log(obj.x); //访问对象属性x,返回2
2. 使用中括号语法
console.log(obj["x"]); //2
obj["x"] = 3; //重写属性值
console.log(obj["x"]); //3
【注意事项】
- 在中括号语法中,必须以字符串形式指定属性名,不能使用标识符。
- 中括号内可以使用字符串,也可以使用字符型表达式,即只要表达式的值为字符串即可。
3.Object.getOwnPropertyNames
使用 Object.getOwnPropertyNames()
函数能够返回指定对象私有属性的名称。私有属性是指用户在本地定义的属性,而不是继承的原型属性。具体用法如下:
语法:Object.getOwnPropertyNames(object);
参数: object 表示一个对象,
返回值: 为一个数组,其中包含所有私有属性的名称。其中包括可枚举的和不可枚举的属性和方法的名称。如果仅返回可枚举的属性和方法的名称,应该使用 Object.keys()
函数。
var obj = {x : 1, y : 2, z : 3};
var arr = Object.getOwnPropertyNames(obj);
console.log(arr); //返回属性名:x,yz
4.Object.keys
使用 Object.keys()
函数仅能获取可枚举的私有属性名称。具体用法如下:
Object.keys(object);
参数 object 表示指定的对象,可以是 JavaScript 对象或 DOM 对象。返回值是一个数组,其中包含对象的可枚举属性名称。
5.Object.getOwnPropertyDescriptor
使用 Object.getOwnPropertyDescriptor()
函数能够获取对象属性的描述符。具体用法如下:
Object.getOwnPropertyDescriptor(object, propertyname);
参数 object 表示指定的对象,propertyname
表示属性的名称。返回值为属性的描述符对象。
var obj = {x : 1, y : 2, z : 3}; //定义对象
var des = Object.getOwnPropertyDescriptor(obj, "x"); //获取属性x的数据属性描述符
for (var prop in des) { //遍历属性描述符对象
console.log(prop + ':' + des[prop]); //显示特性值
}
des.writable = false; //重写特性,不允许修改属性
des.value = 100; //重写属性值
Object.defineProperty(obj, "x", des); //使用修改后的数据属性描述符覆盖属性x
var des = Object.getOwnPropertyDescriptor(obj, "x"); //重新获取属性x的数据属性描述符
for (var prop in des) { //遍历属性描述符对象
console.log(prop + ':' + des[prop]); //显示特性值
}
一旦为未命名的属性赋值后,对象就会自动定义该属性的名称,在任何时候和位置为该属性赋值,都不需要定义属性,而只会重新设置它的值。如果读取未定义的属性,则返回值都是 undefined。
6.最原始的 for…in 循环
var keys =[];
for(var i in testObj){
keys.push(i);
}
console.log(keys); // keys ["name", "age", "action"]
六、遍历
1.for in
for in 循环是最基础的遍历对象的方式,它还会得到对象原型链上的属性
// 创建一个对象并指定其原型,bar 为原型上的属性
const obj = Object.create({
bar: 'bar'
})
// foo 为对象自身的属性
obj.foo = 'foo'
for (let key in obj) {
console.log(obj[key]) // foo, bar
}
可以看到对象原型上的属性也被循环出来了
2.hasOwnProperty()
hasOwnProperty()
方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自 Object的,会在属性存在于调用它的对象实例上时返回 true,如下面的例子所示:
方法过滤掉原型链上的属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(obj[key]) // foo
}
}
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false
意 ECMAScript
的 Object.getOwnPropertyDescriptor()
方法只对实例属性有效。要取得原型属性的描述符,就必须直接在原型对象上调用 Object.getOwnPropertyDescriptor()
。
2.Object.keys
Object.keys()
是 ES5 新增的一个对象方法,该方法返回对象自身属性名组成的数组,它会自动过滤掉原型链上的属性,然后可以通过数组的 forEach()
方法来遍历
Object.keys(obj).forEach((key) => {
console.log(obj[key]) // foo
})
3. Object.values()
4. Object.entries()
另外还有Object.values()
方法和Object.entries()
方法,这两方法的作用范围和 Object.keys()
方法类似,因此不再说明
for in 循环和 Object.keys()
方法都不会返回对象的不可枚举属性
如果需要遍历不可枚举的属性,就要用到前面提到的Object.getOwnPropertyNames()
方法了
返回:一个包含[key, value]
给定对象自己的可枚举字符串属性的所有对的数组。
5.Object.getOwnPropertyNames
Object.getOwnPropertyNames()
也是 ES5 新增的一个对象方法,该方法返回对象自身属性名组成的数组,包括不可枚举的属性,也可以通过数组的 forEach
方法来遍历
// 创建一个对象并指定其原型,bar 为原型上的属性
// baz 为对象自身的属性并且不可枚举
const obj = Object.create({
bar: 'bar'
}, {
baz: {
value: 'baz',
enumerable: false
}
})
obj.foo = 'foo'
// 不包括不可枚举的 baz 属性
Object.keys(obj).forEach((key) => {
console.log(obj[key]) // foo
})
// 包括不可枚举的 baz 属性
Object.getOwnPropertyNames(obj).forEach((key) => {
console.log(obj[key]) // baz, foo
})
6.Object.getOwnPropertySymbols
ES2015 新增了 Symbol 数据类型,该类型可以作为对象的键,针对该类型 ES2015 同样新增Object.getOwnPropertySymbols()
方法
Object.getOwnPropertySymbols(obj).forEach((key) => {
console.log(obj[key])
})
什么都没有,因为该对象还没有 Symbol 属性
// 给对象添加一个不可枚举的 Symbol 属性
Object.defineProperties(obj, {
[Symbol('baz')]: {
value: 'Symbol baz',
enumerable: false
}
})
// 给对象添加一个可枚举的 Symbol 属性
obj[Symbol('foo')] = 'Symbol foo'
Object.getOwnPropertySymbols(obj).forEach((key) => {
console.log(obj[key]) // Symbol baz, Symbol foo
})
7.Reflect.ownKeys
Reflect.ownKeys()
方法是 ES2015 新增的静态方法,该方法返回对象自身所有属性名组成的数组,包括不可枚举的属性和 Symbol 属性
Reflect.ownKeys(obj).forEach((key) => {
console.log(obj[key]) // baz, foo, Symbol baz, Symbol foo
})
8.对比
方式 | 基本属性 | 原型链 | 不可枚举 | Symbol |
---|---|---|---|---|
for in | 是 | 是 | 否 | 否 |
Object.keys() | 是 | 否 | 否 | 否 |
Object.getOwnPropertyNames() | 是 | 否 | 是 | 否 |
Object.getOwnPropertySymbols() | 否 | 否 | 是 | 是 |
Reflect.ownKeys() | 是 | 否 | 是 | 是 |
这其中只有 for in 循环会得到对象原型链上的属性,其它方法都只适用于对象自身的属性
ES 语言后续添加的新特性不会对以前的代码产生副作用,比如在 ES2015 之前就存在的 for in 循环,Object.keys()
和 Object.getOwnPropertyNames()
是肯定不会返回 Symbol 属性的
七、合并
1.Object.assign()
将所有可枚举的自身属性的值从一个或多个源对象复制到目标对象。
语法:Object.assign(target,…sources)
当target和sources对象中有相同的key时,在target对象中的值会被后面source对象的值覆盖。
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, target对象自身会被修改
如果想要避免o1被改变,需要这样写:
var obj = Object.assign({},o1,o2,o3);//给一个空对象作为target,这样改变的是空对象
console.log(obj);// { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1}
八、其他
1.冻结对象Object.freeze()
冻结对象。其他代码无法删除或更改其属性。
2.返回新对象Object.fromEntries()
从可迭代的[key, value]
对中返回一个新对象。(这是的反向 Object.entries
)。
3.返回prototype
指定对象的Object.getPrototypeOf()
4.Object.is()
比较两个值是否相同。求所有NaN
值(不同于“抽象相等比较”和“严格相等比较”)。
5.Object.isExtensible()
确定是否允许扩展对象。
6.Object.isFrozen()
确定对象是否冻结。
7.Object.isSealed()
确定对象是否密封。
8.Object.preventExtensions()
防止对象的任何扩展。
9.Object.setPrototypeOf()
设置对象的原型(其内部[[Prototype]]
属性)。