之前一直不知道this到底代表啥,只知道它和一般面向对象编程语言中的this一定不同。

JavaScript中的this可以在函数中使用,是编译器通过一些条件在函数被调用时绑定进对应作用域的的一个变量,可以明确知道的是,这个变量一定是一个对象,所以你可以用this.xxx的方式访问一个属性。

可以看到this被自动绑定进了作用域中,所以我们可以把它看成普通变量一样对待即可,没啥大不了的。

你不知道的JavaScript——this全面解析_显式

你不知道的JavaScript——this全面解析_优先级_02

图中的this指向了Window对象,也就是浏览器环境下的全局作用域,实际上this可以指向任意位置,具体的规则可以分四种情况。

默认绑定

默认情况下,this指向全局作用域。

function foo(){
    console.log(this.a);
}

var a = 10;
foo(); // 10

在浏览器执行这段代码,可以得到10的结果。

但是,在NodeJS环境下,你会得到一个undefined,这个理论失效了......难道是Node下的this和浏览器上的还不一样??那xx还得学两遍?

NodeJS和浏览器的不同

别太担心,只是NodeJS下的全局作用域和浏览器的有些区别。

你不知道的JavaScript——this全面解析_作用域_03

如上是浏览器的全局作用域,Window对象直接作为所有js文件的全局作用域,所以每个js文件中使用var定义变量时,是直接定义在Window下的,这样容易造成命名冲突。

你不知道的JavaScript——this全面解析_作用域_04

如上是NodeJs的做法,它使用了一个global作为全局作用域,为所有文件通用,但每个js文件又有一个单独的作用域,它们使用var定义的东西会定义在这个文件级别的单独作用域中,这样就不会出命名冲突的问题了。

回过头来看代码

function foo(){
    console.log(this.a);
}

var a = 10;
foo(); // undefined

咱说了,默认情况下this被绑定到全局作用域,那就是global,而a现在在哪儿啊?在文件单独的作用域里,那怎么找啊?

function foo(){
    console.log(this.a);
}

global.a = 10;
foo(); // 10

这样是可以的,但如非必要,请不要影响global作用域。

嘶,,你说不要影响全局作用域,那...你使用this不很容易影响全局作用域吗???

对了,注意,严格模式下这个默认行为会被禁止,也就是说,严格模式下,全局变量不会被绑定给this,默认情况下的this是undefined

隐式绑定

隐式绑定出现在调用位置有上下文对象的情况,this指向上下文对象。

function foo(){
    console.log(this.a);
}

var obj = {
    a: 10,
    foo: foo
}

var a = 20; // Turn to a comment in node env
// global.a = 20 // Release in node env

foo(); // 20
obj.foo(); // 10

第一个foo调用,按照刚刚的默认绑定,毫无疑问会寻找全局作用域中的20打印,但第二个obj.foo(),由于我们调用的位置是在obj的上下文中,所以this指向obj,打印10

这说明,隐式绑定优先级 > 默认绑定,这显然是句废话,但先记着。

看下面的代码

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

这应该不难理解,因为调用动作实际是obj2发出的,上下文理应是obj2,所以this也是obj2

再看

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

这...打印的结果是全局作用域中的a,而不是obj中的。

因为var bar = obj.foo,这句,只是把foo函数赋值给bar了,这和你直接调用foo又有啥区别捏??嘿嘿。

再看

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    fn();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";
doFoo( obj.foo ); // "oops, global"

这个和上面的原理一样,传参本就是一种隐式赋值。这些都是日常编写代码时容易犯的错误。

所以,这些问题让我感觉默认绑定和隐式绑定是如此的不可靠,一丁点儿的不小心就可能会酿成大错。

显式绑定

JS中的函数对象有两个默认方法,applycall,实际上都是调用这个函数,只是它俩可以在调用的同时传递要绑定的this对象。(这和Java里的invoke需要传递一个上下文实例不差不多吗)。

function foo(){
    console.log(this.a);
}

var obj = {
    a: 2
};

foo.call(obj);

上面我们就把对象obj绑定给本次调用的this了。

前面说了,this一定是个对象类型,所以你这里如果传递基本类型,则会被转换成包装类型。

但这好像仍然解决不了前面方法一被赋值this就会被改变的问题,你不能指望你的用户每次调用你的方法时都使用call或者apply并且自己绑定对象。

一个最简单的办法是提供一个函数帮助用户去完成调用call,绑定this的操作

function bar(){
    foo.call(obj)
}

bar();

bar暴露给用户,无论它的作用域是由于疏忽的赋值操作被修改还是被显式的恶意修改了,都不会影响实际内层foo所绑定的this对象。

可以考虑提供一个通用的方法来做这步操作,而不是把每一个函数都包装一层。

function bind(fn,obj){
    return function(){
        return fn.apply(obj,arguments);
    }
}

function sayHello(name){
    console.log(this.prefix + ',' + name);
}

var speaker = {
    prefix: 'Hello',
}

