你不懂js系列学习笔记-this与对象原型- 03

第 3 章:对象

原文:You-Dont-Know-JSjavascript

1 语法

对象能够经过两种形式定义:声明(文字)形式和构造形式java

声明(文字)形式:git

var myObj = {
  key: value
  // ...
};
复制代码

构造形式:github

var myObj = new Object();
myObj.key = value;
复制代码

2 类型

这里书上说 JavaScript 有六种主要类型,ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。数组

关于 js 的类型 https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive数据结构

JavaScript 数据类型和数据结构函数

JavaScript 标准内置对象工具

null 自己是基本类型:null 有时会被看成一种对象类型,可是这其实只是语言自己的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"学习

原理是这样的,不一样的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,天然前三位也是 0,因此执行 typeof 时会返回“object”。测试

2.1 内置对象

JavaScript 中还有一些对象子类型,一般被称为内置对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数能够看成构造函数 (由 new 产生的函数调用)来使用,从而能够构造一个对应子类型的新对象

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 对象。

Object.prototype.toString…的用法有个小技巧:https://gist.github.com/Yunkou/67d5da9d05b922479d771d8bcde3308d 判断 js 类型

核心的代码:

Object.prototype.toString.call(obj).slice(8, -1);
复制代码

基本类型值 "I am a string" 不是一个对象,它是一个不可变的基本字面值。为了对它进行操做,好比检查它的长度,访问它的各个独立字符内容等等,都须要一个 String 对象。

幸运的是,在必要的时候语言会自动地将 "string" 基本类型强制转换为 String 对象类型,这意味着你几乎从不须要明确地建立对象。JS 社区的绝大部分人都 强烈推荐 尽量地使用字面形式的值,而非使用构造的对象形式。

考虑下面的代码:

var strPrimitive = "I am a string";

console.log(strPrimitive.length); // 13

console.log(strPrimitive.charAt(3)); // "m"
复制代码

在这两个例子中,咱们在字符串的基本类型上调用属性和方法,引擎会自动地将它强制转换为 String 对象,因此这些属性/方法的访问能够工做。

nullundefined 没有对象包装的形式,仅有它们的基本类型值。相比之下,Date 的值 仅能够 由它们的构造对象形式建立,由于它们没有对应的字面形式。

3 内容

3.1 属性访问

咱们须要使用 . 或 [ ] 操做符。

两种语法的主要区别在于,. 操做符后面须要一个 标识符(Identifier) 兼容的属性名,而 [".."] 语法基本能够接收任何兼容 UTF-8/unicode 的字符串做为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"] 语法访问,由于 Super-Fun! 不是一个合法的 Identifier 属性名。 [".."] 语法能够传变量。

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

var myObject = {};

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

myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
复制代码

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

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

myArray["3"] = "baz";

myArray.length; // 4

myArray[3]; // "baz"
复制代码

3.2 属性描述符(Property Descriptors)

在 ES5 以前,JavaScript 语言没有给出直接的方法,让你的代码能够考察或描述属性性质间的区别,好比属性是否为只读。

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

考虑这段代码:

var myObject = {
  a: 2
};

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

正如你所见,咱们普通的对象属性 a 的属性描述符(称为“数据描述符”,由于它仅持有一个数据值)的内容要比 value2多得多。它还包含另外三个性质:

  1. writable
  2. enumerable
  3. configurable

当咱们建立一个普通属性时,能够看到属性描述符的各类性质的默认值,同时咱们能够用 Object.defineProperty(..) 来添加新属性,或使用指望的性质来修改既存的属性(若是它是 configurable 的!)。

3.2.1 可写性(Writable)

writable 控制着你改变属性值的能力。

考虑这段代码:

var myObject = {};

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

myObject.a = 3;

myObject.a; // 2
复制代码

如你所见,咱们对 value 的修改悄无声息地失败了。若是咱们在 strict mode 下进行尝试,会获得一个错误:

"use strict";

var myObject = {};

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

myObject.a = 3; // TypeError
复制代码

这个 TypeError 告诉咱们,咱们不能改变一个不可写属性。

注意: 咱们一下子就会讨论 getters/setters,可是简单地说,你能够观察到 writable:false 意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时须要扔出一个 TypeError,来和 writable:false 保持一致。

3.2.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,这与 strict mode 无关,若是你试图改变一个不可配置属性的描述符定义,就会发生 TypeError。要当心:如你所看到的,将 configurable 设置为 false一个单向操做,不可撤销!

注意: 这里有一个须要注意的微小例外:即使属性已是 configurable:falsewritable 老是能够没有错误地从 true 改变为 false,但若是已是 false 的话不能变回 true

configurable:false 阻止的另一个事情是使用 delete 操做符移除既存属性的能力。

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 调用(无声地)失败了,由于咱们将 a 属性设置成了不可配置。

delete 仅用于直接从目标对象移除该对象的(能够被移除的)属性。若是一个对象的属性是某个其余对象/函数的最后一个现存的引用,而你 delete 了它,那么这就移除了这个引用,因而如今那个没有被任何地方所引用的对象/函数就能够被做为垃圾回收。可是,将 delete 当作一个像其余语言(如 C/C++)中那样的释放内存工具是 恰当的。delete 仅仅是一个对象属性移除操做 —— 没有更多别的含义。

3.2.3 可枚举性(Enumerable)

咱们将要在这里提到的最后一个描述符性质是 enumerable(还有另外两个,咱们将在一下子讨论 getter/setters 时谈到)。

它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象-属性枚举操做中出现,好比 for..in循环。设置为 false 将会阻止它出如今这样的枚举中,即便它依然彻底是能够访问的。设置为 true 会使它出现。

全部普通的用户定义属性都默认是可 enumerable 的,正如你一般但愿的那样。但若是你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为 enumerable:false

3.3 存在性(Existence)

咱们能够查询一个对象是否拥有特定的属性,而 没必要 取得那个属性的值:

var myObject = {
  a: 2
};

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

myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
复制代码

in 操做符会检查属性是否存在于对象 ,或者是否存在于 [[Prototype]] 链对象遍历的更高层中(详见第五章)。相比之下,hasOwnProperty(..) 仅仅 检查 myObject 是否拥有属性,但 不会 查询 [[Prototype]] 链。咱们会在第五章详细讲解 [[Prototype]] 时,回来讨论这个两个操做重要的不一样。

经过委托到 Object.prototype,全部的普通对象均可以访问 hasOwnProperty(..)(详见第五章)。可是建立一个不连接到 Object.prototype 的对象也是可能的(经过 Object.create(null) —— 详见第五章)。这种状况下,像 myObject.hasOwnProperty(..) 这样的方法调用将会失败。

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

注意: in 操做符看起来像是要检查一个值在容器中的存在性,可是它实际上检查的是属性名的存在性。在使用数组时注意这个区别十分重要,由于咱们会有很强的冲动来进行 4 in [2, 4, 6] 这样的检查,可是这老是不像咱们想象的那样工做。

3.3.1 枚举(Enumeration)

先前,在学习 enumerable 属性描述符性质时,咱们简单地解释了"可枚举性(enumerability)"的含义。如今,让咱们来更加详细地从新讲解它。

var myObject = {};

Object.defineProperty(
  myObject,
  "a",
  // 使 `a` 可枚举,如通常状况
  { enumerable: true, value: 2 }
);

Object.defineProperty(
  myObject,
  "b",
  // 使 `b` 不可枚举
  { enumerable: false, value: 3 }
);

myObject.b; // 3
"b" in myObject; // true
myObject.hasOwnProperty("b"); // true

// .......

for (var k in myObject) {
  console.log(k, myObject[k]);
}
// "a" 2
复制代码

你会注意到,myObject.b 实际上 存在,并且拥有能够访问的值,可是它不出如今 for..in 循环中(然而使人诧异的是,它的 in 操做符的存在性检查经过了)。这是由于 “enumerable” 基本上意味着“若是对象的属性被迭代时会被包含在内”。

注意:for..in 循环实施在数组上可能会给出意外的结果,由于枚举一个数组将不只包含全部的数字下标,还包含全部的可枚举属性。因此一个好主意是:将 for..in 循环 用于对象,而为存储在数组中的值使用传统的 for 循环并用数字索引迭代。

另外一个能够区分可枚举和不可枚举属性的方法是:

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"]
复制代码

propertyIsEnumerable(..) 测试一个给定的属性名是否直 接存 在于对象上,而且是 enumerable:true

Object.keys(..) 返回一个全部可枚举属性的数组,

Object.getOwnPropertyNames(..) 返回一个 全部 属性的数组,不论能不能枚举。

inhasOwnProperty(..) 区别于它们是否查询 [[Prototype]] 链,

Object.keys(..)Object.getOwnPropertyNames(..) 考察直接给定的对象。

(当下)没有与 in 操做符的查询方式(在整个 [[Prototype]] 链上遍历全部的属性,如咱们在第五章解释的)等价的、内建的方法能够获得一个 全部属性 的列表。你能够近似地模拟一个这样的工具:递归地遍历一个对象的 [[Prototype]] 链,在每一层都从 Object.keys(..) 中取得一个列表——仅包含可枚举属性。

3.4 迭代(Iteration)

for..in 循环迭代一个对象上(包括它的 [[Prototype]] 链)全部的可迭代属性。但若是你想要迭代值呢?

在数字索引的数组中,典型的迭代全部的值的办法是使用标准的 for 循环,好比:

var myArray = [1, 2, 3];

