this+call、apply、bind的区别与使用

http://www.ruanyifeng.com/blog/2018/06/javascript-this.htmljavascript

http://www.javashuo.com/article/p-optnjegs-hn.htmlhtml

在函数中 this 到底取何值,是在函数真正被调用执行的时候肯定下来的,函数定义的时候肯定不了。java

由于 this 的取值是函数执行上下文(context)的一部分,每次调用函数,都会产生一个新的执行上下文环境。当代码中使用了 this,这个 this 的值就直接从执行的上下文中获取了,而不会从做用域链中搜寻。typescript

关于 this 的取值,大致上能够分为如下七种状况:
因为严格模式下,禁止this指向全局对象,因此如下示例均运行在非严格模式下swift

状况一:全局 & 调用普通函数
在全局环境中,this 永远指向 window。segmentfault

console.log(this === window); //true
普通函数在调用时候(注意不是构造函数,前面不加 new),其中的 this 也是指向 window。数组

var x = 10;
function foo(){
console.log(this); //Window
console.log(this.x); //10
}
foo();缓存

匿名函数中的this指向全局对象windowapp

var x = 10;
var foo(){
  a: 20,   
  fn:(function(){dom

  alert(this.a);

})()
}

foo.fn//10
foo();

setInterval和setTimeout定时器中的this指向全局对象window

状况二:构造函数
所谓的构造函数就是由一个函数 new 出来的对象,通常构造函数的函数名首字母大写,例如像 Object,Function,Array 这些都属于构造函数。

function Foo(){
this.x = 10;
console.log(this); //Foo {x:10}
}
var foo = new Foo();
console.log(foo.x); //10
上述代码,若是函数做为构造函数使用,那么其中的 this 就表明它即将 new 出来的对象。

可是若是直接调用 Foo 函数,而不是 new Foo(),那就变成状况1,这时候 Foo() 就变成普通函数。

function Foo(){
this.x = 10;
console.log(this); //Window
}
var foo = Foo();
状况三:对象方法
若是函数做为对象的方法时,方法中的 this 指向该对象。

var obj = {
x: 10,
foo: function () {
console.log(this); //Object
console.log(this.x); //10
}
};
obj.foo();
注意:如果在对象方法中定义函数,那么状况就不一样了。

var obj = {
x: 10,
foo: function () {
function f(){
console.log(this); //Window
console.log(this.x); //undefined
}
f();
}
}
obj.foo();
能够这么理解:函数 f 虽然是在 obj.foo 内部定义的,但它仍然属于一个普通函数,this 仍指向 window。

在这里,若是想要调用上层做用域中的变量 obj.x,可使用 self 缓存外部 this 变量。

var obj = {
x: 10,
foo: function () {
var self = this;
function f(){
console.log(self); //{x: 10}
console.log(self.x); //10
}
f();
}
}
obj.foo();
若是 foo 函数不做为对象方法被调用:

var obj = {
x: 10,
foo: function () {
console.log(this); //Window
console.log(this.x); //undefined
}
};
var fn = obj.foo;
fn();
obj.foo 被赋值给一个全局变量,并无做为 obj 的一个属性被调用,那么此时 this 的值是 window。

状况四:构造函数 prototype 属性
function Foo(){
this.x = 10;
}
Foo.prototype.getX = function () {
console.log(this); //Foo {x: 10, getX: function}
console.log(this.x); //10
}
var foo = new Foo();
foo.getX();
在 Foo.prototype.getX 函数中,this 指向的 foo 对象。不只仅如此,即使是在整个原型链中,this 表明的也是当前对象的值。

状况五:函数用 call、apply或者 bind 调用。
var obj = {
x: 10
}
function foo(){
console.log(this); //{x: 10}
console.log(this.x); //10
}
foo.call(obj);
foo.apply(obj);
foo.bind(obj)();
当一个函数被 call、apply 或者 bind 调用时,this 的值就取传入的对象的值。

状况六:DOM event this
在一个 HTML DOM 事件处理程序里,this 始终指向这个处理程序所绑定的 HTML DOM 节点:

function Listener(){
document.getElementById('foo').addEventListener('click', this.handleClick); //这里的 this 指向 Listener 这个对象。不是强调的是这里的 this
}
Listener.prototype.handleClick = function (event) {
console.log(this); //<div id="foo"></div>
}
var listener = new Listener();
document.getElementById('foo').click();
这个很好理解,就至关因而给函数传参,使 handleClick 运行时上下文改变了,至关于下面这样的代码:

var obj = {
x: 10,
fn: function() {
console.log(this); //Window
console.log(this.x); //undefined
}
};
function foo(fn) {
fn();
}
foo(obj.fn);
你也能够用经过 bind 切换上下文:

function Listener(){
document.getElementById('foo').addEventListener('click',this.handleClick.bind(this));
}
Listener.prototype.handleClick = function (event) {
console.log(this); //Listener {}
}
var listener = new Listener();
document.getElementById('foo').click();
前六种状况其实能够总结为: this 指向调用该方法的对象。

状况七:箭头函数中的 this
当使用箭头函数的时候,状况就有所不一样了:箭头函数内部的 this 是词法做用域,由上下文肯定。

var obj = {
x: 10,
foo: function() {
var fn = () => {
return () => {
return () => {
console.log(this); //Object {x: 10}
console.log(this.x); //10
}
}
}
fn()()();
}
}
obj.foo();
如今,箭头函数彻底修复了 this 的指向,this 老是指向词法做用域,也就是外层调用者 obj。

若是使用箭头函数,之前的这种 hack 写法:

var self = this;
就再也不须要了。

var obj = {
x: 10,
foo: function() {
var fn = () => {
return () => {
return () => {
console.log(this); // Object {x: 10}
console.log(this.x); //10
}
}
}
fn.bind({x: 14})()()();
fn.call({x: 14})()();
}
}
obj.foo();
因为 this 在箭头函数中已经按照词法做用域绑定了,因此,用 call()或者 apply()调用箭头函数时,没法对 this 进行绑定,即传入的第一个参数被忽略。

【复合场景1】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var someone = {
  name: "Bob" ,
  showName: function (){
   alert( this .name);
  }
};
var other = {
  name: "Tom" ,
  showName: someone.showName
}
other.showName();   //Tom
 
//以上函数至关于
 
var other = {
  name: "Tom" ,
  showName: function (){
   alert( this .name);
  }
}
other.showName();   //Tom

【复合场景2】

?
1
2
3
4
5
6
7
8
9
10
11
12
var name = 2;
var a = {
  name: 3,
  fn: ( function (){
   alert( this .name);
  })(),
  fn1: function (){
   alert( this .name);
  }
}
a.fn; //2[匿名函数中的this指向全局对象]
a.fn1(); //3[对象内部函数的this指向调用函数的当前对象]

【复合场景3】

?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var name = "Bob" ;
var nameObj ={
  name : "Tom" ,
  showName : function (){
  alert( this .name);
},
  waitShowName : function (){
   var that = this ;
   setTimeout( function (){
    that.showName();
   }, 1000);
  }
};
nameObj.waitShowName(); //"Tom"[that=this改变this的指向,使this从指向全局变量变化到指向nameObj]
 
var name = "Bob" ;
var nameObj ={
  name : "Tom" ,
  showName : function (){
   alert( this .name);
  },
  waitShowName : function (){
   var that = this ; //that指向nameObj
   setTimeout( function (){
    ( function (){
     alert( this .name);
    })();
   }, 1000);
  }
};
nameObj.waitShowName(); // 'Bob'[造成匿名函数,this指向全局变量]

 

call 和 apply 的共同点

它们的共同点是,都可以改变函数执行时的上下文,将一个对象的方法交给另外一个对象来执行,而且是当即执行的。

为什么要改变执行上下文?举一个生活中的小例子:平时没时间作饭的我,周末想给孩子炖个腌笃鲜尝尝。可是没有适合的锅,而我又不想出去买。因此就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一箭双雕。

改变执行上下文也是同样的,A 对象有一个方法,而 B 对象由于某种缘由,也须要用到一样的方法,那么这时候咱们是单独为 B 对象扩展一个方法呢,仍是借用一下 A 对象的方法呢?固然是借用 A 对象的啦,既完成了需求,又减小了内存的占用。

另外,它们的写法也很相似,调用 call 和 apply 的对象,必须是一个函数 Function。接下来,就会说到具体的写法,那也是它们区别的主要体现。

