Javascript中的this

this

this既不指向自身也不指向函数的词法作用域。

this实际上是在函数被调用时发生的变化,它指向什么完全取决于函数在哪里被调用。

调用位置

函数在代码中被调用的位置。

绑定规则

默认绑定

最常用的函数调用类型:独立函数调用。(无法应用其他规则时的默认规则)

function foo(){
    console.log(this.a);
}
var a = 2;
foo();//2

this指向全局window对象。

PS:在严格模式下,this会指向undefined。

隐式绑定

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
obj.foo();//2

foo()函数在obj对象中被调用。调用位置会使用obj上下文来引用函数。

当函数引用有上下文对象时,隐式绑定规则会将函数中调用的this绑定到这个上下文对象。

以为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。

隐式丢失
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
var bar = obj.foo;//函数别名!
var a = 3;
bar();//3

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时bar()就是一个不带任何修饰的函数调用,应用默认绑定。相当于在全局作用域下调用了foo函数。this指向全局window对象。

在看一个例子:

var a = 3;
function foo(){
    console.log(this.a);
}
function doFoo(fn){
    var a = 4;
    //fn引用的其实就是foo
    fn();//调用位置
}
var obj = {
    a:2,
    foo:foo
}

doFoo(obj.foo); //3

参数传递实际上就是一个隐性赋值。

还有一个更加直观的例子需要理解:

var a = 3;
function doFoo() {
    var a = 4;
    //fn引用的其实就是foo
    function foo() {
        console.log(this.a);
    }
    foo(); //调用位置
}
doFoo();//3

为什么是3不是4?

形成的调用栈(到达调用位置所调用的所有函数)中,foo在doFoo被执行时中被调用,doFoo()在全局下被调用,doo中的this指向全局window。(丢失了this绑定)应用的是默认绑定。

显式绑定

分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。

不想在函数内部包含函数引用,想在某个对象上强制调用函数,怎么做?

可以使用call(…)和apply(…)方法。

第一个参数是一个对象,是给this准备的,在调用函数时将其绑定到this。

function foo(){
    console.log(this.a);
}
var obj = {
    a:2
}
foo.call(obj);//2

通过call方法将foo函数内部的this强制指向了obj。

function foo(){
    console.log(this.a);
}
var obj = {
    a:2
}
var bar = function(){
    foo.call(obj);
}
bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改它的this
bar.call(window);//2

上面的例子中,创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此foo的this强制绑定到了obj上。

之后无论怎么调用bar函数,它总会手动在obj上调用foo。称为硬绑定

硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值。

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a:2
}
var bar = function(){
    return foo.apply(obj,arguments);
}
var b = bar(3);//2,3
console.log(b);//5

还有一种使用方法是创建一个可以重复使用的辅助函数

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
//简单的辅助绑定函数
function bind(fn,obj){
    return function(){
        return fn.apply(obj,argunments);
    }
}
var obj = {
    a:2
}
var bar = bind(foo,obj);
var b = bar(3);//2 3
console.log(b);//5

ES5提供了内置的方法Function.prototype.bind。

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a:2
}
var bar = foo.bind(obj);
var b = bar(3);//2 3
console.log(b);//5

bind()方法会返回一个硬编码的新函数,它会将你制定的参数设置为this的上下文并调用原始函数。

API中调用的上下文
function foo(el){
    console.log(el,this,id);
}
var obj = {
    id:'awesome'
}
//调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj);//1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call()或者apply()实现了显示绑定。

new绑定

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,并不属于某个类,也不会实例化一个类。

使用new来调用函数或者说发生构造函数调用时,会自动执行以下的操作。

  1. 创建一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2

优先级

function foo(){
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    foo:foo
}
obj1.foo();//2
obj2.foo();//3

obj1.foo.call(obj2);//3
obj2.foo.call(obj1);//2

从上面的例子可以看出显式绑定的优先级高于隐式绑定。

function foo(something){
    this.a = something;
}
var obj1 = {
    foo: foo
}
var obj2 = {};

obj1.foo(2);
console.log(obj1.a);//2

obj1.foo.call(obj2, 3);
console.log(obj2.a);//3

var bar = new obj1.foo(4);
console.log(obj1.a);//2
console.log(bar.a);//4

从上面的例子可以看出new绑定的优先级高于隐式绑定。

比较显式绑定和new绑定的优先级。

硬绑定如何工作:Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。

bar被硬绑定到obj1上,但是new bar(3)病没有把obj1.a修改为3。

new修改了硬绑定(到obj1的)调用bar(..)中的this。

因为使用了new绑定,得到一个名为baz的新对象,并且baz.a的值为3。

为什么要在new中使用硬绑定的函数呢?主要目的是预先设置函数的一些参数,这样在new进行初始化的时就可以只传入其余的参数。

bind()的功能之一就是可以把除了第一个参数(用于绑定this)之外的其他参数都传给下层 的函数。

function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val;//p1p2

判断this

  1. 函数是否在new中调用?是的话this绑定的时新创建的对象。 var bar = new foo();
  2. 函数是否通过call、apply或者硬绑定调用?是的话this绑定的是指定的对象。var bar = foo.call(obj);
  3. 函数是否在某个上下文对象中调用(隐式绑定)?是的话this绑定到那个上下文对象中。var bar = obj1.foo();
  4. 如果以上都不是,则使用默认绑定。严格模式下绑定到undefined,否则绑定到全局对象。

例外情况

被忽略的this

如果将null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用的时候会被忽略,实际应用的是默认绑定规则。

function foo(){
    console.log(this.a);
}
var a = 2;
foo.call(null);//2

什么时候传入null?

  1. 使用apply来“展开”一个数组,并将参数传入一个函数。
  2. bind()可以预先设置一些参数。
function foo(a, b){
    console.log("a:" + a + ",b+" + b);
}
//把数组展开成参数
foo.apply(null, [2,3]);//a:2,b:3

//使用bind()预先设置一些参数
var bar = foo.bind(null, 2);
bar(3);//a:2,b:3

这两个方法都需要传入一个参数当做this的绑定对象。如果函数不关心this的话,可以传入null。

在ES6中,可以用…操作符代替apply(..)来“展开”数组,foo(..[1,2])和foo(1,2)是一样的。

总是忽略this,传入null可以会产生一些问题。如果某个函数使用到了this,此时默认绑定规则会将this绑定到全局变量。

更安全的this

一种更加安全的做法是传入一个特殊的对象,把this绑定到这个不会出现任何副作用的对象上。不影响全局。

function foo(a, b){
    console.log("a:" + a + ",b+" + b);
}

var obj = Object.creat(null);

//把数组展开成参数
foo.apply(obj, [2,3]);//a:2,b:3

//使用bind()预先设置一些参数
var bar = foo.bind(obj, 2);
bar(3);//a:2,b:3

Object.creat(null)和{ }很像,但是并不会创建Object.prototype这个委托,它比{ }更加空。

间接引用

有时候在无意中可能会创建一个函数的“间接引用”,这种情况下调用这个函数会应用默认绑定原则。

间接引用在最容易在赋值时发生:

function foo(){
    console.log(this.a);
}
var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};

o.foo(); //3
(p.foo = o.foo)();//2

赋值表达式p.foo = o.foo的返回值时目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。这里会应用默认绑定。

PS:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this绑定到undefined,否则绑定到全局对象。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值