for (var i = 0; i < myArray.length; i++) {
  console.log(myArray[i]);
}
// 1 2 3
复制代码

可是这并无迭代全部的值,而是迭代了全部的下标,而后由你使用索引来引用值,好比 myArray[i]

ES5 还为数组加入了几个迭代帮助方法,包括 forEach(..)every(..)、和 some(..)。这些帮助方法的每个都接收一个回调函数,这个函数将施用于数组中的每个元素,仅在如何响应回调的返回值上有所不一样。

forEach(..) 将会迭代数组中全部的值,而且忽略回调的返回值。every(..) 会一直迭代到最后,或者 当回调返回一个 false(或“falsy”)值,而 some(..) 会一直迭代到最后,或者 当回调返回一个 true(或“truthy”)值。

这些在 every(..)some(..) 内部的特殊返回值有些像普通 for 循环中的 break 语句,它们能够在迭代执行到末尾以前将它结束掉。

若是你使用 for..in 循环在一个对象上进行迭代,你也只能间接地获得值,由于它实际上仅仅迭代对象的全部可枚举属性,让你本身手动地去访问属性来获得值。

注意: 与以有序数字的方式(for 循环或其余迭代器)迭代数组的下标比较起来,迭代对象属性的顺序是 不肯定 的,并且可能会因 JS 引擎的不一样而不一样。对于须要跨平台环境保持一致的问题,不要依赖 观察到的顺序,由于这个顺序是不可靠的。

可是若是你想直接迭代值,而不是数组下标(或对象属性)呢?ES6 加入了一个有用的 for..of 循环语法,用来迭代数组(和对象,若是这个对象有定义的迭代器):

var myArray = [1, 2, 3];

for (var v of myArray) {
  console.log(v);
}
// 1
// 2
// 3
复制代码

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

数组拥有内建的 @@iterator,因此正如展现的那样,for..of 对于它们很容易使用。可是让咱们使用内建的 @@iterator 来手动迭代一个数组,来看看它是怎么工做的:

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 }
复制代码

注意: 咱们使用一个 ES6 的 SymbolSymbol.iterator 来取得一个对象的 @@iterator 内部属性。咱们在本章中简单地提到过 Symbol 的语义(见“计算型属性名”),一样的原理也适用于这里。你老是但愿经过 Symbol 名称,而不是它可能持有的特殊的值,来引用这样特殊的属性。另外,尽管这个名称有这样的暗示,但 @@iterator 自己 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!

正如上面的代码段揭示的,迭代器的 next() 调用的返回值是一个 { value: .. , done: .. } 形式的对象,其中 value 是当前迭代的值,而 done 是一个 boolean,表示是否还有更多内容能够迭代。

注意值 3done:false 一块儿返回,猛地一看会有些奇怪。你不得不第四次调用 next()(在前一个代码段的 for..of 循环会自动这样作)来获得 done:true,以使本身知道迭代已经完成。这个怪异之处的缘由超出了咱们要在这里讨论的范围,可是它源自于 ES6 生成器(generator)函数的语义。

虽然数组能够在 for..of 循环中自动迭代,但普通的对象 没有内建的 @@iterator。这种故意省略的缘由要比咱们将在这里解释的更复杂,但通常来讲,为了将来的对象类型,最好不要加入那些可能最终被证实是麻烦的实现。

复习

JS 中的对象拥有字面形式(好比 var a = { .. })和构造形式(好比 var a = new Array(..))。字面形式几乎老是首选,但在某些状况下,构造形式提供更多的构建选项。

许多人声称“Javascript 中的一切都是对象”,这是不对的。对象是六种(或七中,看你从哪一个方面说)基本类型之一。对象有子类型,包括 function,还能够被行为特化,好比 [object Array] 做为内部的标签表示子类型数组。

对象是键/值对的集合。经过 .propName["propName"] 语法,值能够做为属性访问。无论属性何时被访问,引擎实际上会调用内部默认的 [[Get]] 操做(在设置值时调用 [[Put]] 操做),它不只直接在对象上查找属性,在没有找到时还会遍历 [[Prototype]] 链(见第五章)。

属性有一些能够经过属性描述符控制的特定性质,好比 writableconfigurable。另外,对象拥有它的不可变性(它们的属性也有),能够经过使用 Object.preventExtensions(..)Object.seal(..)、和 Object.freeze(..) 来控制几种不一样等级的不可变性。

属性没必要非要包含值 —— 它们也能够是带有 getter/setter 的“访问器属性”。它们也能够是可枚举或不可枚举的,这控制它们是否会在 for..in 这样的循环迭代中出现。

你也可使用 ES6 的 for..of 语法,在数据结构(数组,对象等)中迭代 ,它寻找一个内建或自定义的 @@iterator 对象,这个对象由一个 next() 方法组成,经过这个 next() 方法每次迭代一个数据。

相关文章
相关标签/搜索