Javascript中神奇的this


Posted in Javascript onJanuary 20, 2016

Javascript 当中的 this 与其他语言是完全不同的机制,很有可能会让一些编写其他语言的工程师迷惑。

1. 误以为 this 指向函数自身

根据 this 的英语语法,很容易将函数中出现的 this 理解为函数自身。在 javascript 当中函数作为一等公民,确实可以在调用的时候将属性值存储起来。但是如果使用方法不对,就会发生与实际预期不一致的情况。具体情况,请看下面代码

function fn(num){
    this.count++;
  }
  
  fn.count = 0;
  
  for(var i=0;i<3;i++){
    fn(i);
  }
  console.log(fn.count); // 0

如果 fn 函数里面的 this 指向自身函数,那么 count 属性的属性值就应该产生变化,但实际上却是纹丝不动。对于这个问题,有些人会利用作用域来解决,比如这么写

var data = {
    count:0
  };
  
  function fn(num){
    data.count++;
  }
  
  for(var i=0;i<3;i++){
    fn(i);
  }
  
  console.log(data.count);  //3

又或者更直接的这么写

function fn(num){
    fn.count++;
  }
  
  fn.count = 0;
  
  for(var i=0;i<3;i++){
    fn(i);
  }
  
  console.log(fn.count);//3

虽然这两种方式都输出了正确的结果,但是却避开了 this 到底绑定在哪里的问题。如果对一个事物的工作原理不清晰,就往往会产生头痛治头,脚痛治脚的问题,从而导致代码变得的丑陋,而且维护性也会变得很差。

2. this神奇的绑定规则

2.1 默认绑定规则

第一种是最常见的 this 的绑定,看一下下面的代码

function fn(){
    console.log(window === this); //浏览器环境
  }
  fn(); //true

函数fn 是直接在全局作用域下调用的,没有带其他任何修饰,这种情况下,函数调用的时候使用了 this 的默认绑定,指向了全局对象。

这样就清楚了第一个例子中的 this 指向, fn 函数中的 this 指向了全局变量,所以 this.count++ 相当于 window.count++(浏览器环境下),当然不会对 fn 函数的count属性产生影响。

有一点要说明的是,上面种情况只能在非严格模式(strict mode)下才能发生,在严格模式下,会将 this 默认绑定为 undefined。以避免全局变量的污染。

2.2 隐式绑定规则

如果函数在以对象为上下文进行调用,那么 this 的绑定就会产生变化。this 会绑定到调用这个函数的对象,查看下面代码:

var obj = {
    a:1,
    fn:function(){
      console.log(this.a);
    }
  }
  
  obj.fn(); //1

即使函数声明不在对象当中,this 指向仍会产生变化

function fn(){
    console.log(this.a);
  }
  var obj = {
    a:1,
    fn:fn
  }
  obj.fn(); //1

由此可见,this 的绑定,不与函数定义的位置有关,而是与调用者和调用方式有关。

在隐式的绑定规则下,有一些特殊的地方,需要注意。

2.2.1 多层对象调用 this 的指向

function fn(){
    console.log(this.a);
  }
  var obj = {
    a:1,
    obj2:obj2
  }
  var obj2 = {
    a:2,
    obj3:obj3
  }
  var obj3 = {
    a:3,
    fn:fn
  }
  
  obj.obj2.obj3.fn(); //3

在多层对象引用下,this 指向的是调用的函数的那个对象。

2.2.2 隐式赋值可能存在丢失现象

查看下面代码

function fn(){
    console.log(this);
  }
  var  obj = {
    fn:fn
  }
  
  var fun = obj.fn;
  fun(); //window

虽然 fn 引用了 obj.fun ,但是函数的调用方式,仍是不带任何修饰的,所以 this 还是绑定在了 window 上。
还有一种情况,容易让大家忽略,那就是传参的时候,其实会进行隐式赋值。

function fn(){
    console.log(this);
  }
  
  function doFn(fn){
    fn();
  }
  
  var obj = {
    fn:fn
  }
  
  doFn(obj.fn); //window

隐式绑定 this 不是一种很推荐的方式,因为很有可能就发生丢失的情况,如果业务当中对 this 的绑定有要求,建议还是使用显示绑定的方式。

2.3 显式绑定规则

显示绑定就是利用函数原型上的 apply 与 call 方法来对 this 进行绑定。用法就是把想要绑定的对象作为第一个参数传进去。

function fn(){
    console.log(this);
  }
  
  var obj = {};
  
  fn.call(obj); //{}

