JS this指针深度剖析

JavaScript 函数被调用后会在一个特定的运行环境内执行,这个运行环境就是函数的调用者,或者说是调用函数的对象。如果函数没有调用者(不是通过对象调用,而是直接调用),那么运行环境就是全局对象 window。

为了在函数执行过程中能够引用(访问)运行环境,JavaScript 专门增加了一个 this 关键字。this 是一个指针型变量,它指向当前函数的运行环境。

在不同的场景中调用同一个函数,this 的指向也可能会发生变化,但是它永远指向其所在函数的真实调用者(谁调用就指向谁);如果没有调用者,this 就指向全局对象 window。

在《JS this和调用对象》一节中我们曾讲到 this 指针的初步使用,不了解的读者请猛击链接学习,本节重点对 this 指针进行深度剖析。

使用 this

this 是由 JavaScript 引擎在执行函数时自动生成的,存在于函数内的一个动态指针,指代当前调用对象。具体用法如下:

this[.属性]

如果 this 未包含属性,则传递的是当前对象。

this 用法灵活,其包含的值也是变化多端。例如,下面示例使用 call() 方法不断改变函数内 this 指代对象。

var x = "window";  //定义全局变量x,初始化字符串为“window”
function a () {  //定义构造函数a
    this.x = "a";  //定义私有属性x,初始化字符a
}
function b () {  //定义构造函数b
    this.x = "b";  //定义私有属性x,初始化为字符b
}
function c () {  //定义普通函数,提示变量x的值
    console.log(x);
}
function f () {  //定义普通函数,提示this包含的x的值
    console.log(this.x);
}
f();  //返回字符串“window”,this指向window对象
f.call(window);  //返回字符串“window”,指向window对象
f.call(new a());  //返回字符a,this指向函数a的实例
f.call(new b());  //返回字符b,this指向函数b的实例
f.call(c);  //返回undefined,this指向函数c对象

下面简单总结 this 在 5 种常用场景中的表现以及应对策略。

1. 普通调用

下面示例演示了函数引用和函数调用对 this 的影响。

var obj = {  //父对象
    name : "父对象obj",
    func : function () {
        return this;
    }
}
obj.sub_obj = {  //子对象
    name : "子对象sub_obj",
    func : obj.func
}
var who = obj.sub_obj.func();
console.log(who.name);  //返回“子对象sub_obj”,说明this代表sub_obj

如果把子对象 sub_obj 的 func 改为函数调用。

obj.sub_obj = {
    name : "子对象sub_obj",
    func : obj.func()  //调用父对象obj的方法func
}

则函数中的 this 所代表的是定义函数时所在的父对象 obj。

var who = obj.sub_obj.func;
console.log(who.name);  //返回“父对象obj”,说明this代表父对象obj

2. 实例化

使用 new 命令调用函数时,this 总是指代实例对象。

var obj = {};
obj.func = function () {
    if (this == obj) console.log("this = obj");
    else if (this == window) console.log("this = window");
    else if (this.contructor == arguments.callee) console.log("this = 实例对象");
}
new obj.func;  //实例化

3. 动态调用

使用 call 和 apply 可以强制改变 this,使其指向参数对象。

function func () {
    //如果this的构造函数等于当前函数,则表示this为实例对象
    if (this.contructor == arguments.callee) console.log("this = 实例对象");
    //如果this等于window,则表示this为window对象
    else if (this == window) console.log("this = window对象");
    //如果this为其他对象,则表示this为其他对象
    else console.log("this == 其他对象 /n this.constructor =" + this.constructor);
}
func();  //this指向window对象
new func();  //this指向实例对象
cunc.call(1);  //this指向数值对象

在上面示例中,直接调用 func() 时,this 代表 window 对象。当使用 new 命令调用函数时,将创建一个新的实例对象,this 就指向这个新创建的实例对象。

使用 call() 方法执行函数 func() 时,由于 call() 方法的参数值为数字 1,则 JavaScript 引擎会把数字 1 强制封装为数值对象,此时 this 就会指向这个数值对象。

