New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript中call,apply,bind以及this的理解 #7

Open
gnipbao opened this Issue Apr 15, 2017 · 0 comments

Comments

Projects
None yet
1 participant
@gnipbao
Copy link
Owner

gnipbao commented Apr 15, 2017

说明

在JavaScript中调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义形参,每个函数接收两个附加的参数:thisarguments。参数this在面向对象中非常重要,它取决于调用的模式。在JavaScript中共有四种**调用模式:**方法调用模式、函数调用模式、构造器调用模式、和apply(),call()方法调用模式。这些模式在如何初始化关键参数this存在差异。本文首先要提到的是this,抛开this单独去说这些方法是没有意义的。然后是如何妙用call,apply,bind这些方法去改变this的指向。

目录

  • this在不同模式下的意义
  • 借鸡下蛋之妙用call,apply
  • 深入理解bind函数

this在不同模式下的意义

  1. 全局上下文
    在全局运行上下文中(在任何函数体外部),this 指代全局对象,无论是否在严格模式下。例如在浏览器环境中任何定义在全局的属性,方法都将成为全局对象window的属性和方法。
console.log(this.document === document); // true
// 在浏览器中,全局对象为 window 对象:
console.log(this === window); // true
this.a = 37;
console.log(window.a); // 37
  1. 函数上下文
    在函数内部,this的值取决于函数是如何调用的。
//直接调用
function f1(){
  return this;
}
f1() === window; // true
//this的值不是由函数调用设定。因为代码不是在严格模式下执行,this 的值总是一个对象且默认为全局对象
function f2(){
  "use strict"; // 这里是严格模式
  return this;
}
f2() === undefined; // true
//在严格模式下,this 是在进入运行环境时设置的。若没有定义,this的值将维持undefined状态。也可能设置成任意值。
  1. 对象方法中的this
    当以对象里的方法的方式调用函数时,它们的 this 是调用该函数的对象.
    下面的例子中,当 o.f() 被调用时,函数内的this将绑定到o对象。
var o = {
  prop: 38,
  f: function() {
    return this.prop;
  }
};
console.log(o.f()); // logs 38

注意,在何处或者如何定义调用函数完全不会影响到this的行为。在上一个例子中,我们在定义o的时候为其成员f定义了一个匿名函数。但是,我们也可以首先定义函数然后再将其附属到o.f。这样做this的行为也一致:

var o = {prop: 37};
function independent() {
  return this.prop;
}
o.f = independent;
console.log(o.f()); // logs 37

这说明this的值只与函数 f 作为 o 的成员被调用有关系。
类似的,this 的绑定只受最靠近的成员引用的影响。在下面的这个例子中,我们把一个方法g当作对象o.b的函数调用。在这次执行期间,函数中的this将指向o.b。事实上,这与对象本身的成员没有多大关系,最靠近的引用才是最重要的。

o.b = {
  g: independent,
  prop: 42
};
console.log(o.b.g()); // logs 42
  1. 原型链中的this
    相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么this指向的是调用这个方法的对象,表现得好像是这个方法就存在于这个对象上一样。
var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5

在这个例子中,对象p没有属于它自己的f属性,它的f属性继承自它的原型。但是这对于最终在o中找到f属性的查找过程来说没有关系;查找过程首先从p.f的引用开始,所以函数中的this指向p。也就是说,因为f是作为p的方法调用的,所以它的this指向了p。这是JavaScript的原型继承中的一个有趣的特性。
5. getter 与 setter 中的 this
再次,相同的概念也适用时的函数作为一个 getter 或者 一个setter调用。作为getter或setter函数都会绑定 this 到从设置属性或得到属性的那个对象。

function modulus(){
  return Math.sqrt(this.re * this.re + this.im * this.im);
}
var o = {
  re: 1,
  im: -1,
  get phase(){
    return Math.atan2(this.im, this.re);
  }
};
Object.defineProperty(o, 'modulus', {
  get: modulus, enumerable:true, configurable:true});
console.log(o.phase, o.modulus); // logs -0.78 1.4142
  1. 构造函数中的 this
    当一个函数被作为一个构造函数来使用(使用new关键字),它的this与即将被创建的新对象绑定。
    注意:当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回this。
function C(){
  this.a = 37;
}
var o = new C();
console.log(o.a); // logs 37
function C2(){
  this.a = 37;
  return {a:38};
}
o = new C2();
console.log(o.a); // logs 38
  1. DOM事件处理函数中的 this
    当函数被用作事件处理函数时,它的this指向触发事件的元素(一些浏览器在动态添加监听器时不遵守这个约定,除非使用addEventListener 这句不太确定翻译的是否正确)。
// 被调用时,将关联的元素变成蓝色
function bluify(e){
  console.log(this === e.currentTarget); // 总是 true
  // 当 currentTarget 和 target 是同一个对象是为 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}
// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

借鸡下蛋之妙用call,apply

apply

fun.apply(thisArg[, argsArray])方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。

  • thisArg 在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数

在调用一个存在的函数时,你可以为其指定一个 this 对象。 this指当前对象,也就是正在调用这个函数的对象。使用apply,你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量.你也可以使用 arguments 对象作为 argsArray 参数。 arguments 是一个函数的局部变量。 它可以被用作被调用对象的所有未指定的参数。 这样,你在使用apply函数的时候就不需要知道被调用对象的所有参数。 你可以使用arguments来把所有的参数传递给被调用对象。 被调用对象接下来就负责处理这些参数。

  1. 使用apply来链接构造器
Function.prototype.construct = function (aArgs) {
  var oNew = Object.create(this.prototype);
  this.apply(oNew, aArgs);
  return oNew;
};
//另一种可选的方法是使用闭包
Function.prototype.construct = function(aArgs) {
  var fConstructor = this, fNewConstr = function() { 
    fConstructor.apply(this, aArgs); 
  };
  fNewConstr.prototype = fConstructor.prototype;
  return new fNewConstr();
};
  1. 使用apply和内置函数
function minOfArray(arr) {
  var min = Infinity;
  var QUANTUM = 32768;
  for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
    var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
    min = Math.min(submin, min);
  }
  return min;
}
var min = minOfArray([5, 6, 2, 3, 7]);
  1. 在"monkey-patching"中使用apply
