每一个对象都是基于一个引用类型建立, 这个引用类型能够是原生类型,如 Object 类型
、Array 类型
、Date 类型
、RegExp 类型
、基本包装类型
,也能够是 自定义类型
。javascript
理解对象java
ok,首先咱们来看下用 对象字面量
的方式建立自定义对象:数组
var person = {
name: 'Fly_001',
age: 22,
sayName: function() {
alert(this.name);
}
};
复制代码
其中这些属性在建立的时候都带有一些 特征值
,JavaScript 经过这些 特征值
来定义它们的行为。函数
ECMAScript 定义只有内部才使用的特性,描述了属性的各类特征,为了表示特性是内部值,该规范把它们放在了两对方括号里,例如 [[Enumerable]],而且在 JavaScript 中不能直接访问它们。ui
ECMAScript 中有两种属性,数据属性
和 访问器属性
:this
数据属性spa
数据属性有 4 个描述其行为的特性:prototype
名称 | 描述 |
---|---|
[[Configurable]] | 表示可否经过 delete 删除属性从而从新定义属性,默认为 true。 |
[[Enumerable]] | 表示可否经过 for-in 循环 ♻️ 返回属性,默认为 true。 |
[[Writable]] | 表示可否修改属性的值,默认为 true。 |
[[Value]] | 包含这个属性的数据值。读取和写入属性值都是在这个位置,默认值为 undefined。 |
要修改属性的特性,可以使用 Object.defineProperty()
方法 ( 话说 Vue 就是经过这个方法实现双向数据绑定的 ),该方法接受三个参数:属性所在的对象、属性的名字和一个描述符对象
,其中描述符对象必须是 configurable
、enumerable
、writable
和 value
的一个或多个:3d
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'Fly_001'
});
alert(person.name); // 'Fly_001';
person.name = 'juejin'; // 修改 person 的 name 属性;
alert(person.name); // 'Fly_001';
复制代码
上面代码建立了一个 name 属性,它的值是只读、不可修改的,若是尝试为它指定新值将会被忽略。( 在严格模式下会抛出 Cannot assign to read only property 'name' of object 的错误 )指针
另外要注意的是若是把 configurable
设置为 false,就不能从对象中删除属性,同时也不能再把它变回可配置了( 此时调用 Object.defineProperty() 方法只能修改 writable
特性 )
多数状况下可能用不到 Object.defineProperty() 方法提供的这些高级功能,不过理解这些概念对咱们理解 JavaScript 对象很是有用。
访问器属性
访问器属性包含一对 getter 和 setter 函数,有以下 4 个特性:
特性名 | 描述 |
---|---|
[[Configurable]] | 表示可否经过 delete 删除属性从而从新定义属性、可否修改属性的特性。 |
[[Enumerable]] | 表示可否经过 for-in 循环 ♻️ 返回属性。 |
[[Get]] | 在读取属性时调用的函数,默认值为 undefined。 |
[[Set]] | 在写入属性时调用的函数,默认值为 undefined。 |
一样滴~ 访问器属性不能直接定义,必须使用 Object.defineProperty()
来定义:
var book = {
_year = 2018,
edition: 1
}
Object.defineProperty(book, 'year', {
get: function() {
return this._year;
},
set: function(newYear) {
if (newYear > 2018) {
this._year = newYear;
this.edition += newYear - 2018;
}
}
});
book.year = 2020;
alert(book.edition); // 3;
复制代码
_year 前面的下划线是一种经常使用的记号,表示只能经过对象方法访问的属性。
另外,不必定要同时指定 getter 和 setter。
只指定 getter 意味着属性是不可写🙅,而只指定 setter 意味着不可读 🙅~
定义多个属性
因为为对象定义多个属性的可能性很大,ECMAScript 又定义了一个 Object.defineProperties()
方法,能够一次定义多个属性,这个方法接受两个对象参数:目标对象
和 要添加或修改的属性
:
var book = {};
Object.defineProerties(book, {
_year: {
value: 2018
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set: function(newYear) {
if (newYear > 2018) {
this._year = newYear;
this.edition += newYear - 2018;
}
}
}
});
复制代码
上述代码在 book 上定义了两个数据属性 ( _year 和 edition ) 和一个访问其属性 ( year ),值得一提的是这里的属性都是在同一时间建立的。
读取属性的特性
既然能修改属性的特性,那就应该能获取属性的特性,因此 ECMAScript 又给咱们提供了 Object.getOwnPropertyDescriptior()
方法,该方法接受两个参数:属性所在的对象
和 要读取的属性名称
。
它的返回值是一个对象。
若是是数据属性,则对象的属性有 configurable
、enumerable
、writable
和 value
;
若是是访问器属性,这个对象的属性有 configurable
、enumerable
、get
和 set
。
举个栗子 🌰 :
var descriptior = Object.getOwnPropertyDescriptior(book, '_year');
alert(descriptior.value); // 2018;
alert(descriptior.configurable); // false;
复制代码
Tips: 在 JavaScript 中,能够针对任何对象 —— 包括 DOM
和 BOM
对象,使用 Object.getOwnPropertyDescriptior()
方法。
建立对象
虽然使用 Object 构造函数或对象字面量均可以建立单个对象,但若是使用同一接口建立不少对象就会产生大量的重复代码。
为解决这个问题,咱们可使用工厂模式的一种变体。
工厂模式
工厂模式是用函数来封装 📦 接口建立对象的细节:
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson('Fly_001', 22);
var person1 = createPerson('juejin', 24);
复制代码
工厂模式虽然解决了建立多个类似对象的问题,但却没有解决 对象识别
的问题 ( 即怎样知道一个对象的类型 ),因此又出现了另外一个模式 ~ 构造函数模式
。
构造函数模式
ECMAScript 的构造函数可用来建立特定类型的对象,从而定义对象的属性和方法:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person('Fly_001', 22);
var person1 = new Person('juejin', 24);
复制代码
咱们注意到,Person() 中的代码与前面 createPerson() 不一样之处在于:
没有显示地建立对象
;直接将属性和方法赋给了 this 对象
;没有 return 语句
。Tips: 按照惯例,构造函数始终都应该以一个大写字母开头。
要建立 Person 的新实例,必须使用 new
操做符,以这种方式调用构造函数会经历如下 4 个步骤:
这样,person1 和 person2 分别保存着 Perosn 的一个不一样实例:
alert(person1.constructor == Person); // true;
alert(person2.constructor == Person); // true;
alert(person1 instanceof Person); // true;
alert(person2 instanceof Person); // true;
复制代码
使用构造函数模式能够将它的实例标识为一种特定的类型,这也正是构造函数模式赛过工厂模式的地方。
构造函数模式虽然好用,但它的缺点是每一个方法在每一个实例上都要从新建立一遍,从逻辑角度来说,此时的构造函数也能够这样定义:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = new function('alert(this.name)'); } 复制代码
同时不一样实例上的同名函数是 不相等
的:
alert(person1.sayName == person2.sayName); // false;
复制代码
因此建立两个完成一样的任务的 Function 实例确实没有必要,好在这些问题又能够经过 原型模式
来解决。
原型模式
咱们建立的每一个函数都有一个 prototype ( 原型 ) 属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含能够由特定类型的全部实例共享的属性和方法。
是否是很绕,若是按照字面意思来理解,那么 prototype 就是经过调用构造函数而建立的那个对象实例的原型对象。(好吧,仍是很迷 🤕),那就放码出来吧~
function Person() {}
Person.prototype.name = 'Fly_001';
Person.prototype.age = 22;
Person.prototype.sex = 'male';
Person.property.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // 'Fly_001';
var person2 = new Person();
alert(person1.sayName == person2.sayName); // true;
复制代码
使用原型对象的好处是可让全部对象实例共享它所包含的属性和方法。
与构造函数模式不一样的是,新对象的属性和方法是由全部实例共享的,换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName() 方法。
要理解原型模式的工做原理,就必须先理解 ECMAScript 中 原型对象
的性质。
理解原型对象
不管何时,只要建立了一个新函数,就会根据一组特定的规则为该函数建立一个 prototype 属性,这个属性指向函数的原型对象。在默认状况下,全部原型对象都会自动得到一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。
就拿前面的栗子 🌰 来讲,Person.prototype.constructor 指向 Person,而经过这个构造函数,咱们还可继续为原型对象添加其它属性和方法。
下图展现了各个对象之间的关系:
在此,Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person。
原型对象中除了包含 constructor 属性外,还包括后来添加的其它属性。
Person 的每个实例 —— person1 和 person2 都包含一个内部属性,该属性仅仅指向 Person.prototype, 换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但却能够调用 sayName() 方法,这是经过 查找对象属性的过程 来实现的。
同时咱们能够经过 isPrototypeOf() 方法来肯定对象之间是否存在这种关系:
alert(Person.prototype.isPrototypeOf(person1)); // true;
alert(Person.prototype.isPrototypeOf(person2)); // true;
复制代码
这里由于 person1 和 person2 内部都有一个指向 Person.prototype 的指针,所以都返回了 true。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具备给定的属性名。搜索首先从对象实例自己开始,若是找到对应的属性,则返回属性值并中止搜索;若是没找到,则继续搜索指针指向的原型对象。
也就是说,在咱们调用 person1.sayName() 的时候,会前后执行两次搜索,先从 person1 实例自己上找,没找到后再从 person1 的原型上寻找,最后发现了 sayName() 方法定义并返回。
另外可使用 hasOwnProperty() 方法来检测一个属性是存在于实例中,仍是存在于原型中,只有当属性存在于对象实例中才会返回 true
:
alert(person1.hasOwnProperty('name')); // false;
复制代码
更简单的原型语法
在前面的例子里,每添加一个属性和方法就要敲一遍 Person.prototype,显得有些繁琐,因此更常见的作法是使用对象字面量来进行封装 📦:
function Person() {}
Person.prototype = {
name: 'Fly_001',
age: 22,
sex: 'male',
sayName: function() {
alert(this.name);
}
};
复制代码
在上面的代码中,咱们将 Person.prototype 设置为等于一个以对象字面量形式建立的新对象,最终结果相同,可是有一个例外:constructor 属性再也不指向 Person 了。
前面介绍过,每建立一个函数,就会同时建立它的 prototype 对象,这个对象也会自动得到 constructor 属性。
而咱们刚才的代码,本质上彻底重写了默认的 prototype 对象,所以 constructor 属性也就变成了新对象的 constructor 属性 ( 指向 Object 构造函数 ),再也不指向 Person 函数。
此时,尽管 instanceof 操做符还能返回正确的结果,但经过 constructor 已经没法肯定对象的类型了:
var friend = new Person();
alert(friend instanceof Person); // true;
alert(friend.constructor == Person); //false;
alert(friend.constructor == Object); // true;
复制代码
若是 constructor 的值真的很重要,能够像下面这样特地将它设置回适当的值:
function Person () {};
Person.prototype = {
constructor: Person,
// 设置其它属性和方法;
}
复制代码
但要注意一点,以这种方式重设 constructor 属性会致使它的 [[enumerable]] 特性被设置为 true。而默认状况下,原生的 constructor 属性是不可枚举的,所以我们可使用 Object.defineProperty() 方法:
// 重设构造函数;
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
复制代码
原型的动态性
因为在原型中查找值的过程是一次搜索,所以咱们对原型对象所作的任何修改都可以当即从实例上反映出来 —— 即便是先建立了实例后修改原型也照样如此:
var friend = new Person();
Person.prototype.sayName = function() {
alert('hi');
};
friend.sayName(); // 'hi', 木有问题~
复制代码
其缘由能够归结为实例与原型之间的松散链接关系。
当咱们调用 friend.sayName() 时,首先会在实例中搜索名为 sayName 的方法,在没找到的状况下会继续搜索原型。由于实例与原型之间的链接是一个指针而非副本,所以能够在原型中找到并返回保存在那里的函数。
尽管能够随时为原型添加属性和方法,而且可以当即在全部对象实例中反映出来,但若是是重写整个对象,那么状况就不同了:
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Fly_001',
age: 22,
sex: 'male',
sayName: function() {
alert(this.name);
}
};
friend.sayName(); // error!
复制代码
由于 friend 指向的原型中不包含以该名字命名的属性,下图展现了这个过程的内幕:
原生对象的原型
原型模式的重要性不只体如今建立自定义类型方面,就连全部原生的引用类型,都是采用这种模式建立的。全部原生引用类型 ( Object、Array、String,等等 )都在其构造函数的原型上定义了方法。
例如,在 Array.prototype 中能够找到 sort() 方法,而在 String.prototype 中能够找到 substring() 方法:
alert(typeof Array.prototype.sort); // 'function';
alert(typeof String.prototype.substring); // 'function';
复制代码
经过原生对象的原型,不只能够取得全部默认方法的引用,还能够定义新方法:
String.prototype.startWith = function(text) {
return this.indexOf(text) == 0;
};
var msg = 'Hello World';
alert(msg.startWith('Hello')); // true;
复制代码
上述代码 👆 就给基本包装类型 String 添加了一个 startWith() 方法。既然方法被添加给了 String.prototype,那么当前环境中的全部字符串均可以调用这个方法。
原型对象的问题
原型模式也不是没有缺点,它的最大问题是由其 共享性
的本性致使的。
原型中全部属性是被不少实例共享的,这种共享对于函数很是合适,对于那些包含基本值的属性也说得过去,毕竟经过在实例上添加一个同名属性,能够隐藏原型中的对应属性。
然鹅,对于包含引用类型值的属性来讲,问题就比较突出了:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Fly_001',
age: 22,
friends: ['Jack', 'Tom'],
sayName: function() {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Daniel');
alert(person1.friends); // 'Jack, Tom, Daniel';
alert(person2.friends); // 'Jack, Tom, Daniel';
alert(person1.friends === person2.friends); // true;
复制代码
因为 friends 数组存在于 Person.prototype 而非 person1 中,因此刚刚的修改也会经过 person2.friends 反映出来。
因此正是这个问题,咱们不能单独使用原型模式~
组合使用构造函数模式和原型模式 ( 闪亮登场 ✨ )
建立自定义类型的最多见方式,就是组合使用构造函数模式与原型模式。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
这样,每一个实例都会有本身的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混合模式还支持向构造函数传递参数;可谓是集两种模式之长呀:
funciton Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.friends = ['Jack', 'Tom'];
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
}
var person1 = new Perosn('Fly_001', 22, 'male');
var person2 = new Person('juejin', 24, 'unknown');
person1.friends.push('Daniel');
alert(person1.friends); // 'Jack, Tom, Daniel';
alert(person2.friends); // 'Jack, Tom';
alert(person1.friends === person2.friends); // false;
alert(person1.sayName === person2.sayName); // true;
复制代码
这里修改了 person1.friends 不会影响到 person2.friends,由于它们分别引用了不一样的数组。
这种构造函数与原型混成的模式,是使用最普遍、认同度最高的一种建立自定义类型的方式,能够说,这是用来定义引用类型的一种默认模式。
关于 JS 中对象的一些浅薄知识,就先讲到这里,下一篇会谈谈 JS 中几种继承方式,敬请期待~ ❤️