原文链接:http://javascript.info/object-methods,translate with ❤️ by zhangbao.

对象通常来用表示现实世界里的实体,像用户或者订单等:

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };

而且,在现实世界里,用户可以行动:从购物车里选东西,登录,注销等。

在 JavaScript 中,行为是通过函数类型的属性体现的。

方法例子

开始,我们实现 user 说 say hi:

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. user.sayHi = function() {
  6. alert("Hello!");
  7. };
  8. user.sayHi(); // Hello!

这里我们使用一个函数表达式来创建函数,并且将它赋值给对象 user.sayHi 属性。

然后我们调用它,用户就可以说话了!

作为对象属性的函数称为方法

因此,我们可以说 user 上有一个方法 sayHi。

当然,我们也可以直接使用预定义函数作为方法使用,像这样:

  1. let user = {
  2. // ...
  3. };
  4. // first, declare
  5. function sayHi() {
  6. alert("Hello!");
  7. };
  8. // then add as a method
  9. user.sayHi = sayHi;
  10. user.sayHi(); // Hello!

⚠️面向对象编程

在代码里,如果我们用对象表示实体,就称为面向对象编程,简称“OOP”。

OOP是一件大事,它本身就是一门有趣的科学。如何选择正确的实体?如何组织它们之间的交互?这就是架构,有很多关于这个主题的书,比如“设计模式:可重用的面向对象软件的元素”by E.Gamma, R.Helm, R.Johnson, J.Vissides 或“应用程序的面向对象分析和设计”by G.Booch 等等。我们会在后面的章节《对象,类,继承》中简单提到这个话题。

方法简写

在对象字面量里可以使用方法的简写形式:

  1. // 这些对象是同一个意思
  2. let user = {
  3. sayHi: function() {
  4. alert("Hello");
  5. }
  6. };
  7. // 方法的简写形式,这样更好,对吧
  8. let user = {
  9. sayHi() { // same as "sayHi: function()"
  10. alert("Hello");
  11. }
  12. };

根据上面的写法,我们可以忽略“function”并且仅写作 sayHi()。

说实话,这些符号并不是完全相同的。与对象继承相关的细微差别(稍后将介绍),但现在它们并不重要。在几乎所有情况下,较短的语法都是首选。

方法中的“this”

很常见的是,对象方法需要访问储存在对象中的信息来完成它的工作。

例如,user.sayHi() 中的代码可能需要 user 的名字。

在方法里访问对象,可以使用 this 关键字。

this 的值是“点之前”的对象,也就是调用的主体。

例如:

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. sayHi() {
  5. alert(this.name);
  6. }
  7. };
  8. user.sayHi(); // John

在 user.sayHi() 执行时,内部的 this 指的是 user。

从技术上将,也可以不用 this,直接通过外部变量来引用也 OK。

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. sayHi() {
  5. alert(user.name); // 用 "user" 而不是 "this"
  6. }
  7. };

但是这样的代码并不可靠,如果我们将 user 复制到另一个变量中,例如,admin = user,并且重写了 user 里面的一些东西。然后我们可能访问了错误的对象。

用下面的代码表示:

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. sayHi() {
  5. alert( user.name ); // leads to an error
  6. }
  7. };
  8. let admin = user;
  9. user = null; // overwrite to make things obvious
  10. admin.sayHi(); // Whoops! inside sayHi(), the old name is used! error!

如果我们使用的 this.name 而不是 user.name 在 alert 里,那代码就能顺利执行了。

“this”没有绑定

在 JavaScript 中,“this”关键字不像其他的编程语言。首先,它可以在任何函数里使用:

下面的代码里不会出现语法错误:

  1. function sayHi() {
  2. alert( this.name );
  3. }

this 值是在代码执行时确定的。可以是任何东西。

例如,相同的函数在执行时可能会有不同的“this”值,因为是在不同对象里调用的:

  1. let user = { name: "John" };
  2. let admin = { name: "Admin" };
  3. function sayHi() {
  4. alert( this.name );
  5. }
  6. // use the same functions in two objects
  7. user.f = sayHi;
  8. admin.f = sayHi;
  9. // these calls have different this
  10. // "this" inside the function is the object "before the dot"
  11. user.f(); // John (this == user)
  12. admin.f(); // Admin (this == admin)
  13. admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)

实际上,我们也可以不再任何对象里调用这个方法。

  1. function sayHi() {
  2. alert(this);
  3. }
  4. sayHi(); // undefined

在这种情形下,严格模式里 this 是指 undefined。如果我们尝试访问 this.name,会报错。

在非严格模式下(如果不使用 use strict),this 指向的是全局对象(浏览器里的 window,滞后接触)。这是一个历史遗留问题,”use strict” 修复了它。

请注意,通常在没有对象的情况下在函数中使用 this 的调用不是正常的,而是一个编程错误。如果一个函数有这个,那么它通常是在一个对象的上下文中调用的。

