this

2018-11-28  本文已影响0人  Bai_白

参考《你不知道的JavaScript上卷》中的this部分
参考文章 关于JavaScript中this的软绑定

1. JavaScript中的this

1.1 关于this的误解
  1. this是指向函数自身的
function foo(num) {
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++) {
    if(i > 5) {
        foo(i);
    }
}
//foo; 6
//foo; 7
//foo; 8
//foo; 9
console.log(foo.count); //0
console.log(count); //NaN

       执行foo.count=0时,的确向函数对象foo添加了一个属性count,但由于foo函数内部代码中的this不是指向函数自身的,所以this.count不等于foo.count,虽然函数名相同,根对象并不相同。而函数内部的this.count会创建一个全局变量,且值为NaN。

  1. this指向函数的作用域
function foo(){
    var a=2;
    this.bar();
}
function bar(){
    console.log(this.a);
}

foo(); //undefined

       bar()无法访问foo()作用域里的变量a,使用this不可能在词法作用域中查到什么。

总结:
1.this既不指向函数自身也不指向函数的词法作用域;
2.this是在函数被调用时发生的绑定,它的绑定和函数声明的位置没有关系,它指向什么完全取决于函数在哪里被调用。


1.2 调用位置

       调用位置就是函数在代码中被调用的位置,而不是声明的位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就是当前正在执行的函数的前一个调用中。

function first(){
    //当前调用栈是:first
    //当前调用位置是全局作用域
    
    second();
}

function second(){
    //当前调用栈是:first -> second
    //当前调用位置是first
}

first();

1.3 绑定规则
  1. 默认绑定
           独立函数调用。(可把这条规则看作是无法应用其他规则时的默认规则)
           在foo()函数内部中,如果使用严格模式,则默认绑定不能绑定到全局对象,而绑定到undefined。
           在严格模式下调用foo()则不影响绑定。
//在foo()函数内部中,如果使用严格模式
function foo(){
    "use strict";
    
    console.log(this.a);
}
var a=2;
foo(); //TypeError: Cannot read property 'a' of undefined
//在严格模式下调用foo()
function foo(){
    console.log(this.a);
}
var a=2;
(function(){
    "use strict";
    foo(); //2
})(); //将脚本文件包裹在一个自执行的匿名函数中,这样相当于所有语句都在一个严格模式的函数中执行
  1. 隐式绑定
           当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。(在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象中)
           对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo(){
    console.log(this.a); //因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
}
var obj = {
    a:2,
    foo:foo
};

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

obj1.obj2.foo(); //42
//bar是obj.foo的一个引用,但是它引用的只是foo函数本身,即只是把foo函数内容赋值给bar。
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
};
var bar=obj.foo;
var a="global";
bar(); //global
//回调函数
//参数传递其实就是一种隐式赋值,即fn = obj.foo,同理只是把foo函数内容赋值给fn
function foo(){
    console.log(this.a);
}
function doFoo(fn){
    fn();
}
var obj = {
    a:2,
    foo:foo
};
var a="global";
doFoo(obj.foo); //global
//把函数传入语言内置的函数,结果一样
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
};
var a="global";
setTimeout(obj.foo, 100); //global
  1. 显式绑定
           使用call()和apply()方法:第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
foo.call(obj); //2
//通过foo.call(),可以在调用foo时强制把它的this绑定到obj上。
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var bar=function(){
    foo.call(obj); //强制把foo的this绑定到了obj
}

bar(); //2
setTimeout(bar, 100); //2

bar.call(window); //2,没办法修改this

硬绑定的主要使用方法是:
1.创建一个包裹函数,负责接收参数并返回值;
2.创建一个可以重复使用的辅助函数
       由于硬绑定是一种常用的模式,所以ES5提供了内置的方法bind(),会把指定的参数设置为this的上下文并调用函数使用指定的this。

  1. new绑定
           使用new来调用函数,会自动执行下面的操作:
    1.创建一个全新的对象。
    2.这个新对象会被执行[[Prototype]]连接。
    3.这个新对象会绑定到函数调用的this。
    4.如果函数没有返回其他对象,则自动返回这个新对象。
function foo(num){
    this.a=num;
}
var bar=new foo(2); //这里会构造一个新对象并把它绑定到foo()调用中的this上
console.log(bar.a); //2