var originalfoo = someobject.foo;
someobject.foo = function() {
  //在调用函数前干些什么
  console.log(arguments);
  //像正常调用这个函数一样来进行调用:
  originalfoo.apply(this,arguments);
  //在这里做一些调用之后的事情。
}
call

fun.call(thisArg[, arg1[, arg2[, ...]]]) 使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法.该方法的作用和 apply() 方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。通过 call 方法,你可以在一个对象上借用另一个对象上的方法,比如Object.prototype.toString.call([]),就是一个Array对象借用了Object对象上的方法。

  1. 使用call方法调用父构造函数
    在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承,类似于Java中的写法。下例中,使用 Food 和 Toy 构造函数创建的对象实例都会拥有在 Product 构造函数中添加的 name 属性和 price 属性,但 category 属性是在各自的构造函数中定义的。
function Product(name, price) {
  this.name = name;
  this.price = price;
  if (price < 0) {
    throw RangeError('Cannot create product ' +
                      this.name + ' with a negative price');
  }
  return this;
}
function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}
Food.prototype = Object.create(Product.prototype);
Food.prototype.constructor = Food; // Reset the constructor from Product to Food
function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}
Toy.prototype = Object.create(Product.prototype);
Toy.prototype.constructor = Toy; // Reset the constructor from Product to Toy
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
  1. 使用call方法调用匿名函数
var animals = [
  {species: 'Lion', name: 'King'},
  {species: 'Whale', name: 'Fail'}
];
for (var i = 0; i < animals.length; i++) {
  (function (i) { 
    this.print = function () { 
      console.log('#' + i  + ' ' + this.species + ': ' + this.name); 
    } 
    this.print();
  }).call(animals[i], i);
}
  1. 使用call方法调用匿名函数并且指定上下文的'this'
function greet() {
  var reply = [this.person, 'Is An Awesome', this.role].join(' ');
  console.log(reply);
}
var i = {
  person: 'Douglas Crockford', role: 'Javascript Developer'
};
greet.call(i); // Douglas Crockford Is An Awesome Javascript Developer

当一个函数的函数体中使用了this关键字时,通过所有函数都从Function对象的原型中继承的call()方法和apply()方法调用时,它的值可以绑定到一个指定的对象上。

 function add(c, d){
   return this.a + this.b + c + d;
 }
 var o = {a:1, b:3};
 // The first parameter is the object to use as 'this', subsequent parameters are passed as 
 // arguments in the function call
 add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
 // The first parameter is the object to use as 'this', the second is an array whose
 // members are used as the arguments in the function call
 add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用 call 和 apply 函数的时候要注意,如果传递的 this 值不是一个对象,JavaScript 将会尝试使用内部 ToObject 操作将其转换为对象。因此,如果传递的值是一个原始值比如 7 或 'foo' ,那么就会使用相关构造函数将它转换为对象,所以原始值 7 通过new Number(7)被转换为对象,而字符串'foo'使用 new String('foo') 转化为对象,例如

 function bar() {
   console.log(Object.prototype.toString.call(this));
 }
 bar.call(7); // [object Number]
 

bind函数

fun.bind(thisArg[, arg1[, arg2[, ...]]])bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

  1. 创建绑定函数
    bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象。(比如在回调中传入这个方法。)如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题:
his.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX(); // 9, because in this case, "this" refers to the global object
// Create a new function with 'this' bound to module
//New programmers (like myself) might confuse the global var getX with module's property getX
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
  1. 偏函数(Partial Functions)
    bind()的另一个最简单的用法是使一个函数拥有预设的初始参数。这些参数(如果有的话)作为bind()的第二个参数跟在this(或其他对象)后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。
function list() {
  return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
  1. 配合 setTimeout
    在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window (或全局)对象。当使用类的方法时,需要 this 引用类的实例,你可能需要显式地把 this 绑定到回调函数以便继续使用实例。
function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};
var flower = new LateBloomer();
flower.bloom();  // 一秒钟后, 调用'declare'方法
  1. 快捷调用
    在你想要为一个需要特定的 this 值得函数创建一个捷径(shortcut)的时候,bind() 方法也很好用.你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,就拿它来举例子吧。你可以创建这样一个捷径:
var slice = Array.prototype.slice;
// ...
slice.apply(arguments);

用 bind() 可以使这个过程变得简单。在下面这段代码里面,slice 是 Function.prototype 的 call() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。这意味着我们压根儿用不着上面那个 apply() 调用了。

// same as "slice" in the previous example
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);
// ...
slice(arguments);

ECMAScript 5 引入了 Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

function f(){
  return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var o = {a:37, f:f, g:g};
console.log(o.f(), o.g()); // 37, azerty

以上内容大部分来自MDN读者可以自行去研究;

@gnipbao gnipbao pinned this issue Jan 31, 2019

@gnipbao gnipbao unpinned this issue Jan 31, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment