更改this指向

更改this指向
功能:在另一个函数中保持this上下文

为了保持函数的上下文,我最开始是这样做的:

var obj = {
    init: function(){
        this.fetchData();
    },
    fetchData: function(){
    	var that = this;
        $.ajax({
            url: 'http://p0.ifengimg.com/a/2017_27/131febdcdb0db9e.jpg',
            data: {},
            method: 'GET',
            success: function(){
                that.render();
            }
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

使用that来保存上下文。
有没有其他更好的方法呢?
1.Function.prototype.bind

var obj = {
    init: function(){
        this.fetchData();
    },
    fetchData: function(){
        $.ajax({
            url: 'http://p0.ifengimg.com/a/2017_27/131febdcdb0db9e.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render();
            }.bind(this)
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

Function.prototype.bind的原理是什么呢?
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值
bind方法实现伪代码:

Function.prototype.bindScope = function(scope){
	var target = this;
	return function(){
		return target.call(scope); // line1
	};
};
var obj = {
    init: function(){
        this.fetchData();
    },
    fetchData: function(){
        $.ajax({
            url: 'http://p0.ifengimg.com/a/2017_27/131febdcdb0db9e.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render();
            }.bindScope(this)
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

输出:render
我的疑惑在line1:

target.call(scope);

this.call(scope);this为什么能调用call方法?
再看一下上面代码,我们改造一下:

var obj = {
    init: function(){
        console.log(this === obj);
    }
};
obj.init();

输出为TRUE
其实this是在执行时创建的,JavaScript中this指向函数执行时的当前对象。
bind方法也能接受形参,下面我们继续改造一下:

Function.prototype.bindScope = function(scope){
	var target = this;
	if(typeof this != 'function'){
		throw new TypeError('只支持bind函数');
	}
	var args = [].slice.call(arguments, 1);
	return function(){
		// 需要合并两个函数的参数
		return target.apply(scope, [].concat.call(args, [].slice.call(arguments))); // line1
	};
};
var obj = {
    init: function(){
        this.fetchData();
    },
    fetchData: function(){
        $.ajax({
            url: 'http://p0.ifengimg.com/a/2017_27/131febdcdb0db9e.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render('getsuccess', arguments);
            }.bindScope(this, 'get', 'img')
        });
    },
    render: function(){
        console.log('render', arguments); // render (2) ["getsuccess", Arguments(5), callee: function, Symbol(Symbol.iterator): function]
    }
};
obj.init();

更严谨的写法如下:

if (!Function.prototype.binds) {
  Function.prototype.binds = function (ctx) {
    if (typeof this !== 'function') {
      throw new TypeError("NOT_A_FUNCTION -- this is not callable")
    }
    var _this = this
    var slice = Array.prototype.slice
    var formerArgs = slice.call(arguments, 1)
    var fun = function () {}
    var fBound = function (){
      let laterArgs = slice.call(arguments, 0)
      // 若通过 new 调用 bind() 之后的函数,则这时候 fBound 的 this 指向的是 fBound 实例,
      // 而下面又定义了 fBound 是 fun 的派生类(其 prototype 指向 fun 的实例),
      // 所以 this instanceof fun === true ,这时 this 指向了 fBound 实例,不另外绑定!
      return _this.apply(this instanceof fun ? this : ctx || this, formerArgs.concat(laterArgs))
    }
    fun.prototype = _this.prototype
    fBound.prototype = new fun()
    return fBound
  }
}

2.call方法

var obj = {
    init: function(){
        this.name = 'obj';
        this.fetchData();
    },
    fetchData: function(){
        $.ajax({
            url: 'http://p3.ifengimg.com/a/2017_28/6c21ba887842358.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render();
            }.call(this)
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

输出render
3.apply方法

var obj = {
    init: function(){
        this.name = 'obj';
        this.fetchData();
    },
    fetchData: function(){
        $.ajax({
            url: 'http://p3.ifengimg.com/a/2017_28/6c21ba887842358.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render();
            }.apply(this)
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

输出render
3种方法的区别?

var base = {
    init: function(){
        console.log(arguments[0]);
    }
};
var obj = {
    init: function(){
        this.fetchData();
    },
    fetchData: function(){
        base.init.bind(this, 'bind');
        base.init.call(this, 'call');
        base.init.apply(this, ['apply']);
        $.ajax({
            url: 'http://p0.ifengimg.com/a/2017_27/131febdcdb0db9e.jpg',
            data: {},
            method: 'GET',
            success: function(){
                this.render();
            }.bind(this)
        });
    },
    render: function(){
        console.log('render');
    }
};
obj.init();

上述代码输出 call apply render
bind代码没有执行。
总结:
共同点:
都可以改变函数执行的上下文环境;
不同点:
bind: 不立即执行函数,一般用在异步调用和事件; call/apply: 立即执行函数。call和apply参数形式不同
下面用几个代码来加深理解吧
(1),,第一个demo

function a() {
    console.log(1)
}
function b() {
    console.log(2)
}
a.call(b);
a.call.call(b);
a.call.call.call(b); 
a.call.call.call.call.call.call.call.call.call(b);

自解:
a.call.call(b) –> a.call(b);
a.call.call.call(b); –> a.call.call(b) –> a.call(b);
所以上述代码我得出的是依次打印1 1 1 1
析:
本题考查this的指向以及对Function.prototype.call的理解
其实call方法伪代码类似于如下:

Function.prototype.callScope = function(scope){
	...
	// 执行this,且this函数中的this指向scope
	this();
};
var fn1 = function(){
	console.log(this);
};
var fn2 = function(){
	console.log('b');
}
fn1.callScope(fn2);

a.call.call(b)
step1,拆解,(a.call).call(b),a.call对象的this指向b。
step2,a.call对象的this执行,即a.call(),
step3,a.call–>this(),this指向b所以执行b(),得2

总结:
Function.prototype.bind.call(obj, arg)
其实就相当于 obj.bind(arg) 。

Function.prototype.call.call(obj, arg)
其实就相当于 obj.call(arg) 。
同理,a.call.call.call(b)得2;a.call.call.call.call.call.call.call.call.call(b)得2.
(2),第二个demo

var a = Function.prototype.call.apply(function(a) {
    return a;
}, [0, 4, 3]);
alert(a);

自解:

Function.prototype.call.apply(function(a) {
    return a;
}, [0, 4, 3]);
<=>

var fn = Function.prototype.call;
var fn1 = function(a) {
    return a;
};
var arr = [0, 4, 3];
fn.apply(fn1, arr);

<=>
(fn1.)fn(0,4,3);
Function.prototype.call中this由Function.prototype指向fn1
相当于fn1.call(0,4,3)
所以fn1的this指向0,参数是4和3.
所以a为4

参考文章:
javascript 原生 bind() 的 ES6/ES5 实现~ ES7 小彩蛋
Function.prototype.bind的内部实现
理解Function.prototype.bind()方法

此条目发表在JavaScript分类目录。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注