JavaScript:深入了解对象

简介

JavaScript 是一门强大并且语义清晰的语言,无论是使用浏览器 DOM API 还是利用 React 或 Angular 等热门库构建元素,JavaScript 语言都能帮助你为网页创建动态体验。可能你一直编写的代码都是按线性顺序运行,顶部的代码先运行,然后下一行代码运行再下一行,一直到文件底部。本篇将介绍使用 JavaScript 进行面向对象编程,借助面向对象编程将能够编写可以创建无数个功能类似的对象实例的类,还将了解可以如何使用代理和继承构建结构合理性能高效的应用。

回顾数组

数组是 JavaScript 中最有用的数据结构之一。在本质上,数组就是一个由方括号(即 [])括起来的有序元素集合。以下是一个 myArray 变量,它被赋给一个空数组:

let myArray = [];

数组中的每个元素都被一个称为索引的数字键
所引用,该索引从 0 开始,并随着数组中的每个增加元素以 1 递增。请看以下示例:

const fruits = ['apple', 'banana', 'orange', 'grape', 'lychee'];

console.log(fruits); // ['apple', 'banana', 'orange', 'grape', `lychee`]

如果想检索数组中的元素可以通过索引

const fruits = ['apple', 'banana', 'orange', 'grape', 'lychee'];

console.log(fruits[0]); // 'apple'
console.log(fruits[fruits.length-1]); // 'lychee'

回顾对象

对象是 JavaScript 中最重要的数据结构之一。毕竟,你正在学习的这门课程就是介绍面向对象编程的!

从根本上来说,对象就是一个有关联的键/值对的集合。我们使用大括号(即 {})来创建对象。以下是 myObject 变量,它被赋给一个空对象:

const myObject = {};

数组 中的元素被数字索引所引用,而对象中的键则必须直接命名,如 color 或 year。请看以下示例:

const car = {
  color: 'red',
  year: 1992,
  isPreOwned: true
};

它是如何运作的:

  • 赋给该对象的变量被命名为 car。
  • 使用大括号来定义 car 对象。
  • 每个键(例如 color)均与一个值(在此处为 ‘red’)相关联。这些键值对通过冒号 (:) 连接起来。
  • 每个独特的键值对(称为该对象的属性)均通过逗号 (,) 与其他属性分隔开来。因此,car 对象包含三个属性。

与数组不同,对象是无序集合。例如,上面的 car 对象可以采用不同的顺序编写键/值对,而不会影响你如何访问 car 的项目:

const car = {
  isPreOwned: true,
  color: 'red',
  year: 1992
};

对象属性语法

另外要注意的是,键(即对象属性的名称)是字符串,但是围绕这些字符串的引号是可选的,前提是该字符串也是一个有效的 JavaScript 标识符(即可以将其用作变量名称或函数名称)。因此,以下三个对象是等价的:

const course = { courseId: 711 };    // ← 没有引号
const course = { 'courseId': 711 };  // ← 单引号
const course = { "courseId": 711 };  // ← 双引号

你会经常看到属性名称中省略了引号。在某些情况下需要包含引号,特别是如果属性名称:

  • 是保留字(例如 foriflettrue 等)。
  • 包含不能出现在变量名称中的空格或特殊字符(即 $ 和 _ 以外的标点符号,包括大多数重音字符)。

访问对象属性

有两种方法:点表示法和方括号表示法。

const bicycle = {
  color: 'blue',
  type: 'mountain bike',
  1: 123,
  wheels: {
    diameter: 18,
    width: 8
  }
};
// 使用点表示法和方括号表示法都可以访问对象属性
bicycle.color;
bicycle['color'];
bicycle.wheels.width;
bicycle['wheels']['width'];

请注意,尽管点表示法可能更易于读写,但它并不适用于所有情况。例如,假设上面的 bicycle 对象中有一个键是数字。那么,像 bicycle.1; 这样的表达式将会导致错误,而 bicycle[1]; 则可以返回预期的值:

bicycle.1; // SyntaxError: unexpected token: numeric literal

bicycle[1]; // 123

另一个问题在于将变量赋给属性名称。假设我们声明了 myVariable,并将其赋给字符串 'color'

const bicycle = {
  color: 'blue',
  type: 'mountain bike',
  1: 123,
  wheels: {
    diameter: 18,
    width: 8
  }
};
const myVariable = 'color';
bicycle[myVariable];// 'blue'
bicycle.['myVariable']; // undefined
bicycle.myVariable; // undefined

