this全面解析

最近在拜读《你不知道的js》,而此篇是对于《你不知道的js》中this部分的笔记整理,但愿能有效的梳理,而且巩固关于this的知识点浏览器

1、调用位置

一、什么调用位置?

调用位置就是函数在代码中被调用的位置(而不是声明的位置)安全

二、如何寻找函数被调用的位置?

关键:分析调用栈,即为了到达当前执行位置所调用的全部函数。而咱们关心的调用位置就在当前正在执行的函数的前一个调用中bash

先来看一段代码:app

function baz() {
    //当前调用栈是:baz
    // 所以,当前调用位置是全局做用域
    console.log("baz");
    bar(); // bar的调用位置
}
function bar() {
    // 当前调用栈是baz -> bar
    // 所以,当前调用位置在baz中
    console.log("bar");
    foo(); // foo的调用位置
}
function foo() {
    // 当前调用栈是baz -> bar -> foo
    // 所以,当前调用位置在bar中
    console.log("foo");
}
baz(); // <-- baz的调用位置
复制代码

咱们能够把调用栈想象成一个函数调用链,但这种方法麻烦且易出错。函数

但咱们可使用另外一种方式:使用浏览器的调试工具,设立断点,或直接在代码中插入debugger。运行代码时,调试器会在那个位置暂停,同时会展现当前位置的函数调用列表,这就是你的调用栈。真正的调用位置是栈中的第二个元素工具

2、绑定规则

一、默认绑定

最经常使用的函数调用类型是独立函数调用。可把这规则看作是没法应用其余规则时的默认规则。oop

先看一段代码:测试

function foo() {
    //当前调用栈是:baz
    // 所以,当前调用位置是全局做用域
    console.log(this.a);
}
var a = 2;
foo(); // 2
复制代码

从代码中发现this指向了全局对象,并且函数调用时应用了this的默认绑定。ui

如何判断是默认绑定?this

可从分析调用位置来看看foo()是如何调用的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,所以只能是默认绑定,没法应用其余规则

但若是是在严格模式下,又会有怎样的结果呢?请看以下代码:

function foo() {
    "use strict"
    console.log(this.a);
}
var a = 2;
foo(); // TypeError:this is undefined
复制代码

这段代码表示:虽然this的绑定规则彻底取决于调用位置,但只有在非严格模式下,默认绑定才绑定全局对象;在严格模式下则会绑定到undefined。

可是在严格模式下调用则不影响默认绑定:

function foo() {
    console.log(this.a);
}
var a = 2;
(function() {
    "use strict"
    foo(); // 2
})();
复制代码

注意:一般来讲不该该在代码中混合使用strict模式与非strict模式

二、隐式绑定

这条规则是指调用位置是否有上下文对象,或者是否被某个对象拥有或包含

先看如下代码:

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

该调用位置使用了obj上下文来引用函数,或者说函数被调用时obj对象“拥有”或“包含”它。

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

上述代码调用foo()时,this被绑定到obj,所以this指向了obj,this.a 与 obj.a 是同样的。

另外对象属性引用链中只有上一层或最后一层在调用位置中起做用。例如:

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo();// 42
复制代码

隐式丢失

被隐式绑定的函数会丢失绑定对象这是一个常见的this绑定问题,也就是说丢失后它会应用默认绑定,从而把this绑定到全局对象或undefined上,取决因而否是严格模式。

例1:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名
var a = "oops, global";// a是全局对象的属性
bar(); // "oops, global"
复制代码

虽然bar是obj.foo的引用,但却引用了foo函数的自己,此时的bar()是不带任何修饰的函数调用,所以使用了默认绑定

例2:

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    // fn其实引用的是foo
    fn(); // 调用位置
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";// a是全局对象的属性
doFoo(obj.foo); // "oops, global"
复制代码

这里使用了参数传递,也是隐式赋值,因此结果和例1同样

例3:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";// a是全局对象的属性
setTimeout(obj.foo, 100);// oops, global
复制代码

回调函数丢失this绑定是常见的,调用回调函数的函数可能会修改this

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

三、显式绑定

方法:可使用call或apply直接指定this的绑定对象

缺点:没法解决丢失绑定的问题

例:

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

若是你传入了一个原始值做为this绑定对象,这个原始值会被转换成它的对象形式(new xxx()),这叫装箱

(1)、硬绑定

此为显式绑定的一个变种,能够解决丢失绑定问题 缺点:会大大下降函数的灵活性,使用绑定以后就没法使用隐式绑定或者显式绑定来修改this

例:

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
复制代码

foo.call(obj)强制把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, arguments)
    }
}
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的上下文并调用原始函数

(2)、API调用“上下文”

经过 call() 或 apply() 实现

四、new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面操做 a、建立一个全新对象 b、新对象会被执行[[Prototype]]连接 c、新对象被绑定到函数调用的this d、若是函数没有返回其余对象,则自动返回新对象 代码:

