资讯专栏INFORMATION COLUMN

Javascript 中神奇的 this

Cristic / 2474人阅读

摘要:以避免全局变量的污染。如果是使用关键字来调用函数的,则会绑定到新创建的那个对象上如果是在事件函数内,则会绑定到触发事件的那个元素上。

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 obj3 = {
        a:3,
        fn:fn
    }
    
    var obj2 = {
        a:2,
        obj3:obj3
    }

    var obj = {
        a:1,
        obj2:obj2
    }

    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元素上。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91614.html

相关文章

  • 浅谈JavaScript闭包

    摘要:在内部,理所当然能访问到局部变量,但当作为的返回值赋给外的全局变量时,神奇的事情发生了在全局作用域中访问到了,这就是闭包。而闭包最神奇的地方就是能在一个函数外访问函数中的局部变量,把这些变量用闭包的形式放在函数中便能避免污染。 一、闭包是什么? 《JavaScript高级程序设计》中写道:闭包是指有权访问另一个函数作用域中的变量的函数,如果用下定义的观点看,这句话就是说闭包是函数,我...

    Riddler 评论0 收藏0
  • H5图片裁剪-压缩-上传-神奇Croppie.js

    摘要:之图片裁剪压缩上传图片裁剪压缩上传预览是常见功能幸运的是我们有这款利器上传选择图片文件裁剪挺好用的图片处理插件另外还有较好的插件有待研究 Croppie.js之图片裁剪压缩上传 h5图片裁剪, 压缩, 上传, 预览是常见功能, 幸运的是我们有cropp.js这款利器. 1. style .actions button, .actions a.btn { ...

    didikee 评论0 收藏0
  • javascriptthis理解

    摘要:的关键字总是让人捉摸不透,关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用,因为函数的调用场景不同,的指向也不同。其实只要理解语言的特性就很好理解。个人对中的关键字的理解如上,如有不正,望指正,谢谢。 javascript的this关键字总是让人捉摸不透,this关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用,因为函数的调用场景不同,this的指向也不...

    jimhs 评论0 收藏0
  • JavaScript 里 ~ 神奇用法

    这几天看koa源码的时候,经常看到if(~notfound.indexOf(err.code)){ doSomeing... }这种在一个表达式前面加~号的,今天就来扒一扒这已黑魔法。 ~ 取反操作符 不熟悉原码,反码,补码的小伙伴可以先看一下这篇文章原码、反码、补码,计算机中负数的表示 在javascript中,假设有一个变量var a = 1, 那么~a + a = -1, 也就是说现在~...

    FreeZinG 评论0 收藏0
  • 读 Zepto 源码之操作 DOM

    摘要:辅助方法这个方法递归遍历的子节点,将节点交由回调函数处理。对集合进行遍历,调用方法,如果为函数,则将回调函数返回的结果作为参数传给否则,如果为,则将也即包裹元素的副本传给,否则直接将传给。 这篇依然是跟 dom 相关的方法,侧重点是操作 dom 的方法。 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto...

    beita 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<