14

1. 什么是方法

首先让我们定义并调用常规函数:

function greet(who) {
  return `Hello, ${who}!`;
}

greet('World'); // => 'Hello, World!'

常规函数定义的形式为关键字 function 后跟名称、参数和函数体:function greet(who) {...}

greet('World')常规函数调用。函数 greet('World') 从参数接受数据。

如果 who 是对象的属性怎么办?要想轻松访问对象的属性,可以将函数附加到该对象,也就是创建一个方法。

让我们把 greet() 作为对象 world 的一个方法:

const world = {
  who: 'World',

  greet() {    
      return `Hello, ${this.who}!`;  
  }
}

world.greet(); // => 'Hello, World!'

greet() { ... } 现在是属于 world 对象的一个方法world.greet()是一种方法调用。

greet() 方法内部,this 指向该方法所属的对象 world。这就是为什么 this.who 表达式能够访问属性 who 的原因。

this 也叫上下文(context)

上下文是可选的

在上个例子中,我们用了 this 来访问该方法所属的对象,但是 JavaScript 并没有强制使用 this 的方法。

所以可以将对象作为方法的命名空间来使用:

const namespace = {
  greet(who) {
    return `Hello, ${who}!`;
  },

  farewell(who) {
    return `Good bye, ${who}!`;
  }
}

namespace.greet('World');    // => 'Hello, World!'
namespace.farewell('World'); // => 'Good bye, World!'

namespace 是一个拥有 2 种方法的对象:namespace.greet()namespace.farewell()

这些方法没有用 this,而 namespace 是方法的所有者。

2. 对象字面量方法

如上面所示,你可以直接在对象字面量中定义方法:

const world = {
  who: 'World',

  greet() {    
      return `Hello, ${this.who}!`;  
  }
};

world.greet(); // => 'Hello, World!'

greet(){....} 是在对象字面量上定义的方法。这种定义类型称为简写方法定义(ES2015+ 开始可用)。

方法定义的语法也更长:

const world = {
  who: 'World',

  greet: function() {    
      return `Hello, ${this.who}!`;  
  }
}

world.greet(); // => 'Hello, World!'

greet: function() {...}方法定义。注意冒号和 function 关键字。

动态添加方法

方法只是一个函数,它作为属性被保存在对象上。因此可以向对象动态添加方法:

const world = {
  who: 'World',

  greet() {
    return `Hello, ${this.who}!`;
  }
};

// 一个带有函数的新属性
world.farewell = function () {
  return `Good bye, ${this.who}!`;
}

world.farewell(); // => 'Good bye, World!'

首先,world 对象没有 farewell 方法,它是被动态添加的。

调用动态添加的方法完全没有问题:world.farewell()

3. 类方法

在 JavaScript 中,class 语法定义了一个类,该类是它实例的模板。

一个类也可以有方法:

class Greeter {
  constructor(who) {
    this.who = who;
  }

  greet() {    
      console.log(this === myGreeter); // => true    
      return `Hello, ${this.who}!`;  
  }
}

const myGreeter = new Greeter('World');
myGreeter.greet(); // => 'Hello, World!' 

greet() {...} 是在类内部定义的方法。

每次使用 new 操作符创建类的实例时(例如,myGreeter = new Greeter('World')),都可以通过方法来创建实例。

myGreeter.greet() 是在实例上调用 greet() 方法的,方法内部的 this 等于实例本身,即 this 等于 greet() { ... } 方法内部的 myGreeter

4. 如何调用方法

4.1 方法调用

对象或类上定义方法只是完成了工作的一半。为了保持方法的上下文,你必须确保将其作为“方法”去调用。

回想一下带有 greet() 方法的 world 对象。让我们检查一下当方法和常规函数 greet() 被调用时,this 的值是什么:

const world = {
  who: 'World',

  greet() {
    console.log(this === world);    return `Hello, ${this.who}!`;
  }
};

// 方法调用
world.greet(); // => true
const greetFunc = world.greet;
// 常规函数调用
greetFunc(); // => false

world.greet() 是一种方法调用。对象 world,后跟一个点 .,最后是方法本身,这就是方法调用

greetFuncworld.greet的功能相同。但是当作为常规函数 greetFunc() 调用时,greet() 内部的 this 不等于 world 对象,而是等于全局对象(在浏览器中是 window)。

