《你不知道的JavaScript(上)-原型对象》学习笔记

2018-09-02  本文已影响0人  One_Hund

3.1 语法

var myObj = { 
    key: value
    // ... 
};
var myObj = new Object(); 
myObj.key = value;

3.2 类型

内置对象
var strPrimitive = "I am a string"; 
typeof strPrimitive; // "string" strPrimitive instanceof String; // false
var strObject = new String( "I am a string" ); 
typeof strObject; // "object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]

原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。 如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。
幸好,在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。
例如:

var strPrimitive = "I am a string"; 
console.log( strPrimitive.length ); // 13 
console.log( strPrimitive.charAt( 3 ) ); // "m"

使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这 样做,是因为引擎自动把字面量转换成 String 对象,所以可以访问属性和方法。

3.3 内容(对象的属性)

var myObject = { };

myObject[true] = "foo"; 
myObject[3] = "bar"; 
myObject[myObject] = "baz";

myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
3.3.1 可计算属性名

ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:

var prefix = "foo";
var myObject = {
    [prefix + "bar"]:"hello", 
    [prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world

可计算属性名最常用的场景可能是 ES6 的符号(Symbol)。简单来说,Symbol是一种新的基础数据类型,包含一个不透明且无法预测的值(从技术角度来说就是一个字符串)。一般来说你不会用到符号的实际值(因为理论上来说在不 同的 JavaScript 引擎中值是不同的),所以通常你接触到的是符号的名称,比如 Symbol. Something(这个名字是我编的):

var myObject = {
    [Symbol.Something]: "hello world"
}
3.3.4 复制对象
3.3.5 属性描述符

writable(可写)enumerable(可枚举)configurable(可配置)

var myObject = {      
    a:2 
}; 
 
Object.getOwnPropertyDescriptor( myObject, "a" );  
// { 
//    value: 2, 
//    writable: true, 
//    enumerable: true, 
//    configurable: true 
// }

在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..) 来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。
举例来说:

var myObject = {}; 

Object.defineProperty( myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true 
} );  
 
myObject.a; // 2
1. Writable

writable 决定是否可以修改属性的值。
思考下面的代码:

var myObject = {}; 
 
Object.defineProperty( myObject, "a", {     
    value: 2,     
    writable: false, // 不可写!     
    configurable: true,     
    enumerable: true 
} ); 
 
myObject.a = 3; 
 
myObject.a; // 2

如你所见,我们对于属性值的修改静默失败(silently failed)了。如果在严格模式下,这种方法会出错:

"use strict"; 
 
var myObject = {}; 
 
Object.defineProperty( myObject, "a", {     
    value: 2,     
    writable: false, // 不可写!     
    configurable: true,     
    enumerable: true 
} ); 
 
myObject.a = 3; // TypeError

TypeError 错误表示我们无法修改一个不可写的属性。

2. Configurable

只要属性是可配置的,就可以使用 defineProperty(..) 方法来修改属性描述符

var myObject = {
    a:2
}; 
 
myObject.a = 3; 
myObject.a; // 3 
 
Object.defineProperty( myObject, "a", {     
    value: 4,     
    writable: true,     
    configurable: false, // 不可配置! 
    enumerable: true 
} ); 
 
myObject.a; // 4  
myObject.a = 5;  
myObject.a; // 5 
 
Object.defineProperty( myObject, "a", {     
    value: 6,     
    writable: true,      
    configurable: true,     
    enumerable: true 
} ); // TypeError

最后一个 defineProperty(..) 会产生一个 TypeError 错误,不管是不是处于严格模式,尝 试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成 false 是单向操作,无法撤销!

要注意有一个小小的例外:即便属性是 configurable:false, 我们还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。

除了无法修改,configurable:false 还会禁止删除这个属性:

var myObject = {      
    a:2 
}; 
 
myObject.a; // 2 
 
delete myObject.a;  
myObject.a; // undefined 
 