⚠️不绑定 this 的后果

如果您来自另一种编程语言,那么您可能已经习惯了“绑定 this”的概念,在这个概念中,对象中定义的方法总是引用该对象。

在JavaScript中,this 是“自由的”,它的值是在调用时决定的,并不依赖于方法声明的位置,而是取决于“点之前”的对象是什么。

运行时决定 this 的概念有优点也有缺点。一方面,一个函数可以为不同的对象重用。另一方面,更大的灵活性为错误开了一扇窗。

在这里,我们的立场不是判断这个语言设计的决定是好是坏。我们将了解如何使用它,如何获得好处和逃避问题。

内部:引用类型

⚠️深入语言特性

这部分讨论的是高级内容,最好知道一些边缘情况。

如果你想快一点,可跳过或者之后再来看。

一种复杂的调用方法的方式,会导致 this 丢失,例如:

  1. let user = {
  2. name: "John",
  3. hi() { alert(this.name); },
  4. bye() { alert("Bye"); }
  5. };
  6. user.hi(); // John (the simple call works)
  7. // now let's call user.hi or user.bye depending on the name
  8. (user.name == "John" ? user.hi : user.bye)(); // Error!

在代码最后一行,有一个三目运算符选择是调用 user.hi 还是 user.bye。在这个例子里,结果是 user.hi。

然后方法立即用括号 () 进行调用。但是失败了!

可以看到调用出错了,调用方法里的 this 变成 undefiend 了。

这样是可以的(对象点方法):

  1. user.hi();

这样就不行了(求得调用到的方法):

  1. (user.name == "John" ? user.hi : user.bye)(); // Error!

为什么?如果我们想知道为啥会这样,就得理解 obj.method() 的调用过程。

拉近看,我们可以看到在 obj.method() 语句中有两个操作:

  1. 首先,点 . 返回属性值 obj.method。

  2. 圆括号 () 调用了它。

所以从第一部分跑到第二部分,this 的信息发生怎样的变化了呢?

如果我们将这步算法分开来看的话,this 肯定会丢失无疑了:

  1. let user = {
  2. name: "John",
  3. hi() { alert(this.name); }
  4. }
  5. // split getting and calling the method in two lines
  6. let hi = user.hi;
  7. hi(); // Error, because this is undefined

这里 hi = user.hi 将函数放进了一个变量里,在下一行它就是完成独立的了,所以这里是没有 this 的了。

为了使 user.hi() 调用生效,JavaScript 使用了一个技巧——点 . 返回的不是一个函数,而是一个特殊的 引用类型 值。

引用类型是一个“特殊的类型”,我们不能精确使用它,但是被语言内部使用。

引用类型的值,是靠三个值组成的 (base, name, strict):

  • base 是指对象。

  • name 是指属性。

  • strict 在使用 use strict 的情况下为 true。

访问 user.hi 的结果不是一个函数,而是一个引用类型值。对在严格模式下调用 user.hi 来说是这样的:

  1. // Reference Type value
  2. (user, "hi", true)

当在引用类型上调用圆括号 () 时,它会返回对象和它的方法的完整信息,并且可以设置正确的 this 值(=user 在这里)。

任何其他像 hi = user.hi 这种方式赋值表达式都会整个丢失引用类型,会将 user.hi(一个函数)的值传递过去,因此进行更进一步就丢失了 this。

因此,如果这个函数是直接用点 obj.method() 直接调用或者使用 objmethod 的语法,那么它的值就会被正确地传递下去。在本教程的后面,我们将学习各种方法来解决这个问题,例如 func.bind()

箭头函数里没有“this”

箭头函数比较特别,它是没有“自己”的 this 的。如果在箭头函数里使用了 this,那么它会从外部“正常”函数里获取。

比如,这里的 arrow() 中使用的 this 就是从外部方法 user.sauHi() 方法里获取的:

  1. let user = {
  2. firstName: "Ilya",
  3. sayHi() {
  4. let arrow = () => alert(this.firstName);
  5. arrow();
  6. }
  7. };
  8. user.sayHi(); // Ilya

这是箭头函数的一个特殊特征,当我们不想要一个单独的函数时,它是有用的,而不是从外部环境中取它。在后面的《重访箭头函数》章节中,我们将更深入地讨论箭头函数。

总结

  • 作为对象属性存储的函数称为“方法”。

  • 方法允许对象以 obj.doSomething() 的形式调用。

  • 方法里可以使用 this 来引用对象。

this 的值是在运行时决定的。

  • 当声明一个函数后,就可以使用 this 了,但 this 是直到函数调用时才有的。

  • 函数可以在对象之间复制。

  • 当一个函数以“方法”语法的形式调用的:object.method(),调用期间 this 的值就是指向 object 的了。

需要注意的是,箭头函数有些特别:它没有 this,当在箭头函数里访问 this 时,会从外部获取的。

(完)