语法

对象来自于两种形式:声明(字面)形式,和构造形式。

  1. var myObj = { --> 字面形式
  2. key: value
  3. // ...
  4. };
  5. var myObj = new Object(); --> 构造形式
  6. myObj.key = value;

类型

JS 的六种主要类型(在语言规范中称为“语言类型”)中的一种:

  • string
  • number
  • boolean
  • null
  • undefined
  • object

简单基本类型stringnumberbooleannull、和 undefined)自身 不是 objectnull有时会被当成一个对象类型,但是这种误解源自于一个语言中的 Bug,它使得 typeof null 错误地(而且令人困惑地)返回字符串 "object"。实际上,null 是它自己的基本类型

一个常见的错误论断是“JavaScript中的一切都是对象”。这明显是不对的
**

  • function 是对象的一种子类型(技术上讲,叫做“可调用对象”)
  • 数组也是一种形式的对象,带有特别的行为

内建对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
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

JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。因为必要时候基本类型会被强制转换为对应的内建对象

内容

对象的内容由存储在特定命名的 位置 上的(任意类型的)值组成,我们称这些值为属性

当我们说“内容”时,似乎暗示着这些值 实际上 存储在对象内部,但那只不过是表面现象。引擎会根据自己的实现来存储这些值,而且通常都不是把它们存储在容器对象 内部。在容器内存储的是这些属性的名称,它们像指针(技术上讲,叫 引用(reference))一样指向值存储的地方

var myObject = {
  a: 2
};

myObject.a;   // 2
myObject["a"];  // 2

为了访问 myObject位置 a 的值,我们需要使用 .[ ] 操作符。.a 语法通常称为“属性(property)”访问,而 ["a"] 语法通常称为“键(key)”访问。都是属于“属性访问”

两种语法的主要区别在于,. 操作符后面需要一个 标识符(Identifier) 兼容的属性名,而 [".."] 语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。比如引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"] 语法访问,因为 Super-Fun! 不是一个合法的 Identifier 属性名;还有个好处就是程序可以动态地组建字符串的值来作为属性名。

在对象中,属性名 总是 字符串。如果你使用 string 以外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中常用于索引的数字,所以要小心不要将对象和数组使用的数字搞混了

var myObject = { };

myObject[true] = "foo"; --> 属性名字为 “true”
myObject[3] = "bar"; --> 属性名为 '3'
myObject[myObject] = "baz"; -->属性名为 '[object Object]'

myObject["true"];       // "foo"
myObject["3"];          // "bar"
myObject["[object Object]"];  // "baz"

计算型属性名

ES6 加入了 计算型属性名,在一个字面对象声明的键名称位置,你可以指定一个表达式,用 [ ] 括起来

var prefix = "foo";

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

myObject["foobar"]; // hello
myObject["foobaz"]; // world

计算型属性名 的最常见用法,可能是用于 ES6 的 [Symbol](https://www.yuque.com/tuyong/skill/zcrgia#35850a95)

属性(Property) vs. 方法(Method)

每次你访问一个对象的属性都是一个 属性访问,无论你得到什么类型的值。如果你 恰好 从属性访问中得到一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐含的 this 绑定处通过最终函数调用点已经解释,this又运行时决定而非编写时)

就算声明一个函数表达式作为字面对象的一部分,那个函数都不会地 属于 这个对象 —— 仍然仅仅是同一个函数对象的多个引用罢了

var myObject = {
  foo: function foo() {
    console.log( "foo" );
  }
};

var someFoo = myObject.foo;

someFoo;    // function foo(){..}

myObject.foo; // function foo(){..}

数组

数组也使用 [ ] 访问形式,在存储值的方式和位置上它们的组织更加结构化(虽然仍然在存储值的 类型 上没有限制)。数组采用 数字索引,这意味着值被存储的位置,通常称为 下标,是一个非负整数

但数组也是对象,所以虽然每个索引都是正整数,你还可以在数组上添加属性:

var myArray = [ "foo", 42, "bar" ];

myArray.baz = "baz"; // --> 对这个数组对象新增加一个属性“bar”
myArray.length; // 3    --> 新增加的属性不会影响数组长度!!!
myArray.baz;  // "baz"

