JavaScript学习(七) —— call、apply、bind

介绍完this的指向问题,有必要专门说一下call、apply、bind三个能改变this指向的函数方法。

在JS中,函数本身也是对象,它也有构造函数(Function),call、apply、bind三个方法定义在函数的构造函数的原型上(Function.prototype),我们声明的函数(即一个Function的实例化对象)都会获得这三个方法。

先对比一下call和apply方法:
var str = 'global';
var obj = {
  str : 'local'
}
function getStr(str1,str2){
  return this.str + '-' + str1 + '-' + str2;
}
console.log(getStr('1','2')); // 'global-1-2'
console.log(getStr.call(obj,'1','2')); // 'local-1-2'
console.log(getStr.apply(obj,['1','2'])); // 'local-1-2'
  • 调用getStr的call或apply方法,都直接调用了getStr
  • call和apply方法可以改变函数的执行环境,把函数getStr内this指向传入的第一个参数的对象obj
  • call和apply区别在于call从第二个参数开始是给getStr分开传递的参数,apply则是把所有要传递给getStr的参数整合成一个数组作为第二个参数。

具体应用:

例子1:

有个由不重复数字元素组成的数组,要取其中最大的元素,像这样遍历一遍?

var arr = [1,10,2,5,8];
function max(){
  var num = 0;
  for( var i = 0; i < arr.length; i++ ){
    if( num < arr[i] ){
      num = arr[i]
    }
  }
  return num;
}
console.log(max(arr)); // 10

还是用sort排序法?

var arr = [1,10,2,5,8];
function sortNumber( a, b ){ // 对比方法
  return b - a;
}
function getCopyArr(arr){ // 复制数组
  return [].concat(arr); 
}
console.log(getCopyArr(arr).sort(sortNumber)[0]); // 10
console.log(arr); // [1,10,2,5,8]

注意:sort会改变原数组,如果不想改变,要复制一份。

如果用apply方法:

var arr = [1,10,2,5,8];
console.log(Math.max.apply(null,arr)); // 10

一行代码足矣。因为数组本身没有max方法,借用一下Math的max方法,apply的第一个参数传null,或者this、window、undefined都可以,因为并没有要把this转给谁,只要占个参数位置即可,第二个参数传数组本身。

例子2: 判断数据类型

判断数据类型有 typeof、constructor、instanceof 和 toString 方法

Object.prototype.toString.call(10) ;    // [object Number]
Object.prototype.toString.call('10') ;   // [object String]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(function(){}) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

其中只有借用Object.prototype的toString方法最好,其他三种都有缺陷不做过多介绍。
toString方法只有通过上面的方式用call或apply调用才能输出类似"[object Object]"这样格式的字符串,它是对象构造函数的一个内部属性 [[Class]] ,如果直接通过对象实例调用toString会把调用的对象转化成普通字符串。

bind方法:
var str = "global";
var obj = {
  str : "local",
  getStr : function( str1, str2 ){
    return this.str + '-' + str1 + '-' + str2;
  }
}
var fn1 = obj.getStr;
var fn2 = obj.getStr.bind(obj);
var fn3 = obj.getStr.bind(obj,'1','3');
console.log(fn1('1','2')) // "global-1-2"
console.log(fn2('1','2')) // "local-1-2"
console.log(fn3('1','2')) // "local-1-3"
console.log(fn3()) // "local-1-3"

使用和不使用bind的区别在于,bind会把函数内this指向调用bind方法时传入的对象,不使用bind,根据是不是严格模式判断this是指向undefined还是它的调用者。

注意: 上面传参的方式,bind方法第一个参数后面可以不传参数,传参的话和call方法一样是一个一个传,但是在bind方法中直接给调用方法传参会影响真正调用时传的参数。

再看这个例子:

var str = "global";
var obj = {
  str : "local",
  getStr1 : function(){
    setTimeout(function(){
      console.log(this.str);
    },1000);
  },
  getStr2 : function(){
    setTimeout(function(){
      console.log(this.str);
    }.bind(this),1000);
  }
}
obj.getStr1(); // "global"
obj.getStr2(); // "local"

对比一下结果。

这里需要解释一下,setTimeout传入的第一个参数的函数,不管它是一个匿名函数,还是定义在什么地方的有名字的函数,问题的关键是setTimeout内部是通过什么方式调用它的。

看这个例子:

"use strict";
var str = "global";
var obj = {
  str : "local",
  getStr1 : function(){
    console.log(this.str);
  },
  getStr2 : function(){
    setTimeout(this.getStr1,1000);
  },
  getStr3 : function(){
    setTimeout(this.getStr1.bind(this),1000);
  }
}
obj.getStr1(); // 直接输出 "local"
obj.getStr2(); // 1秒后输出 "global"
obj.getStr3(); // 1秒后输出 "local"

即使在严格模式下,代码第9行 setTimeout 的 this.getStr1 函数没调用bind(this)方法,代码第16行输出"global",this.getStr1 函数内部的 this 仍指向 window。

我得出结论: JS内部是这样处理的:setTimeout 第一个参数的函数应该是用window对象调用的,它不是严格模式中的独立调用,哪位同学有不同看法欢迎一起讨论。

有点跑题,我们说回bind方法。

还看一个和setTimeout有关的例子:

for(var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

我们当然期望它每隔1秒顺序输出0到9。
可结果是每隔1秒输出10,连续输出10个10。

我们都知道setTimeout是异步操作,JS遇到异步操作会将这个操作加入到一个事件队列,往下执行同步操作,待同步操作完成后去事件队列完成异步操作。这个JS事件循环机制以及微任务和宏任务概念后续详解(又后续?)。

你一定知道可以通过自执行函数创建闭包的方式解决:

for(var i = 0; i < 10; i++) {
  (function(i){
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

或者这样:

for(var i = 0; i < 10; i++) {
  setTimeout(function(i) {
    console.log(i);
  }, i * 1000, i);
}

别忘了,setTimeout从第三个参数开始是往第一个参数的函数里传参,可能很多人都不知道这点。其实这样解决也挺好。

可是你想过用bind解决吗,毕竟我们今天都在说call、apply、bind的相关内容:

for(var i = 0; i < 10; i++) {
  setTimeout(console.log.bind(null, i), i * 1000);
}

这样多简洁,连外层的function(){}都省略了,毕竟函数是一等公民嘛!console.log 它不也是个函数嘛!可能有人不理解这里bind的意思,其实我们没用到this,bind方法第一个参数传null就能看出来,其实我们利用了第二个参数传参的特性,它保证了参数和输出的即时性。

总结:

bind方法和call和apply方法都能改变this指向。区别是它们除了传参数方式有点不同外,还有bind方法并没有调用原函数。call和apply方法在执行的时候同时也调用了原函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值