1.JavaScript 对象
js中的对象都是都是内置对象 Object 的实例,创建一个自定义对象最简单的方法就是 new Objedct() 或者 使用对象字面量方式创建,但是在开发工作中一般都使用对象字面量方式创建对象,而且对象是一组无序的数据和方法的集合,这些数据和方法一般称为对象的属性;这些属性在创建是都带有一些特征值,JavaScript 通过这些特征值来定义它们的行为。
// 实例化 Object 对象
var person = new Object();
person.name = 'Alex';
person.age = 20;
person.getName = function() {
return this.name;
}
// 对象字面量创建对象
var person = {
name: 'Alex',
age: 20,
getName: function() {
return this.name;
}
}
2.对象属性的类型
ECMA-262 第 5 版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。ECME-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们,为了表示特性是内部值,该规范把它们放到了两对方括号中,例如 [[configurable]]。
JavaScript 对象属性有两种类型:数据属性和访问器属性
2.1 数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入。数据属性有四个描述其行为的特性值:
- [[configurable]]:表示能否通过 delete 删除属性从而新定义属性;能否修改属性的特性;能否把属性修改为访问器属性。像上面例子那样在对象上直接定义属性时,它们的这个特性默认值为 true。
- [[enumerable]]:表示能否通过 for-in 循环返回属性。像上面例子那样在对象上直接定义属性时,它们的这个特性默认值为 true。
- [[writable]]:表示能否修改属性的值。像上面例子那样在对象上直接定义属性时,它们的这个特性默认值为 true。
- [[value]]:包含这个属性的数据值。读取属性值的时候从这个位置读;写入属性的时候把新值保存在这个位置。这个特性的默认值为 undefined。
直接在对象上定义属性,则属性的[[configurable]]、[[enumerable]]、[[writable]]特性都被设置为 true,[[value]]特性被设置为指定的值。例如:
var person = {
name: 'Alex'
}
此时 person 的 name 属性的 [[value]] 特性被设置为了 'Alex';
要修改属性的默认特性是,只能使用 ECMAScript 5 的 Object.defineProperty() 方法。该方法接受三个参数:属性所在的对象、属性的名称、一个描述符(descirptor)对象。其中描述符对象的属性必须是:configurable, enumerable, writable和value,设置其中一个或多个会修改对应的特性。
var person = {}
Object.defineProperty(person, 'name', {
writable: false,
value: 'Alex'
})
console.log(person.name) // Alex
person.name = 'ABC'
console.log(person.name) // Alex
上面的例子创建了一个 name 属性,设置 此属性的 writable 为 false,则 name 属性是一个只读属性,在非严格模式下,复制操作将会被忽略;在严格模式下,赋值操作将会抛出错误
Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
at <anonymous>:8:13
同样的规则同样适用于可配置属性 configurable,设置 configurable 表示不能从对象中删除属性,如果对这个属性调用 delete,在非严格模式下会忽略该操作。
var person = {}
Object.defineProperty(person, 'name', {
configurable: false,
value: 'Alex'
})
console.log(person.name) // Alex
delete person.name
console.log(person.name) // Alex
在严格模式下会抛出错误:
Uncaught TypeError: Cannot delete property 'name' of #<Object>
at <anonymous>:8:1
一旦将 configurable 配置 false,再调用 Object.defineProperty() 方法修改 name 的特性都会报错(以 chrome 为例):
屏幕快照 2020-03-14 16.16.14.png
事实上,可以多次调用 Object.defineProperty() 多次修改同一个属性,但是当把 configurable 设置为 false 之后就有限制了。
在调用 Object.defineProperty() 创建一个新属性时,如果不指定,configurable、enumerable,writable默认都为false。但是在修改某个已定义的属性时则无此限制。
var person = {}
Object.defineProperty(person, 'name', {
value: 'Alex'
})
// Object.getOwnPropertyDescriptor() 后面会说
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
/*
* value: "Alex"
* writable: false
* enumerable: false
* configurable: false
*/
var person = {
name: 'Alex'
}
// Object.getOwnPropertyDescriptor() 后面会说
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
/*
* value: "Alex"
* writable: true
* enumerable: true
* configurable: true
*/
IE8 是第一个实现 Object.defineProperty() 方法的浏览器版本。然而这个版本存在着诸多限制:只能在 DOM 对象上使用这个方法,而且只能创建访问器属性。由于实现不彻底,建议不要在 IE8 中使用 Object.defineProperty()
2.2 访问器属性
访问器属性不包含数据值;它包含一对 getter 和 setter 函数(这两个函数都不是必须的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数,这个函数负责处理数据。访问器属性也有四个特性:
- [[configurable]]:表示能否通过 delete 删除属性从而新定义属性;能否修改属性的特性;能否把属性修改为数据属性。
- [[enumerable]]:表示能否通过 for-in 循环返回属性。
- [[set]]:写入属性是调用的函数。默认值是 undefined。
- [[get]]:读取属性是调用的函数。默认值是 undefined。
访问器属性不能直接定义,必须使用 Object.defineProperty() 方法
在调用 Object.defineProperty() 创建一个新引用类型属性时,如果不指定,configurable、enumerable默认都为false
var person = {
name: 'Alex',
__age: 20
}
Object.defineProperty(person, 'age', {
get: function() {
return this.__age;
},
set:function(newValue) {
if (newValue >= 70) {
this.__age = newValue;
this.name = 'old Alex';
}
}
});
person.age // 20
person.age = 71
person.age // 71
person.name // old Alex
上面的代码创建了一个对象,并定义了两个默认属性 name 和 __age,__age 前面加下划线是一种常用记号,表明只能通过对象方法访问的属性,而访问器属性 age 包含 getter 和 setter 函数,getter 函数返回 __age 的值,setter 函数计算当年龄大于 70 的时候把 name 修改为 old Alex,__age 修改为写入的值。这个访问器属性的常见方式,即一个属性的值会导致其他属性发生变化。
不一定非要同时指定 getter 和 setter 函数。只指定 getter 意味着属性不能写,尝试写入属性会被忽略,在严格模式下,尝试写入只指定了 getter 的属性会抛出错误。只指定 setter 意味着属性不能读,尝试读取属性会被忽略,只能读到 undefined(以Chrome为例),
只指定 getter 函数:
只指定 setting 函数:
屏幕快照 2020-03-14 17.28.33.png
在不支持 Object.defineProperty() 方法的浏览器中不能修改 [[configurable]] 和 [[enumerable]]
3.定义多个属性
由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.defineProperties() 方法。这个方法可以通过描述符一次定义多个属性。该方法接受两个对象参数:第一个参数是要添加和修改其属性的对象,第二个参数中的对象属性要和第一个参数中要添加和修改的属性一一对应。例如:
var person = {}
Object.defineProperties(person, {
name: {
configurable: true,
enumerable: true,
writable: true,
value: 'Alex'
},
__age: {
configurable: true,
enumerable: true,
writable: true,
value: 20
},
age: {
set: function(newValue) {
if (newValue >= 70) {
this.__age = newValue;
this.name = 'old Alex';
}
},
get: function() {
return this.__age;
}
}
});
属性特性的定义规则和 Object.defineProperty() 方法的规则一致
4.获取属性特性
ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法可以获取给定属性的描述符。这个方法接受两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是数据属性,这个对象的属性有 configurable, enumerable, writable, value; 如果是访问器属性,这个对象的属性有 configurable, enumerable, set, get。例如:
屏幕快照 2020-03-14 17.54.37.png
5. 创建对象
使用 new Object() 和 对象字面量方式都可以用来创建单个对象,但是如果要创建大量的相似的对象那么势必会产生大量的重复代码,为了解决这个问题,开始使用别的方式创建对象。
5.1 工厂模式
工厂模式抽象了创建对象的具体过程,在 ES6 之前 ECMAScript 不能创建类(其实 ES6 中的类本质上还是构造函数),开发人员发明了一种函数,以函数来封装以特定接口创建对象的细节。
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name)
}
return o;
}
var person1 = createPerson('Alex', 20, 'front-end-Engieer');
var person2 = createPerson('John', 40, 'Doctor');
函数 createPerson 可以根据接受的参数构建一个包含所有必要信息的 Person 的对象。可以无数次的调用这个函数,每次它都会返回一个包含三个属性和一个方法的对象。工厂模式虽然解决了创建大量相似对象的问题,但是却没有解决对象识别问题(即怎样知道一个对象的类型),随着 JavaScript 发展,又一个模式出现了。
5.2 构造函数模式
ECMAScript 中的构造函数可以用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在运行环境中。此外,也可以创建自定义构造函数,从而定义自定义对象的属性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name)
}
}
var person1 = new Person('Alex', 20, 'front-end-Engieer');
var person2 = new Person('John', 40, 'Doctor');
在这个例子。Person() 构造函数取代了 createPerson()函数,Person() 构造函数大体与 createPerson() 类似,但也有不同
1.没有显示的创建对象
2.直接将属性和方法赋给了 this 对象
3.没有 return 语句
此外,构造函数的首字母应该大写。按照惯例,构造函数应该始终以大写字母开头,而非构造函数以小写字母开头。这主要是为了区别构造函数和非构造函数,因为构造函数本身也是函数,只不过用来创建对象而已。
要创建 Person 的实例,必须使用 new 操作符。以这种方式调用构造函数实际会经历一下四步:
1.创建一个新对象。
2.将构造函数的作用域赋值给新对象(因此 this 就指向了新对象)。
3.执行构造函数(给新对象添加属性)。
4.返回这个新对象。
在上面的例子中,person1 和 person2 分别保存着一个 Person 的不同实例,这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person:
person1.constructor === Person // true
person2.constructor === Person // true
对象的 constructor 属性最初是用来表示对象类型的。但是,检测对象类型还是 instanceof 操作符更可靠一些。上面的例子中创建的所有对象既是 Object 的实例,也是 Person 的实例:
person1 instanceof Object // true
person1 instanceof Person // true
person2 instanceof Object // true
person2 instanceof Person // true
创建自定义类型构造函数意味着将来可以将它的实例标识为为一种特定的类型,这正式构造函数模式优于工厂模式的地方。
构造函数也是函数,他与普通函数的区别就是调用方式,所以,任何函数通过 new 操作符调用,那它就是构造函数,不通过 new 操作符调用,那它就是普通函数
// 当作构造函数使用
var person = new Person('Alex', 20, 'Engineer');
person.sayName(); // Alex
// 当作普工函数调用
Person('Alex', 20, 'Engineer');
// 在浏览器中顶级执行环境是 window
window.sayName(); // Alex
// 在另一个对象作用域中调用
var o = new Object();
Person.call(o, 'Alex', 20, 'Engineer');
o.sayName(); // Alex
5.2.1 构造函数的问题
构造函数虽然好用,但也有问题,那就是每个方法都要在实例上重新创建一遍,在上面的例子中,person1 和 person2 都有一个 sayName 方法,但是这两个方法不是同一个实例;在 ECMAScript 中函数也是对象,因此每定义一个函数,也就是实例化了一个对象;在使用 new 操作符调用构造函数执行构造函数给新对象添加属性的时候就会实例化一个函数。
person1.sayName === person2.sayName // false
但是,创建两个完成相同任务的 Function 实例实在是没有必要,完全可以通过将函数定义放到构造函数外来解决这个问题:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person('Alex', 20, 'front-end-Engieer');
var person2 = new Person('John', 40, 'Doctor');
这样两个实例就共享了一个 sayName 的 Function 实例,但是这样又回造成全局作用域污染,没有任何封装可言。这个问题可以通过原型模式解决。
5.3 原型模式
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例所共享的属性和方法。
在字面上理解,prototype 就是通过调用构造函数而创建的那个实例的原型对象。
使用原型对象好处就是可以让所有实例对象共享它所包含的熟悉过和方法。
function Person() {}
Person.prototype.name = 'Alex';
Person.prototype.age = 20;
Person.prototype.job = 'Engieer';
Person.prototype.sayName = function () {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // Alex
var person2 = new Person();
person2.sayName(); // Alex
person1.sayName === person2.sayName // true
理解原型对象
当创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象,默认情况下,所有的原型对象都会自动获得一个 constructor (构造函数)属性,这个属性是一个指向 prototype 所在函数的指针。就那前面的例子来说,Person.prototype.conscructor === Person,通过这个构造函数,还可以继续为原型对象添加属性和方法。
创建了自定义构造函数之后,其原型对象默认只会获得 constructor 属性;其他的方法都是从 Object 继承来的。当调用构造函数创建一个实例之后。该实例内部将包含一个指针(内部属性),指向构造函数的原型对象,ECMA-262 第五版 管这个指针叫 [[prototype]],在脚本中没有标准的方法访问该属性,但部分浏览器在每个对象上都支持一个属性 __ proto__;其他实现中这个属性对脚本完全不可见。但是,这里要明确一点这个链接存在与实例与构造函数的原型对象之间,而不是实例与构造函数之间
在实例中定义与原型对象中同名的属性或方法,那么定义在实例中的属性和方法会屏蔽原型对象中的同名属性或方法,即使设置为 null 也同样会屏蔽,想要能访问原型对象重的同名属性或方法,可以使用 delete 操作符删除实例中的同名属性和方法,这样就可以继续访问原型对象中的属性和方法。
但是原型模式也有自己的问题
function Person() {}
Person.prototype.name = 'Alex';
Person.prototype.age = 20;
Person.prototype.job = 'Engieer';
Person.prototype.friends = ['John', 'Court'];
Person.prototype.sayName = function() {
alert(this.name)
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('XXX');
console.log(person2.friends); // ["John", "Court", "XXX"]
上面代码两个实例共享了原型对象上的 friends 属性,但其实我们一般情况下都是希望每个实例能完全拥有自己的属性,这个时候就可以使用组合使用构造函数模式和原型模式。
5.4 组合使用构造函数模式和原型模式
构造函数模式有实例化多个实例会实例化多余的方法的问题,而原型模式有引用类型的原型属性会被所有实例共享的问题,这时候就要用到祝贺构造函数模式和原型模式。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['John', 'Court']
}
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person('Alex', 20, 'Engieer');
var person2 = new Person('Scorrt', 30, 'Doctor');
person1.friends.push('XXX');
console.log(person1.friends); // ["John", "Court", "XXX"]
console.log(person2.friends); // ["John", "Court"]
person1.sayName(); // Alex
person2.sayName(); // Scorrt
这样的话每个实例都完全拥有自己的属性,同时又公用原型对象上的方法。
网友评论