有些时候会想将函数的 this 绑定在某个对象上,但是不需要立即调用,这样的话,直接利用 call 或者 apply 是无法做的。

function fn(){
    console.log(this);
  }
  
  function bind(fn){
    fn();
  }
  
  var obj = {
    fn:fn
  }
  
  bind.call(obj,fn); //window

上面这个例子,看似好像可以,但实际上是 bind 函数的 this 绑定到了 obj 这个对象,但是 fn 仍然是没有任何修饰的调用,所以 fn 仍然是默认的绑定方式。

function fn(){
    console.log(this);
  }
  
  function bind(fn,obj){
    return function(){
      fn.apply(obj,arguments);
    }
  }
  
  var obj = {
    fn:fn
  }
  
  var fun = bind(fn,obj);
  fun(); //obj

这样调用,就可以将灵活多变的 this ,牢牢的控制住了,因为 fn 的调用方式为 apply 调用。所以,this 就被绑定在传入的 obj 对象上,在 ES5 当中,函数的原型方法上多了一个 bind。效果与上面的函数基本一致,具体用法限于篇幅就不多说了。

2.4 new 绑定

new 是一个被很多人误解的一个关键字,但实际上 javascript 的 new 与传统面向对象的语言完全不同。
个人把 new 理解为一种特殊的函数调用,当使用 new 关键字来调用函数的时候,会执行下面操作,

  • 创建一个全新的对象
  • 将空对象的 __proto__ 指向构造函数的 prototype
  • 将新对象的 this 绑定到调用的函数上
  • 如果函数返回值为基本类型或者为 this又或者不返回任何值,那么将会返回这个创建的新对象,如果返回了一个对象,那么则会返回这个对象,而不会返回创建的新对象。
function fn(a){
    this.a = a;
  }
  fn.prototype.hi = function(){
    console.log('hi')
  }
  
  var obj = new fn(2);
  
  console.log(obj);


  function fn(a){
    this.a = a;
    return {};
  }
  
  var obj = new fn(2);
  
  console.log(obj); //{}

2.5 特殊的传参

null 和 undefined 也是可以作为 this 的绑定对象的,但是实际上应用的是默认的绑定。
但是这种传参的实际效用是什么呢?
常见的用法是将一个数组展开,作为参数传入参数。比如

function fn(a,b){
    console.log('a:',a,'b:',b);
  }
  
  fn.apply(null,[1,2]); // a: 1 b: 2

但是这种用法会有一个坑,那就是如果函数存在了 this ,那么就会应用默认的绑定规则,将 this 绑定在全局对象上,发生于预期不一致的情况。为了代码更加稳健,可以使创建一个比空对象更空的对象。

var obj = Object.create(null);
console.log(obj.__proto__); //undefined

var obj2 = {}
console.log(obj2.__proto__); //Object {}

Object原型上有一个 create 方法,这个方法会创建一个对象,然后将对象的原型指向传入的参数,所以传入 null 的话,产生一个没有 prototype 的对象,所以会比空对象更加"空"。

所以传入这个对象,会比传入 null 更加安全。

var obj = Object.create(null);

fn.apply(obj,[1,2]);

2.6 根据作用域来决定 this 的绑定

在 ES6 当中,出现了一个新的函数类型,箭头函数。

如果使用箭头函数,那么就不会使用上面提到的四种 this 绑定方式,而是根据作用域来决定

比较常见的是用于事件函数和定时器的情况。

下面是比较常见的传统 this 写法

function fn(){
    var _this = this;
    setTimeout(function(){
      console.log(_this.a);
    },100)
  }

  var obj = {
    a:2
  }
  
  fn.call(obj); //2

如果使用箭头函数则可以这么写

function fn(){
    setTimeout(()=>{
      //this 来源于 fn 函数的作用域
      console.log(this.a);
    },100)
  }

  var obj = {
    a:2
  }
  
  fn.call(obj); //2

2.7 事件函数当中 this 的绑定机制

如果是在事件函数当中,this 的绑定是指向触发事件的 DOM 元素的,

$('body')[0].addEventListener('click',function(){
  console.log(this);
},false);

点击 body 元素之后,控制台则会显示 body 元素

3. 小结