命名类似 greetFunc = world.greet 的表达式,将方法与其对象分开。当稍后调用分离的方法 greetFunc() 时,会使 this 等于全局对象。

将方法与其对象分开可以采取不同的形式:

//方法是分开的! this 丢失!
const myMethodFunc = myObject.myMethod;

//方法是分开的! this 丢失!
setTimeout(myObject.myMethod, 1000);

//方法是分开的! this 丢失!
myButton.addEventListener('click', myObject.myMethod)

//方法是分开的! this 丢失!
<button onClick={myObject.myMethod}>My React Button</button>

为避免丢失方法的上下文,要确保使用方法调用 world.greet() 或将方法手动绑定到对象 greetFunc = world.greet.bind(this)

4.2 间接函数调用

在上一节中,常规函数调用已将 this 解析为全局对象。那么有没有一种方法可以使常规函数具有 this 的可自定义值?

可以使用下面的间接函数调用:

myFunc.call(thisArg, arg1, arg2, ..., argN);
myFunc.apply(thisArg, [arg1, arg2, ..., argN]);

myFunc.call(thisArg)myFunc.apply(thisArg) 的第一个参数是间接调用的上下文(this 的值)。换句话说,你可以手动改变函数中 this 的值。

例如,让我们将 greet() 定义为常规函数,并定义一个具有 who 属性的对象 aliens

function greet() {
  return `Hello, ${this.who}!`;
}

const aliens = {
  who: 'Aliens'
};

greet.call(aliens); // => 'Hello, Aliens!'
greet.apply(aliens); // => 'Hello, Aliens!'

greet.call(aliens)greet.apply(aliens) 都是间接方法调用。函数 greet() 中的 this 值等于 aliens 对象。

间接调用使你可以在对象上模拟方法调用。

4.3 绑定函数调用

最后是在对象上使函数作为方法调用的第三种方法:将函数绑定为具有特定上下文。

可以用特殊方法创建绑定函数:

const myBoundFunc = myFunc.bind(thisArg, arg1, arg2, ..., argN);

myFunc.bind(thisArg) 的第一个参数是函数要绑定到的上下文。

例如,让我们重用 greet() 并将其绑定到 aliens 上下文:

function greet() {
  return `Hello, ${this.who}!`;
}

const aliens = {
  who: 'Aliens'
};

const greetAliens = greet.bind(aliens);

greetAliens(); // => 'Hello, Aliens!'

调用 greet.bind(aliens) 会创建一个新函数,其中 this 绑定到 aliens 对象。

然后,当调用绑定函数 greetAliens() 时,this 等于该函数内部的 aliens

同样,使用绑定函数还可以模拟方法调用。

5. 箭头函数方法

不建议将箭头功能用作方法,原因如下:

//把 greet() 方法定义为箭头函数
const world = {
  who: 'World',

  greet: () => {
    return `Hello, ${this.who}!`;
  }
};

world.greet(); // => 'Hello, undefined!'

world.greet() 返回 'Hello, undefined!' ,而不是预期的 'Hello, World!'

问题是箭头函数内的 this 属于外部作用域,你想要 this 等于 world 对象,但是在浏览器中 thiswindow'Hello, ${this.who}!' 的计算结果为 Hello, ${windows.who}!,所以最后的结果是 'Hello, undefined!'

尽管我很喜欢箭头函数,但是不能把它们用作方法。

总结

方法是属于对象的函数。方法的上下文(this值)等于该方法所属的对象。

你还可以在类上定义方法。类方法中的 this 等于实例。

仅定义一个方法是不够的,还要能够调用才行。一般方法调用实用以下语法:

// 方法调用
myObject.myMethod('Arg 1', 'Arg 2');

在 JavaScript 中,你可以定义一个不属于对象的常规函数​​,然后将该函数作为对任意对象的方法来调用。你可以通过间接函数调用或将函数绑定到特定上下文来实现:

// 间接函数调用
myRegularFunc.call(myObject, 'Arg 1', 'Arg 2');
myRegularFunc.apply(myObject, 'Arg 1', 'Arg 2');

// 绑定函数
const myBoundFunc = myRegularFunc.bind(myObject);
myBoundFunc('Arg 1', 'Arg 2');

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:



疯狂的技术宅
44.4k 声望39.2k 粉丝