bicycle[myVariable]; 将会返回 'blue',(注意此处放括号里是不带引号的)因为变量 myVariable 会被替换为它的值(字符串 'color'),且 bicycle['color'] 的值也同样是 'blue'。 但 bicycle.myVariable 则会返回 undefined

这看起来也许很奇怪,但要记得,JavaScript 对象中的所有属性键都是字符串,即使省略了引号也是如此。当使用点表示法时,JavaScript 解释器将在 bicycle 中查找一个值为 'myVariable' 的键。由于该对象中并未定义这样一个键,因此这个表达式将会返回 undefined

小结

在 JavaScript 中,对象是一个无序的属性集合。每个属性由一个键值对组成,并且既可以引用 原始/基本类型(例如字符串、数字、布尔值等),也可以引用其他对象。与通过数字索引进行访问的数组中的元素不同,对象中的属性需要使用方括号表示法或点表示法通过键名进行访问。

创建和修改属性

创建对象

要创建一个新的空白(即「空」)对象,可以使用对象字面量表示法Object() 构造函数

// 使用字面量表示法:
const myObject = {};

// 使用 Object() 构造函数:
const myObject = new Object();

虽然这两个方法最终都会返回一个没有自己属性的对象,但是 Object() 构造函数相对较慢,而且较为冗长。因此,在 JavaScript 中创建新对象的推荐方法是使用字面量表示法。

修改属性

对象中的数据是可以被改变。

const cat = {
  age: 2,
  name: 'Bailey',
  meow: function () {
    console.log('Meow!');
  },
  greet: function (name) {
    console.log(`Hello ${name}`);
  }
};
cat.age++;
console.log(cat.age); // 2

添加属性

const printer = {};

printer.on = true;
printer.mode = 'black and white';
printer['remainingSheets'] = 168;
printer.print = function () {
  console.log('The printer is printing!');
};

printer 就被修成这样

{
  on: true,
  mode: 'black and white',
  remainingSheets: 168,
  print: function () {
    console.log('The printer is printing!');
  }
};

移除属性

const cat = {
  age: 2,
  name: 'Bailey',
  meow: function () {
    console.log('Meow!');
  },
  greet: function (name) {
    console.log(`Hello ${name}`);
  }
};
delete cat.age; // 如果删除成功会返回:true
console.log(cat.age); // undefined

请注意,delete 会直接改变当前的对象,如果删除成功会返回 true。如果调用一个已被删除的方法,JavaScript 解释器将无法再找到 age 属性,因为 age 键(及其值 2)已被删除所以返回 undefined

传递参数

传递一个原始类型

在 JavaScript 中,原始类型(例如字符串、数字、布尔值等)是不可变的。换句话说,对函数中的参数所作的任何更改都会有效地为该函数创建一个局部副本,而不会影响该函数外部的原始类型。

function changeToEight(n) {
  n = 8; // 无论 n 是什么,它此刻都是 8... 但仅仅是在这个函数中!
}

let n = 7;

changeToEight(n);

console.log(n); // 7

传递一个对象

另一方面,JavaScript 中的对象是可变的。如果你向函数传递一个对象,Javascript 会传递一个引用给该对象。如果我们向函数传递一个对象,然后修改一个属性,让我们看看会发生什么:

let originalObject = {
  favoriteColor: 'red'
};

function setToBlue(object) {
  object.favoriteColor = 'blue';
}

setToBlue(originalObject);

originalObject.favoriteColor; // blue

在以上示例中,originalObject 只包含一个属性,即 favoriteColor,它的值是 'red'。我们将 originalObject 传递给 setToBlue() 函数并调用它。在访问 originalObjectfavoriteColor 属性后,我们看到这个值已变为 'blue'

这是由于 JavaScript 中的对象是通过引用传递的,因此如果修改那个引用,其实是在直接修改原始对象本身!

更重要的是:同样的规则适用于将一个对象重新赋给新的变量,然后改变那个副本。同样,由于对象是通过引用传递的,因此原始对象也被改变了。

const iceCreamOriginal = {
  Andrew: 3,
  Richard: 15
};

const iceCreamCopy = iceCreamOriginal;
iceCreamCopy.Richard; // 15

iceCreamCopy.Richard = 99;
iceCreamCopy.Richard; // 99
iceCreamOriginal.Richard; // 99

将两个对象进行比较

针对引用问题,让我们来看看将两个对象进行比较会发生什么。以下对象 parrot 和 pigeon 具有相同的方法和属性:

