5分钟带你搞懂 Javascript 中的this(包含apply、call、bind)

this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。this 的绑定一直是一件非常令人困惑的事,即使经验丰富的开发者有时也较难说清它的指向。

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录(上下文)的其中一个属性,会在函数执行的过程中用到。

一、this的指向

this 总是指向执行时当前对象

JavaScript 的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境

也就是说 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

除了使用 with 和 eval 的情况,常用的可分为以下几种:

  • 作为对象的方法调用。
  • 作为普通函数调用。
  • 构造器调用。
  • Function.prototype.call 或 Function.prototype.apply 调用。

1. 作为对象的方法调用

对象的方法被调用时,this 指向该对象。

var obj = {
a: 1,
getA() {
alert ( this === obj ); // 输出:true alert ( this.a ); // 输出: 1
}
};
obj.getA();

2. 作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指向全局对象。

这里注意对象的方法被单独拷贝出来后执行,那么原来的this会丢失,变成指向全局对象

//在浏览器的JavaScript 里,这个全局对象是window 对象。
window.name = 'globalName';
var getName = function(){
return this.name;
};
console.log( getName() ); // 输出:globalName

//或者:
window.name = 'globalName';
var myObject = {
name: 'sven',
getName: function(){
return this.name;
}
};
var getName = myObject.getName;
console.log( getName() ); // globalName

3. 构造器调用

通常情况下,构造器里的 this 就指向返回的这个对象;

  • 如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,this指向返回的这个对象;
  • 如果构造器显式地返回了一个对象,则实例化的结果会返回这个对象,而不是this;
//构造器里的this 就指向返回的这个对象,见如下代码:
var MyClass = function(){
this.name = 'sven';
};
var obj = new MyClass();
alert ( obj.name ); // 输出:sven


var MyClass = function(){
this.name = 'sven';
return { // 显式地返回一个对象
name: 'anne'
}
};
var obj = new MyClass();
alert ( obj.name ); // 输出:anne


var MyClass = function(){
this.name = 'sven'
return 'anne'; // 返回string 类型
};
var obj = new MyClass();
alert ( obj.name ); // 输出:sven

4. call 或 apply 调用

apply和call可以动态地改变传入函数的 this

var obj1 = {
name: 'sven',
getName: function(){
return this.name;
}
};
var obj2 = {
name: 'anne'
};
console.log( obj1.getName() ); // 输出: sven
console.log( obj1.getName.call( obj2 ) ); // 输出:anne

二、call, apply 和 bind

简介及区别

call 和 apply作用一模一样,区别仅在于传入参数形式的不同。

apply

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组

类数组:

  1. 对象本身要可以存取属性;
  2. 对象的 length 属性可读写。

call

call 传入的参数数量不固定,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数

bind

bind 参数类型和 call 相同,不过它不会执行函数,而是修改 this 后返回一个新的函数

var func = function( a, b, c ){
alert ( [ a, b, c ] );
};
func.apply( null, [ 1, 2, 3 ] );
func.call( null, 1, 2, 3 );

call 和 bind 是包装在 apply 上面的语法糖

Function.prototype.call = function( context ){ 
var argus = [];
for (var i = 1; i < arguments.length; i++) {
argus.push(arguments[i]);
}
this.apply(context, argus);
};

Function.prototype.bind = function( context ){ 
var self = this; // 保存原函数
// 返回一个新的函数
return function(){ 
// 执行新的函数的时候,会把之前传入的 context // 当作新函数体内的 this
return self.apply( context, arguments );
}
};

用途

  1. 改变 this 指向
  2. 借用其他对象的方法: 在操作类数组(比如 arguments) 的时候,可以找 Array.prototype 对象借用方法Array.prototype.push.call(a, 'first' )

三、eval 和 with

词法作用域由写代码期间函数所声明的位置来定义,而 eval 和 with 可以在运行时修改词法作用域。

eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词 法作用域。看起来它们能实现更复杂的功能,并且代码更具有扩展性。

但这些功能已经过时,性能也较差,使用不当容易导致难以预料的问题,已经不被提倡,严格模式下会被限制和禁止,所以不要轻易使用它们

eval

eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。

function foo(str, a) {
eval( str );
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

// 相当于
function foo(str, a) {
var b = 3;
console.log( a, b );
}

字符串的内容可以是一段动态生成的函数代码

JavaScript中 还有其他一些功能效果和eval(..)很相似。setTimeoutsetInterval 的第一个参数可以是字符串。

with

with 可以改变作用域,通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj" 
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式 
with (obj) {
a = 3;
b = 4;
c = 5;
}

性能

eval 和 with性能差

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

但如果引擎在代码中发现了 eval(..) 或 with,就无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。

因此如果大量使用会导致性能变差。

参考

  • JavaScript设计模式与开发实践
  • 你不知道的JavaScript

文章来源于互联网:5分钟带你搞懂 Javascript 中的this(包含apply、call、bind)

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持以下吧
点赞0
分享
评论 抢沙发