如果函数里面返回一个对象,则this会绑定到返回的这个对象中。若返回一个非对象类型数据,则不会出现这样的情况,this仍会绑定到新对象。

function foo(num){
    this.a=num;
    return {a:200};
}
var bar=new foo(2); 
console.log(bar.a);  //200

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

function foo(p1,p2,p3){
    this.val=p1+p2+p3;
}
var bar=foo.bind(null, "p1"); //使用null是因为无需关心硬绑定的this是什么,因为使用new时this会被修改
var baz=new bar("p2","p3"); //通过new把this绑定到baz上
console.log(baz.val); //"p1p2p3"

1.4 绑定例外
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

       但总是使用null来忽略this绑定可能会产生一些副作用,因此更安全的做法是传入一个特殊的对象,比如创建一个DMZ对象(即一个空的非委托的对象)。
创建一个空对象最简单的方法是Object.create(null)
       Object.create(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

       从1.3的隐式丢失问题中可以了解到,赋值表达式p.foo=o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),因此会应用默认绑定。

if(!Function.prototype.softBind){
    Function.prototype.softBind = function(obj){
        var fn=this;
        
        var args=[].slice.call(arguments, 1);
        var bound=function(){
            return fn.apply((!this||this===(window||global))?obj:this, args.concat.apply(args,arguments));
        };
        bound.prototype=Object.create(fn.prototype);
        return bound;
    };
}


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

var obj={name: "obj"};
var obj2={name: "obj2"};
var obj3={name: "obj3"};

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

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

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

setTimeout(obj2.foo, 10); //name: obj
  1. 首先先看[].slice.call(arguments, 1)这个写法。因为arguments是一个对象,不是真正的数组对象,只是与数组类似,所以不具有slice()方法。所以这条语句的过程就是先将传入进来的第一个参数转为数组,再调用slice()方法(即通过call显式绑定来实现arguments变相有slice()方法)。
  2. 再看(! this || this === (window || global)) ? obj : this,这条语句的作用是判断函数的调用位置,即它的this指向,将this绑定到现在正在指向的函数(即隐式绑定或显式绑定)。
           对于fooOBJ();,调用时此处的this绑定到全局对象,则将this绑定到obj中,所以输出的结果是name: obj
           对于obj2.foo();,调用时此处的this绑定到obj2中,所以this不变,输出name: obj2,此处相当于隐式绑定。
           同理,对于fooOBJ.call(obj3);,调用时此处的this绑定到obj3,相当于显式绑定。
           对于setTimeout(obj2.foo, 10);,由1.3的隐式丢失问题中可以了解到回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环境上,但有软绑定,所以这里this还是指向obj。
  3. 可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
  4. 此代码还支持柯里化。apply()传入的第二个参数args.concat.apply(args,arguments)就是运行 foo 所需要的参数,由上面的args(外部参数)和内部的arguments(内部参数)连接成。下面代码就是实现柯里化的例子。
function add(a,b){
    return this.num+a+b;
}
var obj={num: 1};
var func=add.softBind(obj, 2);
func(3); //6

2. ES6中的this

       ES6的箭头函数无法使用之前所说的四条规则,它是根据外层(函数或者全局)作用域来决定this。具体来说,箭头函数会继承外层函数调用的this绑定。

function foo(){
    return (a)=>{
        console.log(this.a);
    };
}
var obj1={a:1};
var obj2={a:2};

var bar=foo.call(obj1);
bar.call(obj2); //1

       foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,且箭头函数的绑定无法被修改,new也不行,所以bar.call(obj2);语句并没有改变bar的this绑定。

       箭头函数最常用于回调函数,例如事件处理器或者定时器,下面是不使用箭头函数和使用箭头函数的结果。

var a=200;
function foo(){
    setTimeout(function display(){
        console.log(this.a); //回调函数丢失this绑定
    }, 100);
}
var obj={a:2};
foo.call(obj); //200
var a=200;
function foo(){
    setTimeout(()=>{
        console.log(this.a); //this在词法上继承自foo(),foo()的this指向obj。
    }, 100);
}
var obj={a:2};
foo.call(obj); //2
上一篇下一篇

猜你喜欢

热点阅读