const parrot = {
  group: 'bird',
  feathers: true,
  chirp: function () {
    console.log('Chirp chirp!');
  }
};

const pigeon = {
  group: 'bird',
  feathers: true,
  chirp: function () {
    console.log('Chirp chirp!');
  }
};

parrot === pigeon; // false
const parrot = {
  group: 'bird',
  feathers: true,
  chirp: function () {
    console.log('Chirp chirp!');
  }
};
const myBird = parrot;
myBird === parrot; // true

也就是说,只有在将对同一个对象的两个引用进行比较时,这个表达式才会返回 true

小结

对象通常使用字面量表示法来创建,并可包含指向函数的属性,称为方法。方法的访问方式与对象其他属性的访问方式相同,而且可以像普通函数一样进行调用,只不过它们可以自动访问其父对象的其他属性。

默认情况下,对象是可变的(除了少数例外),因此其中的数据可以被改变。既可以添加新属性,也可以通过指定属性名称并赋值(或重新赋值)来轻松修改现有属性。此外,对象的属性和方法还可以使用 delete 运算符来删除,它会直接改变对象。

虽然我们在本部分向对象作出了许多修改,但其实有方法可以完全、明确地防止改变。在下一部分,我们将学到一些有用的方法,不仅可以做到这一点,而且还能做到更多!

延伸

调用对象方法

函数与方法

目前,我们所看到的对象属性的行为更像是特性。也就是说,诸如 colortype 这样的属性是描述对象的数据,但它们并不「做」任何事情。我们可以通过向对象添加方法来扩展其功能。

假设有一个函数 sayHello(),它只是简单地把消息记录到控制台,还有一个 developer 对象,它只有一个属性 name,如果把 sayHello() 函数添加到 developer 对象中就像添加其他新属性一样进行添加:通过提供属性名称,然后给它一个值。区别只是,这个属性的值是一个函数

const developer = {
  name: 'Andrew',
  sayHello: function () {
    console.log('Hi there!');
  }
};

那么如何调用它的引用函数呢?

调用方法

我们可以使用属性名称来访问对象中的函数。对象的函数属性的另一个名称是方法。我们可以像访问其他属性一样来访问它:使用点表示法或方括号表示法。让我们回头看看上面更新的 developer 对象,然后调用 sayHello 方法:

const developer = {
  name: 'Andrew',
  sayHello: function sayHi () {
    console.log('Hi there!');
  }
};
// 通过在方法名称末尾添加圆括号来调用对象方法。
developer.sayHello; // 返回函数,要运行它则在末尾加双括号
developer.sayHello(); // 'Hi there!'
developer['sayHello'](); // 'Hi there!'

另外注意,即便调用的是一个有名函数,也是调用对象的属性。

将参数传递给方法

如果方法带有参数,也可以按照相同的方式进行操作:

const developer = {
  name: 'Andrew',
  sayHello: function () {
    console.log('Hi there!');
  },
  favoriteLanguage: function (language) {
    console.log(`My favorite programming language is ${language}`);
  }
};


developer.favoriteLanguage('JavaScript'); // My favorite programming language is JavaScript

方法可以访问被调用的对象

对象可以包含数据和操纵数据的手段。但是,对象究竟如何引用自身的属性,甚至操纵其中一些属性呢?这都是借助 this 关键字来实现的!

使用 this,方法可以直接访问被调用的对象。

const triangle = {
  type: 'scalene',
  identify: function () {
    console.log(this); // 可以看看 this 具体是什么
    console.log(`This is a ${this.type} triangle.`);
  }
};

triangle.identify(); // 'This is a scalene triangle.'

请注意,在 identify() 方法中,使用了值 this。当你说 this 时,你其实是在说「这个对象」或「当前对象」。正是 thisidentify() 方法能够直接访问 triangle 对象的属性。

identify() 方法被调用时,this 的值会被设置为调用它的对象:triangle。因此,identify() 方法可以访问和使用 triangletype 属性,如上面的 console.log() 表达式所示。

另外,this 是 JavaScript 中的一个保留字,不能用作标识符(例如变量名称、函数名称等)。

小结

方法是对象的函数属性。它与对象的任何其他属性的访问方式相同(即使用点表示法或方括号表示法),并且与对象外部的常规函数的调用方式相同(即将圆括号添加到表达式末尾)。

