博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
你不懂js系列学习笔记-this与对象原型- 03
阅读量:6446 次
发布时间:2019-06-23

本文共 10408 字,大约阅读时间需要 34 分钟。

第 3 章:对象

原文:

1 语法

对象可以通过两种形式定义:声明(文字)形式和构造形式

声明(文字)形式:

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

构造形式:

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

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; // falsevar 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); // 13console.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; // 4myArray[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; // 3Object.defineProperty(myObject, "a", {  value: 4,  writable: true,  configurable: false, // 不可配置!  enumerable: true});myObject.a; // 4myObject.a = 5;myObject.a; // 5Object.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; // 2delete myObject.a;myObject.a; // undefinedObject.defineProperty(myObject, "a", {  value: 2,  writable: true,  configurable: false,  enumerable: true});myObject.a; // 2delete 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; // falsemyObject.hasOwnProperty("a"); // truemyObject.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; // truemyObject.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"); // truemyObject.propertyIsEnumerable("b"); // falseObject.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() 方法每次迭代一个数据。

转载于:https://juejin.im/post/5adf240a6fb9a07a9c03dd3b

你可能感兴趣的文章