JavaScript中this关键字含义及其最佳实践(一)


在JavaScript中this关键字非常灵活, 在不同的情况下有不同的含义. 对于初学者来说 比较难以掌握, 即使是有经验的开发人员, 亦容易犯错. Google 一下 understanding this keyword in javascript 有一大把相关的网页, 足以证明this给开发者带来的困 扰.

this含义总则

万剑归宗, 万宗归一. 总得来说,

  • this 指定义处(非运行处)上下文对象.

不同情景

下面根据不同的情景阐述一下.

  1. 纯函数
  2. 对象方法
  3. 类构造函数/类实例方法
  4. call/apply/bind
  5. 事件监听器

纯函数

在浏览器中运行以下示例, 会发现, this都是指向window对象.

var test = function (value) {
    this.hello = value;
    console.log(‘this in test() ‘)
    console.log(this);
    console.log(window.hello);
};

var obj = {};

var wrapper = function (value) {
    console.log(‘this in wrapper() ‘);
    console.log(this);
    test(value || ‘value from wrapper‘);

};

//在全局下运行, 故`this`指向全局对象, 浏览器中即windows.
//若运行在node.js环境中, 指向global
test(‘value from window‘);

//同test()
wrapper();

//此时, 使用call改变了wrapper函数中`this`的指向.
//在wrapper函数中, `this`指向 obj 对象.
//
//虽然在wrapper函数中运行test函数, 但在test函数中`this`依然指向全局对象.
//这种情况很好的说明`this`的含义总则: 指定义处(非运行处)上下文对象.
//
wrapper.call(obj, ‘value from wrapper apply‘);


对象方法

在对象方法中, this总是指向对象本身.

var uncleWang = {
    _name: ‘WangXiaobo‘,
    getName: function () {
        //`this` 指向uncleWang 对象
        return this._name;
    },
    displayName: function () {
        //`this` 指向uncleWang 对象
        var name = this.getName();
        //显示 The name is WangXiaobo
        console.log(‘The name is ‘ + name);
    }
};


若一个纯函数变成了一个对象的方法, 函数作为对象方法时, this 指向对象. 但作为纯函 数调用时, this 依然指向定义处的上下文对象.

window._name = ‘window‘;
var sayName = function () {
    console.log(‘My name is ‘ + this._name);
}

uncleWang.sayName = sayName;

//作为对象方法调用, `this`指向对象
//显示 My name is WangXiaobo
uncleWang.sayName();

//纯函数调用, `this`指向定义处的上下文对象
//显示 My name is window
sayName();


类构造函数/类实例方法

  1. 在类构造函数和类实例方法中, this都指向类实例.
  2. 在类方法中, this指向构造函数本身, 因为构造函数本身也是对象.
    • 在JS中, 除了基本类型外, 一切皆对象
    • 函数也是对象, 构造函数也是对象.
/**
 * @param {String} name
 */
function Person (name) {
    this._name = name;
}

//类方法添加至构造函数的prototype对象上
Person.prototype.getName = function () {
    return this._name;
};

Person.prototype.displayName = function () {
    var name = this.getName();
    console.log(‘name: ‘ + name);
};

Person._className = ‘Person‘;
//类方法中, `this`指向构造函数本身, 因为构造函数本身也是对象.
Person.getNameOfClass = function () {
    return this._className;
};

console.log(‘name of class: ‘ + Person.getNameOfClass());

//使用new创建类实例
var uncleWang = new Person(‘WangXiaobo‘);
uncleWang.displayName();


call/apply/bind

在JavaScript中, 函数有call, apply和bind方法, 都可以用于改变函数的执行上下文. 有 关这些方法详细说明可参考:

在函数中, this指向这些方法绑定的对象, 如果绑定的对象是undefined或者nullthis指向全局上下文.

window.hello = ‘hello of window‘;

var obj = {
    hello: ‘hello of obj‘
};

var sayHello = function () {
    console.log(this.hello);
};

//纯函数执行, `this`指向定义处上下文
//显示 hello of window
sayHello();

//call, apply 方法运行函数并使得运行时函数中`this`指向obj对象
sayHello.call(obj);
sayHello.apply(obj);

