this+call、apply、bind的区别与使用
http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
https://segmentfault.com/a/1190000018017796
在函数中 this 到底取何值,是在函数真正被调用执行的时候确定下来的,函数定义的时候确定不了。
因为 this 的取值是函数执行上下文(context)的一部分,每次调用函数,都会产生一个新的执行上下文环境。当代码中使用了 this,这个 this 的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。
关于 this 的取值,大体上可以分为以下七种情况:
由于严格模式下,禁止this指向全局对象,所以以下示例均运行在非严格模式下
情况一:全局 & 调用普通函数
在全局环境中,this 永远指向 window。
console.log(this === window);     //true
普通函数在调用时候(注意不是构造函数,前面不加 new),其中的 this 也是指向 window。
var x = 10;
function foo(){
    console.log(this);     //Window
    console.log(this.x);   //10
}
foo();
匿名函数中的this指向全局对象window
var x = 10;
var foo(){
  a: 20,   
  fn:(function(){
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 的使用场景
1、对象的继承。如下面这个例子:
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 还可以扩展自己的其他方法。
2、借用方法。还记得刚才的类数组么?如果它想使用 Array 原型链上的方法,可以这样:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
这样,domNodes 就可以应用 Array 下的所有方法了。
apply 的一些妙用
1、Math.max。用它来获取数组中最大的一项。
let max = Math.max.apply(null, array);
同理,要获取数组中最小的一项,可以这样:
let min = Math.min.apply(null, array);
2、实现两个数组合并。在 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 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
this+call、apply、bind的区别与使用的更多相关文章
- call apply bind的区别
		
都是天生自带的内置方法(Function.prototype),所有的函数都可以调取这三个方法,改变this指向 call 语法:fn.call(context,para1......) 把fn方法执 ...
 - call apply bind 的区别
		
1.call和apply都是对函数的直接调用,而bind方法返回的仍然是一个函数,因此后面还需要()来进行调用才可以 var xw={ name: "小王", gender: &q ...
 - js 中call,apply,bind的区别
		
call.apply.bind方法的共同点与区别: apply.call.bind 三者都是用来改变函数的this对象的指向: apply.call.bind 三者都可以利用后续参数传参: bind ...
 - call、apply/bind的区别和用法(简单粗暴的解释)
		
var obj1={ name:"bob", age:20 } var obj2={ name:"coco", age:22 } function getAge ...
 - JS中call,apply,bind的区别
		
1.关于this对象的指向,请看如下代码 var name = 'jack'; var age = 18; var obj = { name:'mary', objAge:this.age, myFu ...
 - 浅谈call apply bind的区别
		
这三个方法的用法非常相似,将函数绑定到上下文中,即用来改变函数中this的指向.举个例子: var zlw = { name: "zlw", sayHello: function ...
 - javascript中的call(),apply(),bind()方法的区别
		
之前一直迷惑,记不住call(),apply(),bind()的区别.不知道如何使用,一直处于懵懂的状态.直到有一天面试被问到了这三个方法的区别,所以觉得很有必要总结一下. 如果有不全面的地方,后续再 ...
 - 有关call和apply、bind的区别及this指向问题
		
call和apply都是解决this指向问题的方法,唯一的区别是apply传入的参数除了其指定的this对象之外的参数是一个数组,数组中的值会作为参数按照顺序传入到this指定的对象中. bind是解 ...
 - this指向与call,apply,bind
		
this指向与call,apply,bind ❝ 「this」问题对于每个前端同学来说相信都不陌生,在平时开发中也经常能碰到,有时候因为「this」还踩过不少坑,并且「this」问题在面试题中出现的概 ...
 - javascript中apply、call和bind的区别
		
在JS中,这三者都是用来改变函数的this对象的指向的,他们有什么样的区别呢.在说区别之前还是先总结一下三者的相似之处:1.都是用来改变函数的this对象的指向的.2.第一个参数都是this要指向的对 ...
 
随机推荐
- CodeForces - 357C Knight Tournament 伪并查集(区间合并)
			
Knight Tournament Hooray! Berl II, the king of Berland is making a knight tournament. The king has a ...
 - html扩展调用qq邮箱
			
总体流程:qq邮箱->设置->账户->邮我(使用邮我) over!over!over!
 - PTA 4-4 先序输出叶结点 【基础题】
			
//二叉树的叶结点:度为0的结点. void PreorderPrintLeaves( BinTree BT ) { if(BT==NULL) //如果传下来根节点就是空,直接返回: return; ...
 - Memcached 查看帮助
			
进入到memcached目录, 输入命令: memcached -h 即可查看帮助 -p<num>要侦听的TCP端口号(默认值:11211) -u<num>udp监听端口号(默 ...
 - Ruby测试小代码[计算50以内的素数]
			
算法思想 判断某一个数,能不能被比他平方根小的素数整除. 首先看看代码 $arr = [] $arr[0] = 2 def add_prime(n) 3.step(n,2){|num| $arr &l ...
 - Execution failed for task ':app:lintVitalRelease'.
			
解决方法:在build.gradle文件的android部分添加如下代码: lintOptions { checkReleaseBuilds false abortOnError false} 最后成 ...
 - SVN有任何胜过git的地方吗?
			
SVN有任何胜过git的地方吗? 好的技术问题通常会引出技术专家们依据经验得出的深层次的观点.但对于这样的问题的答案也很容易演变成完全基于个人喜好的情绪倾泄,而不是根据事实.标准和具体的专业知识.就比 ...
 - IP服务-7-系统日志
			
默认情况下.Cisco路由器和交换机并不在NVRAM (非易失性内存)中记录事件:工程师可以使用命令logging buffered改变设备的这一默认行为.并且还可以使用额外参数来设定日志缓存的大小. ...
 - dzzoffice 任意文件下载漏洞分析
			
dzzoffice 任意文件下载 \updload\dzz\system\save.php第72行开始: elseif($_GET['do']=='move'){ $obz=trim($_ ...
 - FFT与NTT的模板
			
网上相关博客不少,这里给自己留个带点注释的模板,以后要是忘了作提醒用. 以洛谷3803多项式乘法裸题为例. FFT: #include <cstdio> #include <cmat ...