如果想判断一个函数的 this 绑定在哪里,首先是找到函数的调用位置,之后是按照规则来判断。

  • 如果函数调用时没有任何修饰条件,那么在严格模式下则会绑定到 undefined ,非严格模式下会绑定到全局。
  • 如果是用对象做上下文,来对函数进行调用,那么则会绑定到调用的这个对象上。
  • 如果是用 call 或者 apply 方法来进行调用的,则会绑定到第一个传入参数上。
  • 如果是使用 new 关键字来调用函数的,则会绑定到新创建的那个对象上.
  • 如果是在事件函数内,则会绑定到触发事件的那个DOM元素上。

以上就是关于Javascript中神奇的this的相关介绍,希望对大家的学习有所帮助。

Javascript 相关文章推荐
js call方法详细介绍(js 的继承)
Nov 18 Javascript
Javascript基础教程之数据类型转换
Jan 18 Javascript
AngularJS监听路由变化的方法
Mar 07 Javascript
javaScript中封装的各种写法示例(推荐)
Jul 03 Javascript
bootstrap fileinput插件实现预览上传照片功能
Jan 23 Javascript
vue 中滚动条始终定位在底部的方法
Sep 03 Javascript
Vue.js组件高级特性实例详解
Dec 24 Javascript
javascript canvas API内容整理
Feb 16 Javascript
js实现自定义右键菜单
May 18 Javascript
Vue记住滚动条和实现下拉加载的完美方法
Jul 31 Javascript
详谈Vue.js框架下main.js,App.vue,page/index.vue之间的区别
Aug 12 Javascript
vue 实现上传组件
May 31 Vue.js
javascript实现图片轮播效果
Jan 20 #Javascript
JS获取鼠标坐标位置实例分析
Jan 20 #Javascript
属于你的jQuery提示框(Tip)插件
Jan 20 #Javascript
学习JavaScript设计模式之模板方法模式
Jan 20 #Javascript
高性能JavaScript循环语句和条件语句
Jan 20 #Javascript
详解Javascript模板引擎mustache.js
Jan 20 #Javascript
JavaScript优化专题之Loading and Execution加载和运行
Jan 20 #Javascript
You might like
php获取mysql版本的几种方法小结
2008/03/25 PHP
php中将时间差转换为字符串提示的实现代码
2011/08/08 PHP
第二章 PHP入门基础之php代码写法
2011/12/30 PHP
thinkphp 多表 事务详解
2013/06/17 PHP
教你如何开启shopnc b2b2c 伪静态
2014/10/21 PHP
php利用云片网实现短信验证码功能的示例代码
2017/11/18 PHP
jquery.alert 弹出式复选框实现代码
2009/06/15 Javascript
jquery 学习笔记一
2010/04/07 Javascript
js保留两位小数使用toFixed实现
2013/07/29 Javascript
javascript实现列表切换效果
2016/05/02 Javascript
JS与jQuery实现隔行变色的方法
2016/09/09 Javascript
JSP防止网页刷新重复提交数据的几种方法
2016/11/19 Javascript
DOM 事件的深入浅出(二)
2016/12/05 Javascript
JavaScript 日期时间选择器一些小结
2018/04/02 Javascript
layUI实现前端分页和后端分页
2019/07/27 Javascript
node.js中fs文件系统模块的使用方法实例详解
2020/02/13 Javascript
vue全屏事件开发详解
2020/06/17 Javascript
详解 Python 读写XML文件的实例
2017/08/02 Python
如何通过python的fabric包完成代码上传部署
2019/07/29 Python
python实现逆滤波与维纳滤波示例
2020/02/26 Python
python可以用哪些数据库
2020/06/22 Python
CSS3 box-sizing属性
2009/04/17 HTML / CSS
css3如何绘制一个圆圆的loading转圈动画
2018/01/09 HTML / CSS
佐卡伊官网:中国知名珠宝品牌
2017/02/05 全球购物
eVitamins日本:在线购买折扣维生素、补品和草药
2019/04/04 全球购物
广告宣传策划方案
2014/05/21 职场文书
个人廉政承诺书
2015/04/28 职场文书
西游记读书笔记
2015/06/25 职场文书
推普标语口号大全
2015/12/26 职场文书
2016年大学生暑期社会实践活动总结
2016/04/06 职场文书
餐厅如何利用“营销策略”扭转亏本局面
2019/10/15 职场文书
开学季:喜迎新生,迎新标语少不了
2019/11/07 职场文书
tensorflow中的梯度求解及梯度裁剪操作
2021/05/26 Python
mysql配置SSL证书登录的实现
2021/09/04 MySQL
SQL Server Agent 服务无法启动
2022/04/20 SQL Server
Rust中的Struct使用示例详解
2022/08/14 Javascript