call 和 apply 的区别

它们的区别,主要体如今参数的写法上。先来看一下它们各自的具体写法。

call 的写法

Function.call(obj,[param1[,param2[,…[,paramN]]]])

须要注意如下几点:

  • 调用 call 的对象,必须是个函数 Function。
  • call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。若是不传,则默认为全局对象 window。
  • 第二个参数开始,能够接收任意个参数。每一个参数会映射到相应位置的 Function 的参数上。可是若是将全部的参数做为数组传入,它们会做为一个总体映射到 Function 对应的第一个参数上,以后参数都为空。
function func (a,b,c) {} func.call(obj, 1,2,3) // func 接收到的参数其实是 1,2,3 func.call(obj, [1,2,3]) // func 接收到的参数其实是 [1,2,3],undefined,undefined

apply 的写法

Function.apply(obj[,argArray])

须要注意的是:

  • 它的调用者必须是函数 Function,而且只接收两个参数,第一个参数的规则与 call 一致。
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,而且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
func.apply(obj, [1,2,3]) // func 接收到的参数其实是 1,2,3 func.apply(obj, { 0: 1, 1: 2, 2: 3, length: 3 }) // func 接收到的参数其实是 1,2,3

什么是类数组?

先说数组,这咱们都熟悉。它的特征有:能够经过角标调用,如 array[0];具备长度属性length;能够经过 for 循环或forEach方法,进行遍历。

那么,类数组是什么呢?顾名思义,就是具有与数组特征相似的对象。好比,下面的这个对象,就是一个类数组。

let arrayLike = { 0: 1, 1: 2, 2: 3, length: 3 };

类数组 arrayLike 能够经过角标进行调用,具备length属性,同时也能够经过 for 循环进行遍历。

类数组,仍是比较经常使用的,只是咱们平时可能没注意到。好比,咱们获取 DOM 节点的方法,返回的就是一个类数组。再好比,在一个方法中使用 arguments 获取到的全部参数,也是一个类数组。

可是须要注意的是:类数组没法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。

call 和 apply 的用途

下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪一个场景是必须用 call 或者必须用 apply 的,只是我的习惯这么用而已。

call 的使用场景

一、对象的继承。以下面这个例子:

function superClass () { this.a = 1; this.print = function () { console.log(this.a); } } function subClass () { superClass.call(this); this.print(); } subClass(); // 1

subClass 经过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还能够扩展本身的其余方法。

二、借用方法。还记得刚才的类数组么?若是它想使用 Array 原型链上的方法,能够这样:

let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

这样,domNodes 就能够应用 Array 下的全部方法了。

apply 的一些妙用

一、Math.max。用它来获取数组中最大的一项。

let max = Math.max.apply(null, array);

同理,要获取数组中最小的一项,能够这样:

let min = Math.min.apply(null, array);

二、实现两个数组合并。在 ES6 的扩展运算符出现以前,咱们能够用 Array.prototype.push来实现。

let arr1 = [1, 2, 3]; let arr2 = [4, 5, 6]; Array.prototype.push.apply(arr1, arr2); console.log(arr1); // [1, 2, 3, 4, 5, 6]

bind 的使用

最后来讲说 bind。在 MDN 上的解释是:bind() 方法建立一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表做为原函数的参数序列的前若干项。

它的语法以下:

Function.bind(thisArg[, arg1[, arg2[, ...]]])

bind 方法 与 apply 和 call 比较相似,也能改变函数体内的 this 指向。不一样的是,bind 方法的返回值是函数,而且须要稍后调用,才会执行。而 apply 和 call 则是当即调用。

来看下面这个例子:

function add (a, b) {
    return a + b; } function sub (a, b) { return a - b; } add.bind(sub, 5, 3); // 这时,并不会返回 8 add.bind(sub, 5, 3)(); // 调用后,返回 8

若是 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。

总结

call 和 apply 的主要做用,是改变对象的执行上下文,而且是当即执行的。它们在参数上的写法略有区别。

bind 也能改变对象的执行上下文,它与 call 和 apply 不一样的是,返回值是一个函数,而且须要稍后再调用一下,才会执行。

相关文章
相关标签/搜索