this call apply bind

38 阅读7分钟

this的指向是调用时决定的,而不是创建时决定的。(和词法作用域做区分)

this永远指向 最后 调用它的对象!

this指向

全局上下文

在全局执行上下文中this都指代全局对象。

浏览器中this等价于window对象

函数上下文

在函数内部,this的值取决于函数被调用的方式。

直接调用

this指向全局变量。

function foo(){
  return this;
}
console.log(foo() === window); // true

作为对象的一个方法

this指向调用函数的对象。

var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
console.log(person.getName()); // axuebin

这里有一个需要注意的地方

var name = "xb";
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
var getName = person.getName;
console.log(getName()); // xb

发现this又指向全局变量了,这是为什么呢?

还是那句话,this的指向得看函数调用时。第9行在全局环境调用的。

作为一个构造函数

this被绑定到正在构造的新对象。

通过构造函数创建一个对象其实执行这样几个步骤:

  1. 创建新对象
  2. 将 this 指向这个对象
  3. 给对象赋值(属性、方法)
  4. 返回 this

所以this就是指向新创建的这个对象上。

function Person(name){
  this.name = name;
  this.age = 25;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
var person = new Person("axuebin");
console.log(person.name); // axuebin
person.say(); // axuebin:25

作为一个DOM事件处理函数

this指向触发事件的元素

var ele = document.getElementById("id");
ele.addEventListener("click",function(e){
  console.log(this);
  console.log(this === e.target); // true
})

测试(好好看一下)

// demo01
var a = 20;
function fn() {
  var a = 10;
  console.log(this.a);
}
fn();

// demo02
var a = 20;
function fn() {
  var a = 10;
  function foo() {
    var a = 5;
    console.log(this.a);
  }
  foo();
}
fn();

// demo03
var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);
console.log(obj.fn());
console.log(window.obj.fn());

答案:

demo1、demo2 都是20

demo3 第一个是40,第二个是10,第三个也是10

解析:

在一个函数上下文中,this 由调用者提供,由调用函数的方式来决定。

如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的 this 指向该对象。

如果函数独立调用,那么该函数内部的 this,则指向 undefined。在非严格模式中,当 this 指向 undefined 时,它会被自动指向全局对象。

重点是在哪里调用,关注(),还要关注最后调用的对象是哪个

demo1、demo2 的 fn() 是独立调用,调用的 this 指向全局对象。

demo3 的 obj.c 属性使用 this.a + 20 来计算。没有 function 字样单独的 {} 不会形成新的作用域,因此这里的 this.a,由于并没有作用域的限制,仍然处于全局作用域之中。

而 obj.fn() 中fn()是函数,调用对象是obj,所以 this 指向 obj;

而 window.obj.fn() 实质和 obj.fn() 相同,最后调用者为 obj。

再来看一个例子

var a = 20;
var foo = {
  a: 10,
  getA: function () {
    return this.a;
  }
}
console.log(foo.getA());

var test = foo.getA;
console.log(test());

foo.getA() 中,getA 是调用者,他不是独立调用,被对象 foo 所拥有,因此它的 this 指向了 foo。

而 test() 作为调用者,尽管他与 foo.getA 的引用相同,但是它是独立调用的,因此 this 指向 undefined,在非严格模式,自动转向全局 window。

稍微修改一下代码就可以理解了:

var a = 20;
function getA() {
  return this.a;
}
var foo = {
  a: 10,
  getA: getA
}
console.log(foo.getA()); 

var test = foo.getA;
console.log(test());

同理:

var obj1 = {
  name: 'obj1',
  fn: function () {
    console.log(this.name);
  }
};

var obj2 = { name: 'obj2' };

var newFn = obj1.fn;

obj2.fn = newFn;

obj2.fn();

上面代码实质就是 obj2 调用自己对象的 fn 函数

最后来一个:

function foo() {
  console.log(this.a)
}

function active(fn) {
  fn(); // 真实调用者,为独立调用
}