4. 事件处理

在事件处理函数汇总,this 总是指向触发该事件的对象。

<input type="button" value="测试按钮" />
<script>
    var button = document.getElementsByTagName("put")[0];
    var obj = {};
    obj.func = function () {
        if (this == obj) console.log("this = obj");
        if (this == window) console.log("this = window");
        if (this == button) console.log("this = button");
    }
    button.onclick = obj.func;
</script>

在上面代码中,func() 所包含的 this 不再指向对象 obj,而是指向按钮 button,因为 func() 是被传递给按钮的事件处理函数之后才被调用执行的。

如果使用 DOM2 级标准注册事件处理函数,程序如下:

if (window.attachEvent) {  //兼容IE模型
    button.attachEvent("onclick", obj.func);
} else {  //兼容DOM标准模型
    button.addEventListener("click", obj.func, true);
}

在 IE 浏览器中,this 指向 window 对象和 button 对象,而在 DOM 标准的浏览器中仅指向 button 对象。因为,在 IE 浏览器中,attachEvent() 是 window 对象的方法,调用该方法时,this 会指向 window 对象。

为了解决浏览器兼容性问题,可以调用 call() 或 apply() 方法强制在对象 obj 身上执行方法 func(),避免出现不同的浏览器对 this 解析不同的问题。

if (window.attachEvent) {
    button.attachEvent("onclick", function () {  //用闭包封装call()方法强制执行func()
        obj.func.call(obj);
    });
} else {
    button.attachEventListener("onclick", function () {
        obj.func.call(obj);
    }, true);
}

当再次执行时,func() 中包含的 this 始终指向对象 obj。

5. 定时器

使用定时器调用函数。

var obj = {};
obj.func = function () {
    if (this == obj) console.log("this = obj");
    else if (this == window) console.log("this = window对象");
    else if (this.constructor == arguments.callee) console.log("this = 实例对象");
    else console.log("this == 其他对象 /n this.constructor =" + this.constructor);
}
setTimeOut(obj.func, 100);

在 IE 中 this 指向 window 对象和 button 对象,具体原因与上面讲解的 attachEvent() 方法相同。在符合 DOM 标准的浏览器中,this 指向 window 对象,而不是 button 对象。

因为方法 setTimeOut() 是在全局作用域中被执行的,所以 this 指向 window 对象。要解决浏览器兼容性问题,可以使用 call 或 apply 方法来实现。

setTimeOut (function () {
    obj.func.call(obj);
}, 100);

this 安全策略

由于 this 的不确定性,会给开发带来很多风险,因此使用 this 时,应该时刻保持谨慎。锁定 this 有以下两种基本方法。

  • 使用私有变量存储 this。
  • 使用 call 和 apply 强制固定 this 的值。

下面结合 3 个案例进行说明。

示例1

使用 this 作为参数来调用函数,可以避免产生 this 因环境变化而变化的问题。例如,下面做法是错误的,因为 this 会始终指向 window 对象,而不是当前按钮对象。

<input type="button" value="按钮1" onclick="func()" />
<input type="button" value="按钮2" onclick="func()" />
<input type="button" value="按钮3" onclick="func()" />
<script>
    function func() {
        console.log(this.value);
    }
</script>

如果把 this 作为参数进行传递,那么它就会代表当前对象。

<input type="button" value="按钮1" onclick="func(this)" />
<input type="button" value="按钮2" onclick="func(this)" />
<input type="button" value="按钮3" onclick="func(this)" />
<script>
    function func (obj) {
        console.log(obj.value);
    }
</script>

示例2

使用私有变量存储 this,设计静态指针。

例如,在构造函数中把 this 存储在私有变量中,然后在方法中使用私有变量来引用构造函数的 this,这样在类型实例化后,方法内的 this 不会发生变化。

function Base () {  //基类
    var _this = this;  //当初始化时,存储实例对象的引用指针
    this.func = function () {
        return _this;  //返回初始化时实例对象的引用
    };
    this.name = "Base";
}
    function Sub () {  //子类
        this.name = "Sub";
    }