如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):

myArray["3"] = "baz";
myArray.length; // 4
myArray[3];   // "baz"  --> 上面的3这个被转换成了一个数字索引,因为改变了数组的长度,并且设置了
索引为3的数组的值

复制对象

var myObject = {
  a: 2,
  b: anotherObject, // 引用,不是拷贝!
  c: anotherArray,  // 又一个引用!
  d: anotherFunction
};

一个 浅拷贝(shallow copy) 会得到一个新对象,它的 a 是值 2 的拷贝,但 bcd 属性仅仅是引用,它们指向被拷贝对象中引用的相同位置。一个 深拷贝(deep copy) 将不仅复制 myObject,还会复制 anotherObjectanotherArray

JSON 安全的对象(也就是,可以被序列化为一个 JSON 字符串,之后还可以被重新解析为拥有相同的结构和值的对象)可以简单地这样 复制

var newObj = JSON.parse( JSON.stringify( someObj ) );

(浅拷贝)ES6 为此任务已经定义了 Object.assign(..)Object.assign(..) 接收 目标 对象作为第一个参数,然后是一个或多个 对象作为后续参数

var newObj = Object.assign( {}, myObject );

然而在 Object.assign(..) 中发生的复制是单纯的 = 式赋值,所以任何在源对象属性的特殊性质(比如 writable)在目标对象上 都不会保留

属性描述符(Property Descriptors)

在 ES5 中,所有的属性都用 属性描述符(Property Descriptors) 来描述。

var myObject = {
  a: 2
};

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

我们普通的对象属性 a 的属性描述符的内容要比 value2 多得多。它还包含另外三个性质:writableenumerable、和 configurable

可以通过如下代码动态定义某个属性的这三个性质

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

Writable 可写性

当值被设置成false时,任意的赋值操作都无效;并且下 strict mode 下会抛出TypeError 错误; writable:false 意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个 TypeError,来和 writable:false 保持一致

myObject.a = 3; --> 赋值无效
myObject.a; // 2

可配置性(Configurable)

configurable 设置为 false一个单向操作,不可撤销!
configurable:false 阻止的另外一个事情是使用 delete 操作符移除既存属性的能力

可枚举性(Enumerable)

这个性质控制着一个属性是否能在特定的对象-属性枚举操作中出现,比如 for..in 循环。设置为 false 将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为 true会使它出现

所有普通的用户定义属性都默认是可 enumerable

  • propertyIsEnumerable(..) 测试一个给定的属性名是否直 接存 在于对象上,并且是 enumerable:true
  • Object.keys(..) 返回一个所有可枚举属性的数组
  • Object.getOwnPropertyNames(..) 返回一个 所有 属性的数组,不论能不能枚举。

不可变性(Immutability)

将属性或对象(有意或无意地)设置为不可改变的
所有 这些方法创建的都是浅不可变性(内部对象引用不受影响)。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的 内容 不会受影响,任然保持可变

对象常量(Object Constant)

通过将 writable:falseconfigurable:false 组合,你可以实质上创建了一个作为对象属性的 常量(不能被改变,重定义或删除),比如:

var myObject = {};

Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
} );

防止扩展(Prevent Extensions)

如果你想防止一个对象被添加新的属性,但另一方面保留其他既存的对象属性,可以调用 Object.preventExtensions(..)

var myObject = {
    a: 2
};
Object.preventExtensions( myObject );

myObject.b = 3;
myObject.b; // undefined

封印(Seal)

Object.seal(..) 创建一个“封印”的对象,这意味着它实质上在当前的对象上调用 Object.preventExtensions(..),同时也将它所有的既存属性标记为 configurable:false

所以,你既不能添加更多的属性,也不能重新配置或删除既存属性(虽然你依然 可以 修改它们的值)。

冻结(Freeze)

Object.freeze(..) 创建一个冻结的对象,这意味着它实质上在当前的对象上调用 Object.seal(..),同时也将它所有的“数据访问”属性设置为 writable:false,所以它们的值不可改变

[[Get]]属性访问

对一个对象进行默认的内建 [[Get]] 操作,会 首先 检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。

