1.属性类型
ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义 的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]。
属性分为两种:数据属性和访问器属性
1.1 数据属性
数据属性包含一个保存数据的位置,值会在这个位置进行读取和写入,有4个特性来描述其行为
- Configurable :表示属性是否可以通过delete来删除并重新定义,是否可以修改其特性,是否可以改为访问器属性,默认情况下该属性的都为true
- Enumerable : 表示属性是否可以通过for-in循环返回,默认情况下所有直接定义在对象上的属性的这个特性都是true
- Writable : 表示属性的值是否可以修改,默认情况下,所有直接定义在对象上的属性的这个特性都是true
- Value : 包含属性实际的值,这个特性默认为undefined
这里Value的特性会设置为”chuan”,在此之后的所有对name的修改都会保存在这个位置let person{
name:"chuan"
}
如果要修改对象的默认特性,需要使用Object.defineProperty()
方法。这个方法接受三个参数:
- 要添加属性的对象
- 属性的名称
- 属性包含的特性,其中由
{}
包含,内可以设置一个或者多个特性
举个栗子
let newObject={};
Object.defineProperty(newObject,'name',{
value:"yuanmou",
writable:false //false为无法修改,true为可修改,默认为true
});
console.log(newObject);
newObject.name="achuan";
console.log(newObject); //"yuanmou"
这个栗子里创建一个只读的name属性,这个属性无法修改,在非严格模式下尝试给此属性赋值会被忽视,而在严格模式下会报错
类似的还有
let person={};
Object.defineProperty(newObject,'name',{
value:"yuanmou",
configurable:false //false为无法删除,true为可修改,默认为true
});
console.log(newObject); //"yuanmou"
delete newObject.name="achuan"; //删除无效
console.log(newObject); //"yuanmou"
设置为false的configurable无法从对象上进行删除,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用 Object.defineProperty()并修改任何非 writable 属性会导致错误:
let person={};
Object.defineProperty(newObject,'name',{
value:"yuanmou",
configurable:false
});
//抛出错误
Object.defineProperty(newObject,'name',{
value:"yuanmou",
configurable:true
});
- 因此,虽然可以对同一个属性多次调用
Object.defineProperty()
,但在把 configurable 设 置为 false 之后就会受限制了 - 。 在调用
Object.defineProperty(
)时,configurable、enumerable 和 writable 的值如果不 指定,则都默认为 false。 - 多数情况下,可能都不需要
Object.defineProperty()
提供的这些强大的设置,但要理解 JavaScript 对象,就要理解这些概念。
1.2 定义多个属性
在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了 Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添 加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。
let book={};
Object.defineProperties(book,{
name:{
value:"JavaScript"
},
edition:{
value:1
}
})
2. 增强对象语法
ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎 的行为,但极大地提升了处理对象的方便程度。
2.1 属性值简写
在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的。例如:
let name='yuanmou';
let person={
name:name
};
console.log(person) // { name: 'yuanmou' }
为此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同 名的属性键。如果没有找到同名变量,则会抛出 ReferenceError。
以下代码和之前的代码是等价的:
let name='yuanmou';
let person={
name
};
console.log(person) // { name: 'yuanmou' }
代码压缩程序会在不同作用域间保留属性名,以防止找不到引用。以下面的代码为例:
function Person(name) {
return {
name
};
}
let person = Person('yuanmou');
console.log(person.name); // Matt
3. 原型模式
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处 是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以 直接赋值给它们的原型,
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
- 这里,所有属性和 sayName()方法都直接添加到了 Person 的 prototype 属性上,构造函数体中 什么也没有。
- 但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。
- 与构造函数模 式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。
- 因此 person1 和 person2 访问的 都是相同的属性和相同的 sayName()函数。
- 要理解这个过程,就必须理解 ECMAScript 中原型的本质。
3.1 理解原型对象
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向 原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。然后,因构造函数而异,可能会给原型对象添加其他属性和方法。
- 在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。
- 每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。
- 脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露proto属性,通过这个属性可以访问对象的原型。
- 在其他实现中,这个特性 完全被隐藏了。关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
/**
* 构造函数可以是函数表达式
* 也可以是函数声明,因此以下两种形式都可以:
* function Person() {}
* let Person = function() {}
*/
function Person() {}
/**
* 声明之后,构造函数就有了一个
* 与之关联的原型对象:
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }
/**
* 如前所述,构造函数有一个 prototype 属性
* 引用其原型对象,而这个原型对象也有一个
* constructor 属性,引用这个构造函数
* 换句话说,两者循环引用:
*/
console.log(Person.prototype.constructor === Person); // true
/**
* 正常的原型链都会终止于 Object 的原型对象
* Object 原型的原型是 null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
/**
* 构造函数、原型对象和实例
* 是 3 个完全不同的对象:
*/
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 实例通过__proto__链接到原型对象,
* 它实际上指向隐藏特性[[Prototype]]
*
* 构造函数通过 prototype 属性链接到原型对象
*
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
/**
* 同一个构造函数创建的两个实例
* 共享同一个原型对象:
*/
console.log(person1.__proto__ === person2.__proto__); // true
/**
* instanceof 检查实例的原型链中
* 是否包含指定构造函数的原型:
*/
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
对象原型
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。proto对象原型和原型对象 prototype 是等价的 proto对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
constructor构造函数
对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:
function Person(uname, age) {
this.uname = uname;
this.age = age;
}
// 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
Person.prototype = {
// 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
constructor: Person, // 手动设置指回原来的构造函数
sayhi: function() {
console.log('who is ');
},
move: function() {
console.log('where');
}
}
var ym = new Person('袁某', 36);
console.log(ym)
3.1 原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对 象所做的修改也会在实例上反映出来。
function Person(){}
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]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同 的对象也不会变。
- 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
- 记住,实例只有指向原型的指针,没有指向构造函数的指针。
- 来看下面的例子:
在这个例子中,Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()的时 候,会导致错误。这是因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。function Person() {} let friend = new Person(); Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }; friend.sayName(); // 错误
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 上反映出来。
- 如果这是有意在多个实例间共享数组,那没什么问题。但一 般来说,不同的实例应该有属于自己的属性副本。
- 这就是实际开发中通常不单独使用原型模式的原因。