1.属性类型

ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义 的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]。

属性分为两种:数据属性和访问器属性

1.1 数据属性

数据属性包含一个保存数据的位置,值会在这个位置进行读取和写入,有4个特性来描述其行为

  • Configurable :表示属性是否可以通过delete来删除并重新定义,是否可以修改其特性,是否可以改为访问器属性,默认情况下该属性的都为true
  • Enumerable : 表示属性是否可以通过for-in循环返回,默认情况下所有直接定义在对象上的属性的这个特性都是true
  • Writable : 表示属性的值是否可以修改,默认情况下,所有直接定义在对象上的属性的这个特性都是true
  • Value : 包含属性实际的值,这个特性默认为undefined
    1. let person{
    2. name:"chuan"
    3. }
    这里Value的特性会设置为”chuan”,在此之后的所有对name的修改都会保存在这个位置

如果要修改对象的默认特性,需要使用Object.defineProperty()方法。这个方法接受三个参数:

  1. 要添加属性的对象
  2. 属性的名称
  3. 属性包含的特性,其中由{}包含,内可以设置一个或者多个特性

举个栗子

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

yuanxing.png

对象原型

对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。proto对象原型和原型对象 prototype 是等价的 proto对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

img2.png

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]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同 的对象也不会变。
  • 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
  • 记住,实例只有指向原型的指针,没有指向构造函数的指针。
  • 来看下面的例子:
    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 属性。

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 上反映出来。
  • 如果这是有意在多个实例间共享数组,那没什么问题。但一 般来说,不同的实例应该有属于自己的属性副本。
  • 这就是实际开发中通常不单独使用原型模式的原因。