通过this深入理解javascript函数的bindcallapply

11次阅读

共计 3320 个字符,预计需要花费 9 分钟才能阅读完成。

我们都知道函数这个高级公民在 js 中的地位很高。用处很大,可是却很难搞!函数内部有一个很厉害的内部属性 this 关键字。而 bind、call、apply 是函数的 3 个非继承方法 (但是在原型链中找到了这些方法!Fuction.prototype 中找到了!所以我觉得是继承自Fuction原型对象),他们的作用都是用来改变 this 指向的。那么 this 到底是什么东西?有什么作用?

了解函数中的 this

this 引用的是函数据以执行的 环境对象。也就是说 this 就像函数的一个指针,会指向函数执行环境。看一个例子:

window.color = 'red'
function say(){console.log(this.color)
}
say() //red
window.say() // 与 say() 等价与

可以看到,this 指向了 window 对象。因为是 window 调用了 say 函数,所以 say 函数的执行环境 widow;看下面的例子:

window.color = 'red'
var obj = {
    color:'yellow',
    say:function(){console.log(this.color)
    }
}

obj.say(); //yellow
// 此时 say 方法是做为 obj 的方法调用,也就是说 obj 是 say 函数的执行环境对象,所以 this 也就指向了 obj
var fun = obj.say;
fun() //red

将 obj 的 say 方法赋值给 fun 后,调用 fun 后发现 this 指向了 window?也就是说函数此时的执行环境是 window?
这里需要明白一点,函数名仅仅是一个 包含指针 的变量,函数是复杂数据类型,所以函数名就只是一个指针,指向堆中的内存地址!所以 fun 此时只是复制了指针地址,写成下面的写法就会看的清晰了:

window.color = 'red'
function say(){console.log(this.color)
}
var obj = {
    color:'yellow',
    say:say
}

obj.say(); //yellow
var fun = say;
window.fun() //red

apply 和 call

js 提供了一些可以改变函数执行作用域的方法。因为普通函数如果通过上面的写法来改变 this 执行时上下文,写法就太过于麻烦。

apply:
语法:apply(thisObj, 数组参数)
定义:应用某一个对象的一个方法,用另一个对象替换当前对象
说明:如果参数不是数组类型的,则会报一个 TypeError 错误。

call 方法:
语法:call(thisObj, arg1, arg2, argN)
定义:调用一个对象的一个方法,以另一个对象替换当前对象。
说明:call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。
如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj。
apply 与 call 的唯一区别就是 接收参数的格式不同。

window.color = 'red'
function say(){console.log(this.color)
}
var obj = {color:'yellow'}

say.call(obj); //yellow

bind 方法

bind()方法创建一个新的函数,在 bind()被调用时,这个新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

var module = {
  x: 42,
  getX: function() {return this.x;}
}

var unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

var boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

需要注意:

1. 作为构造函数使用的绑定函数

function A(a,b){
    this.a=a;
    this.b=b
    console.log(this)
}
var a = A.bind({x:1},1)
var b = a(5) // {x: 1, a: 1, b: 5}

function A(a,b){
    this.a=a;
    this.b=b
    console.log(this)
}
var a = A.bind({x:1},1)// 作为构造函数使用的绑定函数
var b = new a(5) // {a: 1, b: 5}

可以看出来:使用 new 操作符去构造一个由目标函数创建的新实例。bind 提供的 this 就会被 忽略 。不过提供的 参数列表 仍然会插入到构造函数调用时的参数列表之前。

2.bind 的实现

Function.prototype.bind = function(context){var args = Array.prototype.slice(arguments, 1),
  F = function(){},
  self = this,
  bound = function(){var innerArgs = Array.prototype.slice.call(arguments);
      var finalArgs = args.concat(innerArgs);
      return self.apply((this instanceof F ? this : context), finalArgs);
  };

  F.prototype = self.prototype;
  bound.prototype = new F();
  retrun bound;
};

3.js 中当函数执行 bind 后再次执行 bind 或 call 时会怎样

var test = function(x,y){console.log(this,arguments)
}
var a=test.bind({s:1},1)
a.call({d:1},2) // {s: 1} 1 2
var b = a.bind({d:1},2)
b()// {s: 1} 1 2

var c = b.bind({e:3},3) 
c()// {s: 1} 1 2 3

es5 文档中中说到如果我们在一个由 bind 创建的函数中调用 call,假设是 x.call(obj,y,z,…)并且传入 this,和参数列表的时候会执行下面的步骤:
1. 首先用三个参数分别保存 函数 x 函数的内部属性中存的 this 值 目标函数 参数列表
2. 然后执行目标函数的内部 call 函数,也就是执行目标函数的代码,并且传入 1 中保存的 this 和实参(这里的实参是目标函数本来就有的也就是 bind 时传入的实参加上调用 call 时传的实参)
重点在 1 中,从 es5 的 bind 函数说明中我们知道,当我们用一个函数调用 bind 的时候,返回的函数中会保存这三个参数。所以最后调用 call 的时候执行的函数是目标函数,也就是调用了 bind 的函数,传入的 this 也是 bind 调用时传入的,这些都是无法被修改的了,但是参数是调用 bind 和 call 时的叠加,这是我们唯一可以修改的地方。执行两次 bind 的原理可以参考 bind 的源码,和 call 的差不多,也是目标函数和 this 是被固定的了,只有参数列表会叠加。

箭头函数

箭头函数与普通函数些不同,有几个使用注意点。

(1)函数体内的 this 对象,就是定义时所在的对象 ,而不是 使用时所在的对象

(2)不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。

(3)不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

function foo() {return () => {return () => {return () => {console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

正文完
 0