Javascript中this绑定的3种方法与比较


Posted in Javascript onOctober 13, 2016

介绍

this 可以说是 javascript 中最耐人寻味的一个特性,学习this 的第一步就是明白 this 既不是指向函数自身也不指向函数的作用域。 this 实际上是在函数被调用时发生的绑定,它指向什么地方完全取决于函数在哪里被调用。

为什么需要绑定this

this代指当前的上下文环境,在不经意间容易改变:

var info = "This is global info";
var obj = {
 info: 'This is local info',
 getInfo: getInfo
}
function getInfo() {
 console.log(this.info);
}
obj.getInfo() //This is local info

getInfo() //This is global info 当前上下文环境被修改了

在上面的例子中,我们在对象内部创建一个属性getInfo,对全局作用域下的getInfo进行引用,而它的作用是打印当前上下文中info的值,当我们使用obj.getInfo进行调用时,它会打印出对象内部的info的值,此时this指向该对象。而当我们使用全局的函数时,它会打印全局环境下的info变量的值,此时this指向全局对象。

这个例子告诉我们:

     1、同一个函数,调用的方式不同,this的指向就会不同,结果就会不同。

     2、对象内部的属性的值为引用类型时,this的指向不会一直绑定在原对象上。

其次,还有不经意间this丢失的情况:

var info = "This is global info";
var obj = {
 info: 'This is local info',
 getInfo: function getInfo() {
 console.log(this.info);

 var getInfo2 = function getInfo2() {
  console.log(this.info);
 }
 getInfo2();
 }
}
obj.getInfo();

//This is local info
//This is global info

上面的例子中,对象obj中定义了一个getInfo方法,方法内定义了一个新的函数,也希望拿到最外层的该对象的info属性的值,但是事与愿违,函数内函数的this被错误的指向了window全局对象上面,这就导致了错误。

解决的方法也很简单,在最外层定义一个变量,存储当前词法作用域内的this指向的位置,根据变量作用域的关系,之后的函数内部还能访问这个变量,从而得到上层函数内部this的真正指向。

var info = "This is global info";
var obj = {
 info: 'This is local info',
 getInfo: function getInfo() {
 console.log(this.info);

 var self = this;  //将外层this保存到变量中
 var getInfo2 = function getInfo2() {
  console.log(self.info); //指向外层变量代表的this
 }
 getInfo2();
 }
}
obj.getInfo();

//This is local info
//This is local info

然而这样也会有一些问题,上面的self变量等于重新引用了obj对象,这样的话可能会在有些时候不经意间修改了整个对象,而且当需要取得多个环境下的this指向时,就需要声明多个变量,不利于管理。

有一些方法,可以在不声明类似于self这种变量的条件下,绑定当前环境下的上下文,确保编程内容的安全。

如何绑定this

1. call, apply

call和apply是定义在Function.prototype上的两个函数,他们的作用就是修正函数执行的上下文,也就是this的指向问题。

以call为例,上述anotherFun想要输出local thing就要这样修改:

...
var anotherFun = obj.getInfo;
anotherFun.call(obj) //This is local info

函数调用的参数:

      Function.prototype.call(context [, argument1, argument2 ])

      Function.prototype.apply(context [, [ arguments ] ])

从这里就可以看到,call和apply的第一参数是必须的,接受一个重新修正的上下文,第二个参数都是可选的,他们两个的区别在于,call从第二个参数开始,接受传入调用函数的值是一个一个单独出现的,而apply是接受一个数组传入。

function add(num1, num2, num3) {
 return num1 + num2 + num3;
}
add.call(null, 10, 20, 30); //60
add.apply(null, [10, 20, 30]); //60

当接受的context为undefined或null时,会自动修正为全局对象,上述例子中为window

2. 使用Function.prototype.bind进行绑定

ES5中在Function.prototype新增了bind方法,它接受一个需要绑定的上下文对象,并返回一个调用的函数的副本,同样的,它也可以在后面追加参数,实现函数的柯里化。

Function.prototype.bind(context[, argument1, argument2])
//函数柯里化部分
function add(num1 ,num2) {
 return num1 + num2;
}
var anotherFun = window.add.bind(window, 10);
anotherFun(20); //30

同时,他返回一个函数的副本,并将函数永远的绑定在传入的上下文中。

...
var anotherFun = obj.getInfo.bind(obj)
anotherFun(); //This is local info

polyfill

polyfill是一种为了向下兼容的解决方案,在不支持ES5的老旧浏览器上,如何使用bind方法呢,就得需要使用旧的方法重写一个bind方法。

if (!Function.prototype.bind) {
 Function.prototype.bind = function (obj) {
 var self = this;
 return function () {
 self.call(obj);
 }
 }
}

上面的写法实现了返回一个函数,并且将上下文修正为传入的参数,但是没有实现柯里化部分。