Sub.prototype = new Base();  //继承基类
var sub = new Sub();  //实例化子类
var _this = sub.func();
console.log(_this.name);  //this始终指向基类实例,而不是子类实例

示例3

使用 call 和 apply 强制固定 this 的值。

作为一个动态指针,this 也可以被转换为静态指针。实现方法:使用 call() 或 apply() 方法强制指定 this 的指代对象。

//把this转换为静态指针
//参数obj表示预设置this所指代的对象,返回一个预备调用的函数
Function.prototype.pointTo = function (obj) {
    var _this = this;  //存储当前函数对象
    return function () {  //返回一个闭包函数
        return _this.apply(obj, arguments);  //返回执行当前函数,并强制设置为指定对象
    }
}

为 Function 扩展一个原型方法 pointTo(),该方法将在指定的参数对象上调用当前函数,从而把 this 绑定到指定对象上。

下面利用这个扩展方法,实现强制指定对象 obj1 的方法 func() 中的 this 始终指向 obj1。具体如下说明:

var obj1 = {
    name : "this = obj1"
}
obj1.func = (function () {
    return this;
}).pointTo(obj1);  //把this绑定到对象obj1身上
var obj2 = {
    name : "this = obj2",
    func : obj1.func
}
var _this = obj2.func();
console.log(_this.name);  //返回“this=obj1”,说明this指向obj1,而不是obj2

可以扩展 new 命令的替代方法,从而间接实现自定义实例化类。

//把构造函数转换为实例对象
//参数func表示构造函数,返回构造函数func的实例对象
function instanceFrom (func) {
    var _arg = [].slice.call(arguments, 1);  //获取构造函数可能需要的初始化函数
    func.prototype.constructor = func;  //设置构造函数的原型结构器指向自身
    func.apply(func.prototype, _arg);  //在原型对象上调用构造函数
                                       //此时this指代原型对象,相当于实例对象
    return func.prototype;  //返回原型对象
}

下面使用这个实例化类函数把一个简单的构造函数转换为具体的实例对象。

function F () {
    this.name = "F";
}
var f = instanceFrom(F);
console.log(f.name);

call() 和 apply() 具有强大的功能,它不仅能够执行函数,也能够实现 new 命令的功能。

绑定函数

绑定函数是为了纠正函数函数的执行上下文,把 this 绑定到指定对象上,避免在不同执行上下文中调用函数时,this 指代的对象不断变化。

function bind(fn, context) {
    return function () {
        return fn.apply(context, arguments);
    };
}

bind() 函数接收一个函数和一个上下文环境,返回一个在给特定环境中调用给函数的函数,并且将返回函数的所有的参数原封不动地传递给调用函数。

这里的 arguments 属于内部函数,而不属于 bind() 函数。在调用返回的函数时,会在给定的环境中执行被传入的函数,并传入所有参数。

函数绑定可以在特定的环境中为指定的参数调用另一个函数,该特征常与回调函数、时间处理函数一起使用。

<button id="btn">测试按钮</button>
<script>
    var handler = {  //事件处理对象
        message : 'handler',  //名称
        click : function (event) {  //时间处理函数
            console.log(this.message);  //提示当前对象的message值
        }
    };
    var btn = document.getElementById('btn');
    btn.addEventListener('click', handler.click);
</script>

在上面示例中,为按钮绑定单击事件处理函数,设计当单击按钮时,将显示 handler 对象的 message 属性值。。但是,实际测试发现,this 最后指向了 DOM 按钮,而不是 handler。

解决方法:使用闭包进行修正。

var handler = {  //事件处理函数
    message : 'handler',  //名称
    click : function (event) {  //时间处理函数
        console.log(this.message);  //提示当前对象的message值
    }
};
var btn = document.getElementById('btn');
btn.addEventListener('click', function () {  //使用闭包进行修正:封装事件处理函数的调用
    handler.click();
});  //'handler'