由于对象既包括数据,又包括对这些数据进行操作的手段,因此方法可以使用特别的 this 关键字来访问被调用的对象。当方法被调用时,this 的值将被确定,它的值就是调用该方法的对象。由于 this 是 JavaScript 中的一个保留字,因此它的值不能用作标识符。

延伸

注意全局变量

属于对象的东西

在前面的部分,我们看到包含在某个对象中的属性和方法属于该对象。让我们用一个简单的例子来解释一下:

const chameleon = {
  eyes: 2,
  lookAround: function () {
     console.log(`I see you with my ${this.eyes} eyes!`);
  }
};

chameleon.lookAround(); // 'I see you with my 2 eyes!'

该函数体内部是代码 this.eyes。由于 lookAround() 方法在 chameleon 对象上作为 chameleon.lookAround() 被调用,因此 this 的值就是 chameleon 对象本身!相应地,this.eyes 就是数字 2,因为它指向 chameleon 对象的 eyes 属性。

function whoThis () {
  this.trickyish = true
}

whoThis(); // undefined

如果对象里的属性函数的 this 是对象本身,那么一个常规下的函数中的 this 是什么?是 window

const chameleon = {
  eyes: 2,
  lookAround: function () {
     console.log(`I see you with my ${this.eyes} eyes!`);
  }
};

function whoThis () {
  this.trickyish = true
}

chameleon.lookAround();

whoThis();

this 和调用

函数如何调用决定了函数内的 this 的值。

由于 .lookAround() 作为一个方法被调用,因此 .lookAround() 中的 this 的值就是调用时位于点左侧的部分。由于调用如下所示:

chameleon.lookAround();

chameleon 对象位于点的左侧。因此,在 .lookAround() 方法中,this 将指向 chameleon 对象!那么

whoThis();

点的左侧也没有任何对象,那whoThis() 函数中的 this 的值是什么?这是 JavaScript 语言一个有趣的特点。当一个常规函数被调用时,this 的值就是全局 window 对象。当一个常规函数被调用时,this 的值就是全局 window 对象。

window 对象

如果你还没有用过 window 对象,该对象是由浏览器环境提供的,并可使用标识符 window 在 JavaScript 代码中进行全局访问。该对象不是 JavaScript 规范(即 ECMAScript)的一部分,而是由 W3C(英) 开发的。

全局变量是 window 上的属性

由于 window 对象处于最高(即全局)级别,因此全局变量声明会导致一个有趣的结果。每个在全局级别(在函数外部)进行的变量声明都会自动成为 window 对象上的一个属性!

var currentlyEating = 'ice cream';

window.currentlyEating === currentlyEating; // true!

全局变量和 varletconst

JavaScript 中使用关键字 varletconst 来声明变量。自从该语言出现以来,var 就已经存在了,而 letconst 则是过了很久才新增的(增入 ES6 中)。

只有使用 var 关键字来声明变量才会将其添加到 window 对象中。如果你用 letconst 在函数外部声明一个变量,它将不会被作为属性添加到 window 对象中。

let currentlyEating = 'ice cream';

window.currentlyEating === currentlyEating; // false!

全局函数是 window 上的方法

与全局变量可以作为 window 对象上的属性进行访问类似,任何全局函数声明都可以作为 window 对象上的方法进行访问:

function learnSomethingNew() {
  window.open('https://www.udacity.com/');
}

window.learnSomethingNew === learnSomethingNew; // true

learnSomethingNew() 函数声明为一个全局函数声明(即它可以全局访问,而没有写入另一个函数内部)将使其可以作为 learnSomethingNew()window.learnSomethingNew() 供你的代码访问。

避免全局变量

如前所述,声明全局变量和函数会将它们作为属性添加到 window 对象。你可能会觉得,全局可访问的代码听起来好像超级有用,然而,与直觉相反,全局变量和函数并不理想。这是由于很多原因,不过我们要看的两种是:

  • 紧密耦合
  • 名称冲突

紧密耦合

紧密耦合是开发者用来表示代码过于依赖彼此细节的一个短语。「耦合」一词是指「将两样东西配对」。在紧密耦合中,代码片断以一种紧密的方式连接在一起,使得更改一代码会无意中改变其他代码的功能:

var instructor = 'Richard';

function richardSaysHi() {
  console.log(`${instructor} says 'hi!''`);
}

在以上代码中,看看 instructor 变量是如何全局声明的?richardSaysHi() 函数并未使用某个局部变量来存储讲师的名字。相反,它使用了外部的全局变量。如果我们重构这段代码,将这个变量从 instructor 改为 teacher,则会破坏 richardSaysHi() 函数(或者我们必须单独更新它!)。这是一个紧密耦合代码的(简单)示例。