...
Function.prototype.bind = function(obj) {
 var args = Array.prototype.slice.call(arguments, 1); 
 //记录下所有第一次传入的参数

 var self = this;
 return function () {
 self.apply(obj, args.concat(Array.prototype.slice.call(arguments)));
 }
}

当使用bind进行绑定之后,即不能再通过call,apply进行修正this指向,所以bind绑定又称为硬绑定。

3. 使用new关键字进行绑定

在js中,函数有两种调用方式,一种是直接进行调用,一种是通过new关键字进行构造调用。

function fun(){console.log("function called")}
//直接调用
fun() //function called
//构造调用
var obj = new fun() //function called

那普通的调用和使用new关键字的构造调用之间,又有哪些区别呢?

准确的来说,就是new关键字只是在调用函数的基础上,多增加了几个步骤,其中就包括了修正this指针到return回去的对象上。

var a = 5;
function Fun() {
 this.a = 10;
}
var obj = new Fun();
obj.a //10

几种绑定方式的优先级比较

以下面这个例子来进行几种绑定状态的优先级权重的比较

var obj1 = {
 info: "this is obj1",
 getInfo: () => console.log(this.info)
}

var obj2 = {
 info: "this is obj2",
 getInfo: () => console.log(this.info)
}

1. call,apply和默认指向比较

首先很显然,根据使用频率来想,使用call和apply会比直接调用的优先级更高。

obj1.getInfo() //this is obj1
obj2.getInfo() //this is obj2
obj1.getInfo.call(obj2) //this is obj2
obj2.getInfo.call(obj1) //this is obj1

使用call和apply相比于使用new呢?

这个时候就会出现问题了,因为我们没办法运行类似 new function.call(something)这样的代码。所以,我们通过bind方法返回一个新的函数,再通过new判断优先级。

var obj = {}
function foo(num){
 this.num = num;
}

var setNum = foo.bind(obj);
setNum(10);
obj.num //10

var obj2 = new setNum(20);
obj.num //10
obj2.num //20

通过这个例子我们可以看出来,使用new进行构造调用时,会返回一个新的对象,并将this修正到这个对象上,但是它并不会改变之前的对象内容。

那么问题来了,上面我们写的bind的polyfill明显不具备这样的能力。而在MDN上有一个bind的polyfill方法,它的方法如下:

if (!Function.prototype.bind) { 
 Function.prototype.bind = function (oThis) { 
 if (typeof this !== "function") { 
 throw new TypeError("Function.prototype.bind - 
 what is trying to be bound is not callable"); 
 }
 var aArgs = Array.prototype.slice.call(arguments, 1),
 fToBind = this, 
 fNOP = function () {}, 
 fBound = function () { 
  return fToBind.apply(this instanceof fNOP ? 
    this : oThis || this, 
   aArgs.concat(Array.prototype.slice.call(arguments))); 
 }; 
 fNOP.prototype = this.prototype; 
 fBound.prototype = new fNOP(); 
 return fBound; 
 };
}

上面的polyfill首先判断需要绑定的对象是否为函数,防止使用Function.prototype.bind.call(something)时,something不是一个函数造成未知错误。之后让需要返回的fBound函数继承自this,返回fBound。

特殊情况

当然,在某些情况下,this指针指向也存在一些意外。

箭头函数

ES6中新增了一种定义函数方式,使用"=>"进行函数的定义,在它的内部,this的指针不会改变,永远指向最外层的词法作用域。

var obj = {
 num: 1,
 getNum: function () {
 return function () {
 //this丢失
 console.log(this.num); 
 //此处的this指向window
 }
 }
}
obj.getNum()(); //undefined

var obj2 = {
 num: 2,
 getNum: function () {
 return () => console.log(this.num); 
 //箭头函数内部绑定外部getNum的this,外部this指向调用的对象
 }
}
obj2.getNum()(); //2

软绑定

上面提供的bind方法可以通过强制修正this指向,并且再不能通过call,apply进行修正。如果我们希望即能有bind效果,但是也能通过call和apply对函数进行二次修正,这个时候就需要我们重写一个建立在Function.prototype上的方法,我们给它起名为"软绑定"。

if (!Function.prototype.softBind) {
 Function.prototype.softbind = function (obj) {
 var self = this;
 var args = Array.prototype.slice.call(arguments, 1);
 return function () {
 return self.apply((!this || this === (window || global)) ? 
   obj : this, 
   args.concat(Array.prototype.slice.call(arguments)));
 }
 }
}

bind,call的妙用

在平日里我们需要将伪数组元素变为正常的数组元素时,往往通过Array.prototype.slice方法,正如上面的实例那样。将arguments这个对象变为真正的数组对象,使用 Array.prototype.slice.call(arguments)进行转化.。但是,每次使用这个方法太长而且繁琐。所以有时候我们就会这样写:

var slice = Array.prototype.slice;
slice(arguments);
//error

同样的问题还出现在:

var qsa = document.querySelectorAll;
qsa(something);
//error

上面的问题就出现在,内置的slice和querySelectorAll方法,内部使用了this,当我们简单引用时,this在运行时成为了全局环境window,当然会造成错误。我们只需要简单的使用bind,就能创建一个函数副本。

var qsa = document.querySelectorAll.bind(document);
qsa(something);

同样的,使用因为call和apply也是一个函数,所以也可以在它们上面调用bind方法。从而使返回的函数的副本本身就带有修正指针的功能。

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice(arguments);

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或工作能带来一定的帮助,如果有疑问大家可以留言交流。

Javascript 相关文章推荐
JavaScript基本对象
Jan 11 Javascript
帮助避免错误的Javascript陷阱清单
May 31 Javascript
jquery keypress,keyup,onpropertychange键盘事件
Jun 25 Javascript
Javascript开发之三数组对象实例介绍
Nov 12 Javascript
使用Node.js实现一个简单的FastCGI服务器实例
Jun 09 Javascript
jQuery源码解读之hasClass()方法分析
Feb 20 Javascript
简单介绍JavaScript数据类型之隐式类型转换
Dec 28 Javascript
JS实现超简单的汉字转拼音功能示例
Dec 22 Javascript
javascript容错处理代码(屏蔽js错误)
Jan 20 Javascript
JavaScript队列函数和异步执行详解
Jun 19 Javascript
浅谈vue的iview列表table render函数设置DOM属性值的方法
Sep 30 Javascript
vue 动态设置img的src地址无效,npm run build 后找不到文件的解决
Jul 26 Javascript
手机端实现Bootstrap简单图片轮播效果
Oct 13 #Javascript
jquery  实现轮播图详解及实例代码
Oct 12 #Javascript
ExtJS 4.2 Grid组件单元格合并的方法
Oct 12 #Javascript
JS代码实现百度地图 画圆 删除标注
Oct 12 #Javascript
如何使用jquery实现文字上下滚动效果
Oct 12 #Javascript
微信js-sdk界面操作接口用法示例
Oct 12 #Javascript
微信小程序 location API接口详解及实例代码
Oct 12 #Javascript
You might like
用PHP函数解决SQL injection
2006/10/09 PHP
PHP调用Twitter的RSS的实现代码
2010/03/10 PHP
PHP实现的微信公众号扫码模拟登录功能示例
2019/05/30 PHP
Thinkphp页面跳转设置跳转等待时间的操作
2019/10/16 PHP
php适配器模式简单应用示例
2019/10/23 PHP
jquery 查找新建元素代码
2010/07/06 Javascript
图片img的src不变让浏览器重新加载实现方法
2013/03/29 Javascript
使用js修改客户端注册表的方法
2013/08/09 Javascript
jQuery中find()方法用法实例
2015/01/07 Javascript
nodejs 中模拟实现 emmiter 自定义事件
2016/02/22 NodeJs
js计算时间差代码【包括计算,天,时,分,秒】
2016/04/26 Javascript
Vue2 Vue-cli中使用Typescript的配置详解
2017/07/24 Javascript
JS基于ES6新特性async await进行异步处理操作示例
2019/02/02 Javascript
JavaScript实现的联动菜单特效示例
2019/07/08 Javascript
vue.js购物车添加商品组件的方法
2019/09/17 Javascript
Vue快速实现通用表单验证的方法
2020/02/24 Javascript
[01:02:25]2014 DOTA2华西杯精英邀请赛 5 24 iG VS DK
2014/05/26 DOTA
[02:08]什么藏在DOTA2 TI9“小紫本”里?斧王历险记告诉你!
2019/05/17 DOTA
python连接mongodb操作数据示例(mongodb数据库配置类)
2013/12/31 Python
在Python中marshal对象序列化的相关知识
2015/07/01 Python
Python向MySQL批量插数据的实例讲解
2018/03/31 Python
python编程使用协程并发的优缺点
2018/09/20 Python
python Kmeans算法原理深入解析
2019/08/23 Python
Python 实现大整数乘法算法的示例代码
2019/09/17 Python
python 穷举指定长度的密码例子
2020/04/02 Python
python批量合成bilibili的m4s缓存文件为MP4格式 ver2.5
2020/12/01 Python
Tuckernuck官网:经典的美国品质服装、鞋子和配饰
2021/01/11 全球购物
护理学毕业生自荐信
2013/10/02 职场文书
《浅水洼里的小鱼》听课反思
2014/02/28 职场文书
青年文明号创建承诺
2014/03/31 职场文书
小学新学期寄语
2014/04/02 职场文书
党员个人查摆剖析材料
2014/10/16 职场文书
考察邀请函范文
2015/01/31 职场文书
红色电影观后感
2015/06/18 职场文书
听证会主持词
2015/07/03 职场文书
什么是执行力?9个故事告诉您:成功绝非偶然!
2019/07/05 职场文书