改进方法:使用闭包比较麻烦,如果创建多个闭包可能会令代码变得难以理解和调试,而使用 bind() 绑定函数就很方便。

var handler = {  //事件处理函数
    message : 'handler',  //名称
    click : function (event) {  //事件处理函数
        console.log(this.message);  //提示当前对象的message值
    }
};
var btn = document.getElementById('btn');
btn.addEventListener('click', bind(handler.click, handler));  //‘handler’

使用 bind

ECMAScript 5 为 Function 新增了 bind 原型方法,用来把函数绑定到指定对象上。在绑定函数中,this 对象被解析为传入的对象。具体用法如下:

function.bind(thisArg [,arg1 [,arg2 [,argN]]]);

参数说明如下:

  • function:必需参数,一个函数对象。
  • thisArg:必需参数,this 可在新函数中引用的对象。
  • arg1 [,arg2 [,argN]]:可选参数,要传递到新函数的参数的列表。

bind() 方法将返回与 function 函数相同的新函数,thisArg 对象和初始参数除外。

示例1

下面示例定义原始函数 check,用来检测传入的参数值是否在一个指定范围内,范围下限和上限根据当前实例对象的 min 和 max 属性决定。然后使用 bind() 方法把 check 函数绑定到对象 range 身上。如果再次调用这个新绑定后的函数 check1,就可以根据该对象的属性 min 和 max 来确定调用函数时传入值是否在指定的范围内。

var check = function (value ) {
    if (typeof value !== 'number') return false;
    else return value >= this.min && value <= this.max;
}
var range = {min : 10, max : 20};
var check1 = check.bind(range);
var result = check1(12);
console.log(result);  //true

示例2

在上面示例基础上,下面示例为 obj 对象定义了两个上下限属性,以及一个方法 check。然后,直接调用 obj 对象的 check 方法,检测 10 是否在指定范围,返回值为 false,因为当前 min 和 max 值分别为 50 和 100。接着把 obj.check 方法绑定到 range 对象,再次传入值 10,返回值为 true,说明在指定范围,因为此时 min 和 max 值分别为 10 和 20。

var obj = {
    min : 50,
    max : 100,
    check : function (value) {
        if (typeof value !== 'number') {
            return false;
        } else {
            return value >= this.min && value <= this.max
        }
    }
}
var result = obj.check(10);
console.log(result);  //false
var range = {min : 10, max : 20};
var check1 = obj.check.bind(range);
var result = check1(10);
console.log(result);  //true

示例3

下面示例演示了如何使用 bind() 方法为函数传递两次参数值,以便实现连续参数求值计算。

var func = function (val1, val2, val3, val4) {
    console.log(val1 + " " + val2 + " " + val3 + " " + val4);
}
var obj = {};
var func1 = func.bind(obj, 12, "a");
func1 ("b", "c");  //12 a b c

链式语法

jQuery 框架最大亮点之一就是它的链式语法。实现方法:设计每一个方法的返回值都是 jQuery 对象(this),这样调用方法的返回结果可以为下一次调用其他方法做准备。

示例

下面示例演示如何在函数中返回 this 来设计链式语法。分别为 String 扩展了 3 个方法:trim、writeln 和 log,其中 writeln 和 log 方法返回值都为 this,而 trim 方法返回值为修剪后的字符串。这样就可以用链式语法在一行语句中快速调用这 3 个 方法。

Function.prototype.method = function (name, func) {
    if (!this.prototype[name]) {
        this.prototype.[name] = func;
        return this;
    }
}
String.method = ('trim', function () {
    return this.replace(/^/s+|/s+$/g, '');
});
String.method = ('writeln', function () {
    console.log(this);
    return this;
});
String.method = ('log', function () {
    console.log(this);
    return this;
});
var str = "abc";
str.trim().writeln().log();

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/23192.html

(0)
上一篇 2021年7月20日 11:02
下一篇 2021年7月20日 11:02

相关推荐

发表回复

登录后才能评论