var obj = {};
obj.__proto__ = Base.prototype;
var result = Base.call(obj);
return typeof result === 'obj' ? result : obj;
复制代码

3、优先级

一、隐式绑定与显式绑定

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
复制代码

显然:显式绑定 > 隐式绑定

二、new绑定与隐式绑定

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); // 3
console.log(obj2.a);// 3

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

new绑定 > 隐式绑定

三、new绑定与显式绑定

new和call/apply没法一块儿使用,所以没法经过new foo.call(obj1) 来直接测试,但咱们可使用硬绑定来测试

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

var baz = new bar(3);
console.log(obj1.a);// 2
console.log(bar.a);// 3
复制代码

这里bar被硬绑定在了obj1上,但new bar(3)并无把obj1.a修改成3。相反,new修改了硬绑定(到obj1的)调用bar()中的this。由于使用了new绑定,咱们获得了一个名为baz的新对象,而且baz.a的值为3 new绑定 > 硬绑定(显式绑定)

四、判断this

(1)、由new调用? 绑定到新建立的对象(new绑定)

var bar = new foo();
复制代码

(2)、由call或apply或bind调用?绑定到指定对象(显式绑定)

var bar = foo.call(obj2);
复制代码

(3)、由上下文对象调用?绑定到那个上下文对象(隐式绑定)

var bar = obj1.foo();
复制代码

(4)、默认绑定:严格模式下绑定到undefined,不然为全局对象

var bar = foo();
复制代码

4、绑定例外

一、被忽略的this

若是你把null货undefined做为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际应用默认绑定规则:

function foo() {
    console.log(this.a);
}
var a = 2;
foo.call(null); // 2
复制代码
function foo(a, b) {
    console.log("a:"+ a + ", b:" + b);
}
foo.apply(null, [2, 3]);// a:2, b:3
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
复制代码

老是用null来忽略this绑定可能会产生一些反作用。若是某个函数使用了this(如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(浏览器中为window),这会致使不可预计的后果(如修改全局对象),或者致使更多难以分析和追踪的bug

更安全的this

一种更安全的作法是传入一个特殊对象,把this绑定到这个对象不会对你的程序产生任何反作用。

可建立一个"DMZ"非军事区对象,一个空的非委托的对象,任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响

function foo(a, b) {
    console.log("a:"+ a + ", b:" + b);
}
// 咱们的DMZ空对象
var __null = Object.create(null);
foo.apply(__null, [2, 3]);// a:2, b:3
var bar = foo.bind(__null, 2);
bar(3); // a:2, b:3
复制代码

二、间接引用

间接引用的状况下,调用这个函数会应用默认绑定规则,而且最容易在赋值时发生:

function foo(a, b) {
    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(),这里会使用默认绑定

对于默认绑定来讲,决定this绑定对象的并非调用位置是否处于严格模式,而是函数体是否处于严格模式

三、软绑定

给默认绑定指定一个全局对象和undefined之外的值,可实现和硬绑定相同的效果,同时保留隐式绑定或显式绑定修改this的能力

if(!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获全部curried参数
        var curried = [].slice.call(arguments, 1);
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                    obj : this,
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    }
}
复制代码
function foo(a, b) {
    console.log("name: " + this.name);
}
var obj = { name: 'obj' },
    obj2 = { name: 'obj2' },
    obj3 = { name: 'obj3' };
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj

obj2.foo = softBind(obj);
obj2.foo(); // name: obj2

fooOBJ.call(obj3); // name: obj3

setTimeout(obj2.foo, 100); // name: obj 使用了软绑定
复制代码

从上述代码中能够看到软绑定版本的foo()能够手动将this绑定到obj2或obj3上,但若是应用默认绑定,则会将this绑定到obj

5、箭头函数

箭头函数不使用this的四种标准规则,而是根据外层(函数或全局)做用域来决定this

function foo() {
    // 返回一个箭头函数
    return (a) => {
        // this继承自foo();
        console.log(this.a);
    };
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);// 2, 不是3
复制代码

foo()内部建立的箭头函数会捕获调用时foo()的this。因为foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定没法被修改(new也不行)

function foo() {
    var self = this; 
    setTimeout(function(){
        console.log(self.a);
    }, 100);
}
var obj = {
    a: 2
};
foo.call(obj);// 2
复制代码

self=this与箭头函数均可以取代bind,但本质上是替代了this机制

常常编写this风格代码,但绝大部分时候会使用self=this或箭头函数来否认this机制,应当注意如下两点:

a、只是用词法做用域并彻底抛弃错误this风格的代码

b、彻底采用this风格,在必要时使用bind(),尽可能避免使用self=this和箭头函数

两种风格混用一般会使代码更难维护,而且可能也会更难编写

相关文章
相关标签/搜索