导读
在JavaScript中关于this的各种问题已经是屡见不鲜了,但是当我们看到某个关于this的问题时,有时候还是不能快速、准确地构建出思路。今天这篇文章帮助你系统地构建出this的各种使用场景。
this解读
为什么会有this
熟悉JavaScript的同学都知道,执行环境
是JavaScript的核心概念之一。简单介绍下,执行环境就是JavaScript中所有变量生存的环境,环境决定了变量的生命周期,以及引用关系(我能引用谁 AND 谁能引用我)。
而函数是比较特殊的,每一个函数都有自己的执行环境。反过来说,函数也可以在不同的环境下运行,所以需要一种机制,能够在函数体内快速获得当前
的运行环境。this的设计目的,就是在函数内部指出当前的运行环境。
this的原理
this 总是(非严格模式下)指向一个对象,而具体指向哪个对象是在运行
时基于函数的执行环境
动态绑定的,而非函数被声明
时的环境。 简单来说,this对象是基于函数的执行环境绑定的。
this指向的不同场景
对象中的this
在对象中,通常用this区分对象属性和变量,这时this指向该对象
var obj={
name: "Li",
getName:function(){
console.log(this.name)
},
setName:function(name){
this.name=name;
}
}
函数中的this
当函数不是作为某个对象属性被调用,是以普通函数被调用,this一般指向全局对象
var name="Li";
var getName=function(){
console.log(this.name);
}
getName(); //"Li"
当函数用于特殊的作用时,会导致this的行为发生改变。例如在JavaScript中模拟构造函数
。
var name="Li"
var Person=function(){
this.name="AAA";
}
var me=new Person();
console.log(me.name); //"AAA"
当通过new
关键字调用函数时,总会返回一个该函数的实例,this通常也指向这个实例对象
。
call和apply
在ECMAScript
规范中,每个函数都包含call和apply方法。他们的作用实质都是在特定的作用域
调用函数,也就是改变当前函数
的this指向。
不同的是,call和apply在传参形式上不同。
- call() 参数一:函数上下文的对象 后续的参数为函数的参数列表
- apply() 参数一:函数上下文的对象 参数二为函数的参数组成的数组
用法
改变this指向
var obj1={
name:"AAA"
}
var obj2={
name:"BBB",
getName:function(){
console.log("名字:"+this.name);
}
}
obj2.getName(); // 名字:BBB
obj2.getName.apply(obj1); // 名字:AAA
这里我们把obj1作为apply的参数传入,此时函数里的this便指向了obj1,此时相当于访问obj1.name
扩充函数运行时的作用域(也是所谓的继承)
var name="AAA";
var obj={
name: "BBB"
}
function getName(){
console.log(this.name);
}
getName();//AAA
getName.apply(this); //AAA
getName.apply(obj); //BBB
在函数中是不存在name这个变量的,但是根据作用域链,最终会到全局作用域中寻找变量,从而显示"AAA";但是我们通过apply去显式改变执行环境,就能改变this的指向,从而获取到name属性。
调用函数
apply、call方法都会使函数立即执行,因此也可以用它们来调用函数
function fn(){
console.log("hello");
}
fn.apply(); //hello
箭头函数
箭头函数是函数的一种简写方式,它和普通函数最重要的区别就是它自身没有this值。简单来说,箭头函数在定义时,this就继承了定义箭头函数所在的环境的this
var name="AAA";
var obj={
name:"BBB",
getName: ()=>{
console.log(this.name);
}
}
obj.getName(); //AAA
var a=obj.getName();
a(); //AAA
可以看到,我们改变函数的执行环境并不会改变输出结果,因为箭头函数中的this在定义时就已经绑定到全局环境了。
再看下面这个例子:
var name="AAA";
function Person(){
this.name="BBB";
return ()=>{
console.log(this.name);
}
}
var a=new Person();
a(); //BBB
当我们在函数中使用箭头函数时,箭头函数的this就指向这个函数。
细说bind函数
bind函数时ES6新增的方法,它和call很相似(传参方面),但是不同的是,它会返回一个改变了上下文 this后的函数,对调用函数本身不会造成影响。
var obj={
name:"AAA"
}
function fn(){
this.name="BBB";
console.log(this.name);
}
var fn1=fn.bind(obj);
fn1(); //AAA
fn(); //BBB
在低版本浏览器中没有bind方法,为了兼容,我们可以自己实现一个,利用call方法
if(!Function.prototype.bind){
Function.prototype.bind=function(){
var self=this; //保存原函数
context=[].shift.call(arguments) //从arguments取出第一个参数,为需要绑定的上下文环境
args=[].slice.call(arguments) //把剩余的参数取出
return function(){
//返回一个函数,并且把传入bind方法的参数和使用新函数传入的参数传入
//因为在传参方面,我们通常使用arguments便于操作,它是一个类数组,
//我们使用apply方法更方便传参
self.apply(context,[].concat.call(args,...arguments));
}
}
}
这里简单说下 [].slice和Array.prototype.slice的区别:
[].slice实际上就是一个Array的实例,然后调用slice方法,而这个方法来自于Array的prototype上,在使用上效果完全一样。
要说不同之处,也就是this的指向不同,在[].slice中,this指向这个实例;在Array.prototype.slice中,this指向的是Array.prototype。
最后
借用《你不知道的JavaScript》中的总结:
Javascript 是一个文本作用域的语言,就是说,一个变量的作用域, 在写这个变量的时候确定。 this关键字是为了在JS中加入动态作用域而做的努力。所谓动态作用域,就是说变量的作用范围,是根据函数调用的位置而定的。
this是JS中的动态作用域机制,具体来说有四种,优先级有低到高分别如下:
默认的this绑定
:就是说在一个函数中使用了this,但是没有为this绑定对象。这种情况下, 非严格默认,this就是全局变量Node环境中的global,浏览器环境中的window。隐式绑定
:使用obj.foo()这样的语法来调用函数的时候,函数foo中的this绑定到obj 对象。显示绑定
: foo.call(obj,…),foo.apply(obj,[…]),foo.bind(obj,…)构造绑定
: new foo(),这种情况,无论foo是否做了绑定,都要创建一个新的对象,然后 foo中的this引用这个对象。