名称冲突

当两个(或多个)函数依赖于具有相同名称的变量时,则会发生名称冲突。这里的一个主要问题是,两个函数都会尝试更新变量和/或设置变量,但是这些更改将被相互覆盖。让我们来看一下这个 DOM 操作代码中的名称冲突示例:

let counter = 1;

function addDivToHeader () {
  const newDiv = document.createElement('div');
  newDiv.textContent = 'div number ' + counter;

  counter = counter + 1;

  const headerSection = document.querySelector('header');
  headerSection.appendChild(newDiv)
}

function addDivToFooter() {
  const newDiv = document.createElement('div');
  newDiv.textContent = 'div number ' + counter;

  counter = counter + 1;

  const headerSection = document.querySelector('footer');
  headerSection.appendChild(newDiv)
}

在这段代码中,我们有一个 addDivToHeader() 函数和一个 addDivToFooter() 函数。这两个函数都会创建一个 <div> 元素,并增加 counter 变量。

这段代码看起来没问题,但是如果你尝试运行这段代码,并在 <header><footer> 元素中添加一些 <div>,就会发现编号无法增加!addDivToHeader()addDivToFooter() 都需要一个全局的 counter 变量供其访问,而且不会从其外部发生改变。

由于这两个变量都会增加 counter 变量,如果代码交替调用 addDivToHeader()addDivToFooter(),那么它们各自的 <div> 将不会有上升的数字。例如,如果我们有以下调用:

addDivToHeader();
addDivToHeader();
addDivToFooter();
addDivToHeader();

开发者可能希望让 <header> 有三个带有数字 1、2、3 的 <div> 元素,并让 <footer> 元素有一个带有数字 1 的 <div>。然而,这段代码将会产生的是一个带有三个 <div>,但是数字为 1、2 和 4(而不是 3)的 <header> 元素,以及一个数字为 3 的 <footer> 元素…这是非常不同的结果。这是因为,这两个函数都依赖于 counter 变量,并且都会更新它。

那正确的做法是应该尽可能少写全局变量,将变量所在所要需要用到的函数中,且尽可能近的放在需要的地方。除非实在没办法了才写全局变量。

小结

window 对象由浏览器提供,而不属于 JavaScript 语言或规范的一部分。任何全局变量声明(即那些使用 var 的变量)或全局函数声明都会作为属性添加到此 window 对象。过度使用全局变量并不明智,反而会导致意想不到的问题,妨碍你准确地编写代码。

无论你是使用 window 对象还是自己创建的对象,都要记住,所有对象都由键/值对组成。

延伸

提取属性和值

对象方法

还记得使用 Object() 构造函数来创建(即实例化)带有 new 关键字的新对象吗?

const myNewFancyObject = new Object();

实际上,Object() 函数包含一些自己的方法,可以帮助你开发应用程序。这些方法是:

  • Object.keys()
  • Object.values()

Object.keys()Object.values()

在本质上,对象只是一个键/值对的集合。 如果我们只想从对象中提取键呢?假设我们有以下对象,表示一个字典:

const dictionary = {
  car: 'automobile',
  apple: 'healthy snack',
  cat: 'cute furry animal',
  dog: 'best friend'
};

获得一个仅包含单词(即 dictionary 的键)的集合可能非常有用。虽然我们可以使用 for...in 循环遍历一个对象,并构建我们自己的键列表,但它可能会有点混乱和冗长。谢天谢地,JavaScript 专门为此提供了一个抽象!

Object.keys() 被赋予一个对象时,它只会提取该对象的,并将它们返回为一个数组:

Object.keys(dictionary); // ['car', 'apple', 'cat', 'dog']

因此,Object.keys() 会返回一个数组,其中包含给定对象的属性名称。现在,如果我们需要一个对象的列表,我们可以使用 Object.values()

Object.values(dictionary); // ['automobile', 'healthy snack', 'cute furry animal', 'best friend']

小结

Object() 构造函数可以访问几种方法来帮助开发。简而言之:

  • Object.keys() 会返回一个数组,其中包含给定对象自己的键(属性名称)。
  • Object.values() 会返回一个数组,其中包含给定对象自己的值(属性)。

延伸

您可能还喜欢...

发表评论

电子邮件地址不会被公开。 必填项已用*标注