Object.defineProperty( myObject, "a", {     
    value: 2,     
    writable: true,      
    configurable: false,      
    enumerable: true 
} ); 
 
myObject.a; // 2  
delete myObject.a;  
myObject.a; // 2

如你所见,最后一个 delete 语句(静默)失败了,因为属性是不可配置的。

3. Enumerable

这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。
用户定义的所有的普通属性默认都是 enumerable,这通常就是你想要的。但是如果你不希 望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false。

3.3.6 不变性
1. 对象常量

结合 writable:falseconfigurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除):

var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false 
} );
2. 禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..):

var myObject = { 
    a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined

在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。

3. 密封

Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

4. 冻结

Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..), 然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因 为这样做有可能会在无意中冻结其他(共享)对象。

3.3.7 [[Get]]

在语言规范中,myObject.amyObject 上实际上是实现了 [[Get]] 操作(有点像函数调 用:[[Get]]())。对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。
然而,如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为遍历可能存在的 [[Prototype]] 链, 也就是原型链
如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined

3.3.8 [[Put]]

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。

  1. 属性是否是访问描述符(参见3.3.9节)?如果是并且存在setter就调用setter。
  2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。我们会在第 5 章讨论 [[Prototype]] 时详细进行介绍。

3.3.9 Getter和Setter

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏 函数,会在设置属性值时调用。
当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get(还有 configurable 和 enumerable)特性。

var myObject = {
    // 给 a 定义一个 getter 
    get a() {
        return 2; 
    }
};
Object.defineProperty( 
    myObject, // 目标对象 
    "b", // 属性名
    {   // 描述符
        // 给 b 设置一个 getter
        get: function(){ 
            return this.a * 2 
        },
        // 确保 b 会出现在对象的属性列表中
        enumerable: true
    }
);
myObject.a; // 2
myObject.b; // 4

不管是对象文字语法中的get a(){..},还是 defineProperty(..) 中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的 [Put]操作。通常来说 getter 和 setter 是成对出现的(只定义一个的话 通常会产生意料之外的行为):