然而,如果按照被请求的名称 没能 找到属性,[[Get]] 的算法定义了另一个重要的行为。我们会在第五章来解释 接下来 会发生什么(遍历 [[Prototype]] 链,如果有的话)

通过任何方法都不能找到被请求的属性的值,那么它会返回 undefined

[[Put]]

调用 [[Put]] 时,它根据几个因素表现不同的行为,包括(影响最大的)属性是否已经在对象中存在了。

如果属性存在,[[Put]] 算法将会大致检查:

  1. 这个属性是访问器描述符吗(见下一节”Getters 与 Setters”)?如果是,而且是 setter,就调用 setter。
  2. 这个属性是 writablefalse 数据(属性)描述符吗?如果是,在非 strict mode 下无声地失败,或者在 strict mode 下抛出 TypeError
  3. 否则,像平常一样设置既存属性的值。

Getters 与 Setters

ES5 引入了一个方法来覆盖这些默认操作的一部分,但不是在对象级别而是针对每个属性,就是通过 getters 和 setters。Getter 是实际上调用一个隐藏函数来取得值的属性。Setter 是实际上调用一个隐藏函数来设置值的属性。

当你将一个属性定义为拥有 getter 或 setter 或两者兼备,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对)。对于访问器描述符,它的 valuewritable 性质因没有意义而被忽略,取而代之的是 JS 将会考虑属性的 setget 性质(还有 configurableenumerable

var myObject = {
  // 为 `a` 定义一个 getter
  get a() {
    return 2;
  }
};

Object.defineProperty(
  myObject, // 目标对象
  "b",    // 属性名
  {     // 描述符
    // 为 `b` 定义 getter
    get: function(){ return this.a * 2 }, //this的调用点是在myObject内部,因此可访问 a

    // 确保 `b` 作为对象属性出现
    enumerable: true
  }
);
myObject.a; // 2
myObject.b; // 4

setter的操作

var myObject = {
  // 为 `a` 定义 getter
  get a() {  --> 这里a 就表示属性a名称,且必须是 属性的名称,要么无法重定义属性的set/get成功
    return this._a_; // _a_只是一个代名词而已,可以用任意的名字替换 如 value
  },

  // 为 `a` 定义 setter
  set a(val) {
    this._a_ = val * 2;
  }
};

myObject.a = 2;

myObject.a; // 4

存在性(Existence)

我们可以查询一个对象是否拥有特定的属性,而 不必 取得那个属性的值:

var myObject = {
    a: 2
};

("a" in myObject);                // true
("b" in myObject);                // false

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

in 操作 和 hasOwnProperty 操作的不同

  • in 操作符会检查属性是否存在于对象 ,或者是否存在于 [[Prototype]] 链对象遍历的更高层中
  • hasOwnProperty(..) 仅仅 检查 myObject 是否拥有属性,但 不会 查询 [[Prototype]]

通过委托到 Object.prototype,所有的普通对象都可以访问 hasOwnProperty(..)。但是创建一个不链接到 Object.prototype 的对象也是可能的(通过 Object.create(null) )。这种情况下,像 myObject.hasOwnProperty(..) 这样的方法调用将会失败。

在这种场景下,一个进行这种检查的更健壮的方式是 Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基本的 hasOwnProperty(..) 方法而且使用 明确的 this 绑定 来对我们的 myObject 实施调用这个方法

迭代(Iteration)

ES5 还为数组加入了几个迭代帮助方法,forEach(..) 将会迭代数组中所有的值,并且忽略回调的返回值。every(..) 会一直迭代到最后,或者 当回调返回一个 false(或“falsy”)值,而 some(..) 会一直迭代到最后,或者 当回调返回一个 true(或“truthy”)值。

ES6 加入了一个有用的 for..of 循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器),不关心下标!

for..of 循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator 的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next() 方法,循环迭代的内容就是这些连续的返回值。

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }

通过 Symbol 名称,而不是它可能持有的特殊的值,来引用这样特殊的属性(属性名就是Symbol``.``iterator)。另外,尽管这个名称有这样的暗示,但 @@iterator 本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!

这章与generator有很大的牵扯,需要再次看看