在 JavaScript 中, this 指当前调用对象,用在函数体内。
有些函数具有 this 引用,有时候这些 this 确实会指向调用位置的对象引用。但是 这种用法从本质上来说并没有把一个函数变成一个"方法",因为 this 是在运行时根据调 用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。
this 是一个函数体内的自带的的对象指针,它始终指向调用对象。当函数被调用时,使用 this 可以访问调用对象。其使用局限在函数体内或调用范围内。
this[.属性]
如果 this 未包含属性,则传递的是当前对象。
this 用法比较灵活,它可以存在任何位置,它并不仅仅局限于对象的方法内,还可以被应用在全局域内、函数内,以及其它特殊上下文环境中。
var x = 1; //声明全局变量的初始值
var obj = {
f: function () {
//定义方法 f
console.log(this.x); //访问当前运行环境中的 x 属性值
},
x: 2, //定义属性 x ,赋值为 2
}((obj.f = obj.f))(); //1
(false || obj.f)(); //1
(obj.f, obj.f)(); //1
函数的引用和调用时两个不同的概念。虽然它们都无法改变函数的定义作用域。但是引用函数,却能够改变函数的执行作用域,而调用函数不会改变函数的执行作用域。
var o = {
name: '对象 o',
f: function () {
return this;
},
};
o.ol = {
name: '对象 ol',
me: o.f,
};
//引用对象 o 的方法 f
在上面的实例中,函数中的 this 所代表的是当前执行对象 ol :
var who = o.ol.me();
alert(who.name);
// 返回字符串"对象 ol",当前 this 代表 ol
但如果把对象 ol 的 me 属性改成函数调用:
o.ol = {
name = "对象 ol",
me = o.f()
// 调用对象 o 的方法 f
}
则函数中 this 所代表的是当前执行对象 o :
var who = o.ol.me;
alert(who.name); //返回字符串"对象 o",说明当前 this 代表对象 o
call() 和 apply() 方法可以直接改变被执行函数的作用域,使其作用域指向所传递的参数对象。因此,函数中的 this 也指向参数对象。
function f() {
// 如果当前执行域对象的构造函数等于当前函数,则表示 this 为实例对象
if (this.constructor == arguments.callee) alert('this = 实例对象');
// 如果当前执行域对象等于 window ,则表示 this 为 window 对象
else if (this == window) alert('this = window 对象');
// 如果当前执行域为其它对象,则 this 为其它对象
else alert('this = 其它对象 \n this.constructor =' + this.constructor);
}
f(); //this 指向 window 对象
new f(); // this 指向 实例对象
f.call(1); // this 指向数字实例对象
直接调用 f() 时,函数的执行作用域为全局作用域,所以 this 代表 window 。当使用 new 运算符调用函数时,将创建一个新的实例对象,函数的执行作用域为实例对象所在的上下文,所以 this 就指向这个新的实例对象。而使用 call() 方法执行函数 f() 时, call 会把函数 f() 的作用域强制修改为参数对象所在的上下文。由于 call() 这个方法的参数值为数字 1,则 JavaScript 解释器强行将数字 1 封装成数值对象,此时 this 就会指向这个数值对象。
function f() {
// 函数 f()
alert(this.x + this.y);
}
var o = {
//对象直接量
x: 1,
y: 2,
};
f.call(o); //执行函数 f(),返回值 3
JavaScript 通过原型继承模式实现类的延续和继承。如果在父类的成员中包含了 this 关键字,当子类继承了父类的这些成员时, this 的指向变得很迷人。
一般情况下,子类继承父类的方法后, this 指向子类的实例对象,但是也可能指向子类的原型对象,而不是子类的实例对象。
function Base() {
//基类
this.m = function () {
//基类的方法
return 'Base';
};
this.a = this.m(); //基类的属性 a ,调用当前作用域中 m() 方法
this.b = this.m; //基类的方法 b(),调用当前作用域中 m() 方法
this.c = function () {
//基类的方法 c ,以闭包结构调用当前的作用域的 m() 方法
return this.m();
};
}
function F() {
//子类
this.m = function () {
//子类的方法
return 'F';
};
}
F.prototype = new Base(); //继承子类
var f = new F(); //实例化子类
alert(f.a); //返回字符串 "Base"
alert(f.b()); //返回字符串 "Base"
alert(f.c()); //返回字符串 "F"
在上面的示例中,基类 Base 包含四个成员,其中成员 b 和 c 以不同的方式引用当前作用域方法 m(),而成员 a 存储着当前作用域内方法 m() 的调用值。当这些成员继承给子类 F 后,其中 m 、 b 和 c 成为原型对象的方法,而 a 成为原型对象的属性。但是, c 的值是一个闭包体,当在子类的实例化中调用时,实际上它会返回值已经成为实例化对象的成员,也就是说,闭包体在哪了被调用,则其中的包含的 this 就会指向哪而。所以, f.c() 指向的是实例对象,而不是 F 类的原型对象。
为了避免因继承关系而影响父类中 this 所代表的对象,除了通过上面介绍的的方法,把方法的引用传递给父类的成员外,我们还可以为父类定义私有函数,然后再把它的引用传递给其它的父类成员,这样就避免了函数闭包的原因,而改变 this 的值。
function Base() {
//定义基类的私有函数 _m()
var _m = () => {
return 'Base';
};
this.a = _m;
this.b = _m();
}
这样基类的函数 _m()
具有完全隐私性,外界其它的任何对象都无法直接访问基类的私有函数 _m()
。所以,在一般情况下,定义方法的时候,对于相互依赖的方法,可以把它定义私有函数,并引用方法外的方式对外公布,这样就避免了外界对于依赖方法的影响。
异步调用就是通过事件机制或者计时器来延迟函数的调用时间和时机。通过调用函数的执行作用域不再是原来的的定义作用域,所以函数中的 this 总是指向引发该事件的对象。
<input type="button" value="Button" />
<script>
var button = document.getElementsByTagName('input')[0];
var o = {};
o.f = () => {
if (this == o) alert('this = o');
if (this == window) alert('this = window');
if (this == button) alert('this = button');
};
button.onclick = o.f;
</script>
书中说该函数的 this 指向 button ,在测试 edge 、 Firefox 和 chrome 开发版浏览器,都显示 this 指向 window 。
异步调用的另一种方式,就是计时器来调用函数,计时器就是指调用 window 对象的 setTimeout() 和 setInterval() 方法来延长调用函数。
var o = {};
o.f = () => {
if (this == o) alert('this = o');
if (this == window) alert('this = window');
if (this == button) alert('this = button');
};
setTimeout(o.f, 100);
this 的复杂性绝大部分取决于用户的使用方式。由于 this 指代灵活,如果把其放到复杂的环境中,其也会变得不确定。
如果把 this 当作参数值来调用函数,则可以避免许多的 this 多变的问题因为 this 始终与当前对象保持一致。
下面是错误的使用方式,因为 this 始终指向 window 。
<input type="button" value="按钮1" onclick="f()" />
<input type="button" value="按钮2" onclick="f()" />
<input type="button" value="按钮3" onclick="f()" />
<script>
function() { alert(this) }
</script>
但如果将 this 作为参数传入,则会达到预期的效果。
<input type="button" value="按钮1" onclick="f(this)" />
<input type="button" value="按钮2" onclick="f(this)" />
<input type="button" value="按钮3" onclick="f(this)" />
<script>
function(o) {
alert(o.value)
}
</script>
设置静态的 this 指针
如果确保构造函数的方法始终在初始化之后包含的 this 指针不再变化,一个简单的方法就是:在沟站函数中把 this 指针储存在私有变量中,然后在方法中使用私有变量来引用 this 指针,这样所引用的对象始终都是初始化的实例对象,而不会在类型继承中发生变化。
function Base() {
//基类
var _this = this; //储存初始化时对象的引用指针
this.m = () => {
return _this; //返回初始化对象的引用指针
};
this.name = 'Base';
}
function F() {
//子类
this.name = 'F';
}
F.prototype = new Base(); //继承基类
var f = new F(); //实例化基类
var n = f.m();
alert(n.name); // this 始终指向原型。即 "Base"
对于对象直接量来说,如果希望 this 代表直接量,则可以直接调用对象直接量的名称,而不是 this 关键字。
var o = {
name: 'this = o',
b: () => {
return o;
},
};
var ol = { name: 'this = ol', b: o.b };
var a = ol.b();
alert(a.name);
设计静态的 this 扩展方法
当然,作为一个动态指针, this 也可以被转化为静态指针。实现的方法主要有利用 JavaScript 对象 call() 和 apply() 方法。
//把 this 转换成静态指针//参数: o 表示返回设置 this 所指代的对象//返回值:返回一个闭包函数
Function.prototype.pointTo = o => {
var _this = this;
return () => {
return _this.apply(o, arguments); //返回当前函数,并把当前函数的作用域强制设置为指定对象
};
};
该方法调用当前函数,并在指定的参数对象上执行,从而把 this 绑定在该对象上。
var o = { name: 'this = o' };
o.b = function () {
return this;
}.pointTo(o);
var ol = { name: 'this = ol', b: o.b };
var a = ol.b();
alert(a.name);
还可以扩展 new 运算符的代替方法。
//构造函数转化为实例对象//参数: f 表示构造函数//返回值:返回构造函数 f() 的实例对象
function instanceFrom(f) {
//获取构造函数的参数
var a = [].slice.call(arguments, 1);
// 手工设置构造函数的原型构造器
f.prototype.constructor = f;
// 在原型对象上强制指定构造函数,则原型对象就成为了构造函数的实例
// 同时由于它的构造器以及设置了构造函数,则此时构造函数就相当于一个构造函数的实例对象
f.apply(f.prototype, a);
// 返回该原型对象
return f.prototype;
}
function F() {
this.name = 'F';
}
var f = instanceFrom(F);
alert(f.name); // 返回 "F"
由此可见, call() 和 apply() 方法具有强大的功能,它不仅能够执行普通函数,也可以实例化构造函数,担当 new 运算符的运算功能。
function f() {
this.x = function (y) {
return y;
};
}
f();
alert(f.x(4));
在上例中,由于 f() 的调用发生在 window 对象上,而 x() 又是函数 f() 体内定义,所以 this 指向 window 对象。
在 JavaScript 中,共有 4 种函数调用模式:方法调用模式、函数调用模式、构造器调用模式和 apply() 调用模式。这些模式在如何初始化 this 上存在差异。
调用运算符是小括号,小括号内可包含零个或多个用逗号分开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形参。当实际参数 ( arguments ) 的个数与形式参数 ( parameters ) 的个数不匹配时不会导致运行时错误。不会对参数进行类型检查,任何类型的值都可以被传递给参数。
当一个函数被保存为对象的一个属性值时,将之称之为方法。当一个方法被调用时, this 被绑定到当前对象。
var obj = {
value: 0,
increment: function (inc) {
this.value += typeof inc === 'number' ? inc : 1;
},
};
obj.increment();
console.log(obj.value);
obj.increment(2); // 1
console.log(obj.value); // 3
在上面创建了一个 obj 对象,它有一个 value 属性和一个 increment 方法。 increment 方法接受一个可选参数,当参数为非数字时,那么默认为 1。
increment 方法可以通过使用 this 去访问对象,所以它能从它的对象中取值或修改对象。 this 到对象的绑定发生在调用的时候。这个延迟绑定使函数可以对 this 高度复用。通过 this 可取的 increment 方法所属对象的上下文的方法称为公共方法。
当以函数模式调用时, this 被绑定在全局对象。这是语言设计的缺陷,如果语言设计正确,当在函数体内调用函数时, this 应该绑定在外部函数的 this 变量。这个设计错误的后果就是方法不能在利用内部函数来帮助它工作,因为内部函数和的 this 被绑定了错误值,所以不能共享该方法对对象的访问权。
解决方法:如果该方法定义的一个变量并将其赋值为 this ,那么内部函数就可以通过这个变量访问 this 。按照约定,将这个变量命名为 that 。
var obj = {
value: 1,
doub: function () {
var that = this;
var helper = () => (that.value = that.value * 2);
helper();
},
};
obj.doub();
console.log(obj.value); //2
JavaScript 是基于原型继承的语言,对象可以直接从其它对象继承属。当今大多数语言都是基于类的语言,虽然原型继承有着强大的表现力,但它偏离了主流方法,并不被广泛理解。 JavaScript 为了能够兼容基于类语言的编写风格,提供了类似类语言的对象构建语法。
如果在一个函数前加 var 运算符来调用,那么将建立隐藏链接到该函数的 prototype 原型语言的新实例对象,同时 this 将会被绑定在这个新实例对象上。注意, new 运算符也会改变 return 的语言行为。
var F = function (string) {
this.status = string;
};
F.prototype.get = function () {
return this.status;
};
var f = new F('new object');
console.log(f.get()); //2
上面代码创建了名为 F 的构造函数,此函数构建了一个带有 status 属性的对象。然后,为 F 所有的实例提供了一个名为 get 的公共方法。最后,创建一个实例对象,并调用 get 方法,以读取 status 属性的值。
结合 new 前缀调用的函数成为构造函数。按照约定,构造函数应该保存以大写格式命名的变量中。如果调用构造函数时没有在前面家伙是那个 new ,可能会发生非常糟糕的事情,既没有编译错误,也没有运行警告,所以大写约定非常重要。
JavaScript 是面向对象的编程语言,函数可以拥有方法。 apply 就是函数的一个基本方法,使用该方法可以调用函数,并修改函数体内的 this 值。 apply 方法包括两个参数:第 1 个参数设置绑定给 this 值,第二个参数包含函数参数的数组。
var array = [5, 4];
var add = function () {
var i,
sum = 0;
for (i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
};
var sum = add.apply({}, array);
alert(sum); //9