speaker.sayHello = bind(sayHello,speaker);

speaker.sayHello('Julia'); // Hello,Julia

bind方法用于提供将一个对象obj绑定给函数fn的能力,它返回一个新的方法,这个方法和我们之前的包装方法无异。

这样写代码会更优雅一点,以后我们的每个方法都可以直接使用bind来绑定this,而不用单独提供一个包装方法。

ES5提供了默认的bind方法。

function sayHello(name){
    console.log(this.prefix + ',' + name);
}
var speaker = {
    prefix: 'Hello',
}

speaker.sayHello = sayHello.bind(speaker);
speaker.sayHello('Julia'); // Hello,Julia

效果和我们所写的一样,但更加符合面向对象的编程模式。

使用callapply来绑定的this的优先级毫无疑问要高于隐式绑定和默认绑定。

改写我们之前的代码

function sayHello(name){
    console.log(this.prefix + ',' + name);
}
var speaker = {
    prefix: 'Hello',
    sayHello: sayHello
}
var anotherSpeaker = {
    prefix: 'Hi',
}

speaker.sayHello('Julia'); // Hello,Julia
speaker.sayHello.call(anotherSpeaker,'Julia'); // Hi,Julia

正常调用speaker.sayHello使用的就是当前speaker的上下文对象,这是隐式调用。而当你使用callapply给它绑定一个其他的对象时,this对象会被绑定成你指定的对象。

这也不用故意去记,这是很自然的事。

截至目前,优先级排序为:默认绑定 < 隐式绑定 < 显式绑定

new绑定

JS中的new和其他语言的不同。

new后面可以跟一个函数,然后会自动执行这个函数,并在执行之前做几件事情:

  1. 创建一个新对象
  2. 这个新对象被执行[[原型]]链接 (先不管他是啥)
  3. 这个新对象会被绑定到后面所跟的函数的this
  4. 如果函数没返回其他对象,这个新对象会被默认返回
function foo(a){
    this.a = a;
}

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

嘿,即使js中new的执行原理看似和传统面向对象语言并不搭边儿,但出来的效果还真和构造函数挺像呢哈。准确点来说应该是构造调用。

因为new创建了一个新对象,所有操作在这个新对象上进行,所以它的优先级理应是最高的,注意这里说它优先级最高的意思是它不会被其他绑定操作所影响,因为这个新对象和之前的老对象已经完全没关系了。

最终的优先级排序为:默认绑定 < 隐式绑定 < 显式绑定 < new绑定

软绑定

默认绑定的行为在严格模式和非严格模式下是不确定的,所以一般情况下应该避免,但如果使用bind来绑定,那么我们将不能再修改函数的this指向。

软绑定解决的就是这个问题,它在函数执行默认绑定时,将this指向一个设定好保底对象上,而不是全局对象,而当函数执行其他绑定时,将执行之前正常的绑定行为。

if(!Function.prototype.softBind){
    // 为函数对象的原型链上添加softBind方法
    Function.prototype.softBind=function(obj){
        var fn=this;
        var args=Array.prototype.slice.call(arguments,1);
        var bound=function(){
            return fn.apply(
                //如果this为空或者被绑定到全局对象,绑定到用户设定好的保底对象上,否则执行正常绑定
                (!this||this===(window||global))?obj:this,
                args.concat.apply(args,arguments)
            );
        };
        bound.prototype=Object.create(fn.prototype);
        return bound;
    };
}

咦,,,NodeJS的种种限制,上面这段经过深思熟虑的各种判断的代码并不能直接运行在Node环境。。。。

下面是softBind的使用示例

function foo(){
    console.log("name: "+);
}

var obj1={name:"obj1"},
    obj2={name:"obj2"},
    obj3={name:"obj3"};

var fooOBJ=foo.softBind(obj1);
fooOBJ();//"name: obj1" 

obj2.foo=foo.softBind(obj1);
obj2.foo();//"name: obj2"

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

setTimeout(obj2.foo,1000);//"name: obj1"

第一个调用,因为fooOBJ调用时是默认绑定,所以将默认的obj1绑定到this上。

第二个调用,因为obj2.foo调用时是隐式绑定,所以obj2被绑定到this上。

第三个调用,显式绑定,obj3被绑定在this上。

第四个,因为传参了,有一个隐式的赋值操作,这就会造成调用时实际上this被绑定成全局对象,所以理应软绑定的默认对象发挥作用,被绑定到this上的是obj1

如果在Node下使用setTimeout,会有一个默认的上下文叫Timeout,不会被直接绑定到全局对象,但这个Timeout里又没有name属性,所以输出应该是undefined。坑死。

md我可能真理解不了JS的设计哲学,怎么这么乱套啊....

箭头表达式中的this

箭头表达式继承外部this。

function foo(){
    setTimeout(()=>{
        console.log();
    },1000);
}
foo.bind({name: 'JJJJ'})() // JJJJ

未完...