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 相关文章推荐
Js动态创建div
Sep 25 Javascript
获取内联和链接中的样式(js代码)
Apr 11 Javascript
JS实现的数组全排列输出算法
Mar 19 Javascript
IE浏览器下PNG相关功能
Jul 05 Javascript
Jquery插件easyUi实现表单验证示例
Dec 15 Javascript
在vue-cli脚手架中配置一个vue-router前端路由
Jul 03 Javascript
使用vue-cli导入Element UI组件的方法
May 16 Javascript
vue2.0项目实现路由跳转的方法详解
Jun 21 Javascript
使用D3.js构建实时图形的示例代码
Aug 28 Javascript
vue实现动态显示与隐藏底部导航的方法分析
Feb 11 Javascript
微信小程序导航栏跟随滑动效果的实现代码
May 14 Javascript
vue实现桌面向网页拖动文件的示例代码(可显示图片/音频/视频)
Mar 01 Vue.js
手机端实现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_xmlhttp 乱码问题解决方法
2009/08/07 PHP
PHP实现仿百度文库,豆丁在线文档效果(word,excel,ppt转flash)
2016/03/10 PHP
php实现URL加密解密的方法
2016/11/17 PHP
PHP连接MySQL进行增、删、改、查操作
2017/02/19 PHP
PHP基于curl模拟post提交json数据示例
2018/06/22 PHP
在网页中屏蔽快捷键
2006/09/06 Javascript
表单类各种类型(文本框)失去焦点效果jquery代码
2013/04/26 Javascript
变量声明时命名与变量作为对象属性时命名的区别解析
2013/12/06 Javascript
javascript实现删除前弹出确认框
2015/06/04 Javascript
图文详解Heap Sort堆排序算法及JavaScript的代码实现
2016/05/04 Javascript
详解Vue学习笔记进阶篇之列表过渡及其他
2017/07/17 Javascript
浅谈JS中的常用选择器及属性、方法的调用
2017/07/28 Javascript
IE11下使用canvas.toDataURL报SecurityError错误的解决方法
2017/11/19 Javascript
vue elementUI tree树形控件获取父节点ID的实例
2018/09/12 Javascript
使用异步组件优化Vue应用程序的性能
2019/04/28 Javascript
vue-cli配置flexible过程详解
2019/07/04 Javascript
小程序自定义模板实现吸顶功能
2020/01/08 Javascript
javascript 内存模型实例详解
2020/04/18 Javascript
vue实现分页的三种效果
2020/06/23 Javascript
Vue中的nextTick作用和几个简单的使用场景
2021/01/25 Vue.js
win7 下搭建sublime的python开发环境的配置方法
2014/06/18 Python
Python结巴中文分词工具使用过程中遇到的问题及解决方法
2017/04/15 Python
Python3编程实现获取阿里云ECS实例及监控的方法
2017/08/18 Python
python中实现将多个print输出合成一个数组
2018/04/19 Python
详解Django中六个常用的自定义装饰器
2018/07/04 Python
Python解析、提取url关键字的实例详解
2018/12/17 Python
在PyCharm中控制台输出日志分层级分颜色显示的方法
2019/07/11 Python
PyTorch加载预训练模型实例(pretrained)
2020/01/17 Python
Python插件机制实现详解
2020/05/04 Python
Python如何转换字符串大小写
2020/06/04 Python
Python爬取网站图片并保存的实现示例
2021/02/26 Python
巧用CSS3 border实现图片遮罩效果代码
2012/04/09 HTML / CSS
CSS3属性box-sizing使用指南
2014/12/09 HTML / CSS
美国电力供应商店/电气批发商:USESI
2018/10/12 全球购物
机关干部个人对照检查材料思想汇报
2014/09/28 职场文书
党员干部廉政承诺书
2015/04/28 职场文书