//bind返回一个包装过的函数
var bindedSayHello = sayHello.bind(obj);
//执行包装函数时, 会调用sayHello函数, 且sayHello函数中`this`指向obj对象
bindedSayHello();
//
var obj2 = {
    hello: ‘hello of obj2‘
};
//即使使用call或apply改变包装函数中`this`的上下文, 但执行sayHello函数时, sayHello函
//数中`this`依然指向obj对象
//有一点点难于理解, 是吧?
bindedSayHello.call(obj2);


bind有时难于理解, 可以参考以下bind的简单实现以理解其原理.

/**
 * @param {Function} fn
 * @param {Object} context
 * @return {Function}
 */
var bind = function (fn, context) {
    return function () {
        fn.apply(context, arguments);
    };
}
var bindedSayHello2 = bind(sayHello, obj);
bindedSayHello2();


事件监听器

由于各个浏览器实现的不一致, DOM事件监听器中的this会给开发者带来一点困扰.

绑定事件监听器的方式

Html元素绑定事件监听器的方式一般有以下几种:

  1. 行内绑定
    • 在html 元素的属性上直接设置, 比如<a ... onclick="alert(‘link‘)" ...
  2. DOM Level 0 方式绑定
    • 使用JS设置元素属性方式, 如 link.onclick = function () {...};
  3. DOM Level 2 方式绑定
    • 使用addEventListener方法绑定事件
    • 只适合支持w3c DOM 2规范的浏览器
  4. IE
    • ie9 之前版本(不包含ie9) 使用attachEvent方法绑定事件
    • ie9+ 可以使用DOM Level 2 方式绑定事件

测试

可以用以下html代码测试在不同浏览器下, 使用不同方式绑定的监听器中this的指向.

<!DOCTYPE html>
<html>
<head>
    <title>test this</title>
    <meta charset="utf-8" />
</head>
<body>
    <h1> test this </h1>
    <div>
        <p id="inline" onclick="console.log(this)" >
            <input type="button" value="test inline" />
        </p>
        <p id="dom0" >
            <input type="button" value="test dom level 0" />
        </p>
        <p id="dom2-w3c" >
            <input type="button" value="test dom level 2 for w3c" />
            (only available in browser that supports DOM2)
        </p>
        <p id="dom-ie" >
            <input type="button" value="test dom level 2 for ie" />
            (only available in IE)
        </p>
    </div>
    <script>
        var dom0= document.getElementById(‘dom0‘),
            dom2W3c = document.getElementById(‘dom2-w3c‘),
            domIe = document.getElementById(‘dom-ie‘);

        dom0.onclick = function () {
            console.log(this);
        };
        if (domIe.attachEvent) {
            domIe.attachEvent(‘onclick‘, function () {
                console.log(this);
            }, false);
        }
        if (dom2W3c.addEventListener != null) {
            dom2W3c.addEventListener(‘click‘, function () {
                console.log(this);
            }, false);
        }
    </script>
</body>
</html>


如果在不同的浏览器测试以上代码, 可以总结做成以下图表.

浏览器 行内绑定 DOM Level 0 addEventListener attachEvent
w3c 系列 Element Element Element X
ie 系列 Element Element Element (ie9+) Window
  • Element : this 指向被绑定的元素
  • Window : this指向window对象
  • X : 浏览器不支持某些特性, 无法测试

结论

根据以上图表, 我们很容易得出结论:

  1. 在IE中使用attachEvent 方式绑定的监听器中, this 指向window对象.
  2. 在其他方式绑定的监听器中, this 指向被绑定的元素.
    • 注意: 被绑定的元素, 并不是event.target, 虽然在某些情况下二者可能是同一对象.

扩展阅读

  1. Javascript的this用法 - 阮一峰的网络日志
  2. Function.prototype.apply() - JavaScript | MDN
  3. Function.prototype.call() - JavaScript | MDN
  4. Function.prototype.bind() - JavaScript | MDN
  5. Javascript - The this keyword
  6. Javascript - Early event handlers
  7. Javascript - Traditional event registration model
  8. Javascript - Advanced event registration models

@end

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。