var a = 20;
var obj = {
  a: 10,
  getA: foo
}

active(obj.getA);

改变 this 指向

改变 this 的指向我总结有以下几种方法:

  • 使用callapplybind
  • 使用 ES6 的箭头函数
  • 在函数内部使用 _this = this
  • 构造函数 new 实例化一个对象

call apply bind

this指向绑定的对象上。

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)
var person = {
  name: "axuebin",
  age: 25
};
function say(job,word){
  console.log(this.name+":"+this.age+":"+job+":"+word);
}
say.call(person,"FE","HH"); // axuebin:25:FE:HH
say.apply(person,["FE","HH"]); // axuebin:25:FE:HH
var bindFn = say.bind(person,"FE","HH")
bindFn()

callapplythis的绑定角度上来说是一样的,唯一不同的是它们的第二个参数。

call是每个参数一个一个传入,apply传入参数数组。

bind返回一个函数可稍后执行,而callapply是立即调用,传参和call类似。

箭头函数

箭头函数不会创建自己的this ,它 只会从作用域链的上一层继承this

箭头函数常用在回调函数中,例如定时器

var name = "windowsName";

var a = {
  name: "Cherry",

  func1: function () {
    console.log(this.name)
  },

  func2: function () {
    setTimeout(function () {
      this.func1()
    }, 100);
  },
  
  func3: function () {
    setTimeout(() => {
      this.func1()
    }, 100);
  },

  func4: function () {
    that = this;	    	// 用 that 保存 this
    setTimeout(function () {
      that.func1()
    }, 100);
  }
  
};

a.func2()     // this.func1 is not a function
a.func3()     // Cherry
a.func4()     // Cherry

不使用箭头函数会报错,因为最后调用 setTimeout 的对象是 window, window 中并没有 func1 函数。

var a = 'global'
function foo() {
  var a = 'foo'
  setTimeout(() => {
    console.log(this.a);
  },1000)
}
var obj = {
  a: 'obj'
}
foo()
foo.call(obj);

用箭头函数输出 global obj,不用的话是 global global

在函数内部使用 that = this

将调用这个函数的对象保存在变量 that 中,然后在函数中都使用这个 that

var name = "windowsName";
var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)     
    },
    func2: function () {
        var that = this;
        setTimeout(function() {
            that.func1()
        },100);
    }
};
a.func2()       // Cherry

在 func2 中,设置var that = this,这里的this指向调用 func2 的对象 a,为了防止在 func2 中的 setTimeout 被 window 调用而导致的在 setTimeout 中的this为 window。我们将this(指向变量a)赋值给一个变量that,这样,在 func2 中我们使用that就是指向对象 a 了。

this 总结

如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。

然后可以顺序按照下面规则来判断this的绑定对象:

  1. new调用:绑定到新创建的对象
  2. callapplybind调用:绑定到指定的对象
  3. 由上下文对象调用:绑定到上下文对象
  4. 默认:全局对象
  5. 箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。

手写 call apply bind

fn.call(target, 1, 2)

实现步骤:

  1. 判断调用对象是否为函数。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 处理传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性。
  7. 返回结果。
// call函数实现
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }

  // 获取参数
  let args = [...arguments].slice(1),
  let result = null;

  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;

  // 将调用函数设为对象的方法,context是对象,this是调用函数
  context.fn = this;

  // 调用函数
  result = context.fn(...args);

  // 将方法删除
  delete context.fn;

  return result;
};

fn.apply(target, [1, 2])

和call基本一致,除了参数处理不同

// apply 函数实现

Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  let result = null;

  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;

  // 将函数设为对象的方法
  context.fn = this;

  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  // 将属性删除
  delete context.fn;

  return result;
};

fn.bind(target)(1,2)

实现步骤:

  1. 判断调用对象是否为函数。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
// bind 函数实现
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 获取参数
  var args = [...arguments].slice(1)
  var fn = this;

  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

参考

全方位解读this

JavaScript基础心法——this

this、apply、call、bind