var myObject = {
    // 给 a 定义一个 getter 
    get a() {
        return this._a_; 
    },
    // 给 a 定义一个 setter 
    set a(val) {
        this._a_ = val * 2; 
    }
};
myObject.a = 2;
myObject.a; // 4
3.3.10 存在性
1. 枚举
var myObject = { };
 Object.defineProperty(
    myObject,
    "a",
    // 让 a 像普通属性一样可以枚举 
    { enumerable: true, value: 2 }
);
Object.defineProperty(
    myObject,
    "b",
    // 让 b 不可枚举
    { enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

3.4 遍历

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();   // 使用 ES6 中的符号 Symbol.iterator 来获取对象的 @@iterator 内部属 性。
it.next(); // { value:1, done:false } 
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true }

注:和数组不同,普通的对象没有内置的 @@iterator,所以无法自动完成 for..of 遍历。

第4章 混合对象“类”

类、继承、实例化、多态

4.1.1 “类”设计模式
4.2 类的机制
4.3.1 多态

多态是说父类的通用行为可以被子类用更特殊的行为重写。
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。

第5章 原型

5.1 [[Prototype]]
5.1.1 [[Prototype]] 的“尽头”

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype,所以它包含 JavaScript 中许多通用的功能。

5.1.2 属性设置和屏蔽

如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
    有些情况下会隐式产生屏蔽,一定要当心。思考下面的代码:
var anotherObject = { 
    a:2
};

var myObject = Object.create( anotherObject );

anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 隐式屏蔽! 

anotherObject.a; // 2
myObject.a; // 3

myObject.hasOwnProperty( "a" ); // true

尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a,天呐!
修改委托属性时一定要小心。如果想让 anotherObject.a 的值增加,唯一的办法是 anotherObject.a++。

5.2 “类”

5.2.1 “类”函数

所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象:

function Foo() { 
    // ...
}
Foo.prototype; // { }

var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

new Foo() 只是间接完成我们的目标:一个关联到其他对象的新对象。

5.2.2 “构造函数”
function Foo() { 
    // ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
// a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo

Foo.prototype 默认有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数(本例中是 Foo)。此外,我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性,指向 “创建这个对象的函数”。
实际上 a 本身并没有 .constructor 属性。而且,虽然 a.constructor 确实指向 Foo 函数,但是这个属性并不是表示 a 由 Foo“构造”。实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo。a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo,这和“构造”毫无关系。

5.3 (原型)继承

function Foo(name) { 
    this.name = name;
}
Foo.prototype.myName = function() { 
    return this.name;
};
function Bar(name,label) { 
    Foo.call( this, name ); 
    this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype 
Bar.prototype = Object.create( Foo.prototype );
// 注意!现在没有 Bar.prototype.constructor 了 
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function() { 
    return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype )。调用 Object.create(..)凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你指定的对象(本例中是 Foo.prototype)。

// ES6 之前需要抛弃默认的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 开始可以直接修改现有的 Bar.prototype 
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

如果忽略掉 Object.create(..) 方法带来的轻微性能损失(抛弃的对象需要进行垃圾回 收),它实际上比 ES6 及其之后的方法更短而且可读性更高。不过无论如何,这是两种完全不同的语法。

检查“类”关系

思考下面的代码:

function Foo() { 
    // ...
}
Foo.prototype.blah = ...; 
var a = new Foo();
a instanceof Foo; // true

instanceof 回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?
instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。
可惜,这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系。如 果你想判断两个对象(比如 a 和 b)之间是否通过 [[Prototype]] 链关联,只用 instanceof 无法实现。

Foo.prototype.isPrototypeOf( a ); // true

isPrototypeOf(..) 回答的问题是:在 a 的整条 [[Prototype]] 链中是否出现过Foo.prototype ?

Object.getPrototypeOf( a ) === Foo.prototype; // true
a.__proto__ === Foo.prototype; // true

.proto 看起来很像一个属性,但是实际上它更像一个 getter/setter:

Object.defineProperty( Object.prototype, "__proto__", { 
    get: function() {
        return Object.getPrototypeOf( this ); },
    set: function(o) {
        // ES6 中的 setPrototypeOf(..) 
        Object.setPrototypeOf( this, o ); 
        return o;
    } 
} );

5.4 对象关联

[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他
对象。
通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的 引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

5.4.1 创建关联
Object.create(..)
if (!Object.create) { 
    Object.create = function(o) {
    function F(){} 
    F.prototype = o; 
    return new F();
}; }
var anotherObject = { 
    a:2
};
var myObject = Object.create( anotherObject, { 
    b: {
        enumerable: false, 
        writable: true, 
        configurable: false, 
        value: 3
    }, 
    c: {
        enumerable: true, 
        writable: false, 
        configurable: false, 
        value: 4
    } 
});

myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true

myObject.a; // 2
myObject.b; // 3
myObject.c; // 4

5.5 小结

如果要访问对象中并不存在的一个属性,[[Get]] 操作就会查找对象内部[[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
所有普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。toString()、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。
关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤(第 2 章)中会创建一个关联其他对象的新对象。
使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样。
虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但 是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。
出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。
相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。

第6章 行为委托

类模型(面向对象风格)
function Foo(who) { 
    this.me = who;
}
Foo.prototype.identify = function() {
    return "I am " + this.me; 
};
function Bar(who) { 
    Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" ); 
b1.speak();
b2.speak();
委托模型(对象关联风格)
Foo = {
    init: function(who) {
        this.me = who; 
    },
    identify: function() {
        return "I am " + this.me;
    } 
};
Bar = Object.create( Foo );
Bar.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
6.6 小结

在软件架构中你可以选择是否使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式:行为委托。
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。 对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。

上一篇下一篇

猜你喜欢

热点阅读