前言
this的绑定规则总共有下面5种。
1、默认绑定(严格/非严格模式)
2、隐式绑定
3、显式绑定
4、new绑定
5、箭头函数绑定
this调用
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
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;语句,运行时调试器会在那个位置暂停,同时展示当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。
默认绑定
独立函数调用,可以把默认绑定看作是无法应用其他规则时的默认规则,this指向全局对象。
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。
function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";
console.log( this.a );
}
var a = 2;
// 调用
foo(); // TypeError: Cannot read property 'a' of undefined
// --------------------------------------
function foo() { // 运行
console.log( this.a );
}
var a = 2;
(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";
foo(); // 2
})();
上例中
(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";
foo(); // 2
})();
等效于
var foo= function(){...};//访问的是全局对象widows
foo();//结果为2
立即函数赋值为间接引用,指一个定义对象的方法引用另一个对象存在的方法,这种情况下会使得this指向window.
关于立即执行函数请查看:https://blog.csdn.net/qq_21653855/article/details/103000334
隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起作用。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); //2,根据上下文继承,foo()等于this.a,obj.foo()=obj.a=2
隐式丢失
被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
// 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"
引用赋值丢失(函数别名)
function thisTo(){
console.log(this.a);
}
var data={
a:2,
foo:thisTo //通过属性引用this所在函数
};
var a=3;//全局属性
var newData = data.foo; //这里进行了一次引用赋值
newData(); // 3
理解: newData实际上引用的是foo函数本身,这就相当于:var newData = thisTo;data对象只是一个中间桥梁,data.foo只起到传递函数的作用,所以newData跟data对象没有任何关系。而newData本身又不带a属性,最后a只能指向window。
传参丢失(回调函数)
function thisTo(){
console.log(this.a);
}
var data={
a:2,
foo:thisTo //通过属性引用this所在函数
};
var a=3;//全局属性
setTimeout(data.foo,100);// 3
所谓传参丢失,就是在将包含this的函数作为参数在函数中传递时,this指向改变。
setTimeout函数的本来写法应该是setTimeout(function(){…},100);100ms后执行的函数都在“…”中,可以将要执行函数定义成var fun = function(){…},即:setTimeout(fun,100),100ms后就有:fun();所以此时此刻是data.foo作为一个参数,是这样的:setTimeout(thisTo,100);100ms过后执行thisTo(),原理跟应用赋值差不多,没有调用thisTo的对象,this只能指向window。
显式绑定
通过call(…) 或者 apply(…)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。因为直接指定this的绑定对象,称之为显示绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上
显示绑定依旧无法解决隐式丢失
隐式丢失解决方法
硬绑定
为了解决隐式丢失(隐式丢失专用)的问题,ES5专门提供了bind方法,bind()会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。(这个bind可跟$(selector).bind(‘click’,function(){…})的用法不同),硬绑定可以把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this,被硬绑定的函数无法修改this指向。
function thisTo(){
console.log(this.a);
}
var data={
a:2
};
var a=3;
var bar=thisTo.bind(data);
console.log(bar()); //2
// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
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
API调用的“上下文”
JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(…)一样,确保回调函数使用指定的this。这些函数实际上通过call(…)和apply(…)实现了显式绑定。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
new绑定
在JS中,构造函数只是使用new操作符时被调用的普通函数,他们不属于某个类,也不会实例化一个类。
包括内置对象函数(比如Number(…))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1、创建(或者说构造)一个新对象。
2、这个新对象会被执行[[Prototype]]连接。
3、这个新对象会绑定到函数调用的this。
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
使用new来调用foo(…)时,会构造一个新对象并把它(bar)绑定到foo(…)调用中的this。
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
new方式是优先级最高的一种调用方式,只要是使用new方式来调用一个构造函数,this一定会指向new调用函数新创建的对象:
function() thisTo(a){
this.a=a;
}
var data=new thisTo(2); //在这里进行了new绑定
console.log(data.a); //2
手写一个new实现:
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
使用这个手写的new:
function Person() {...}
// 使用内置函数new
var person = new Person(...)
// 使用手写的new,即create
var person = create(Person, ...)
代码原理解析:
1、用new Object()的方式新建了一个对象obj
2、取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments会被去除第一个参数
3、将 obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
4、使用apply,改变构造函数this 的指向到新建的对象,这样 obj就可以访问到构造函数中的属性
5、返回 obj
在new中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数(柯里化)
function foo(p1, p2) {
this.val = p1 + p2;
}
// 之所以使用null是因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new时this会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
ES6箭头函数
ES6的箭头函数在this这块是一个特殊的改进,箭头函数使用了词法作用域取代了传统的this机制,所以箭头函数无法使用上面所说的这些this优先级的原则,注意的是在箭头函数中,根据外层(函数或者全局)作用域(词法作用域)来决定this.
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)。
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!
下面的例子不用箭头函数,发生this传参丢失,最后的this默认绑定到全局作用域,输出3。
function thisTo(){
setTimeout(function(){
console.log(this.a);
},100);
}
var obj={
a:2
}
var a=3;
thisTo.call(obj); //3
用了箭头函数,不会发生隐式丢失,this绑定到外层父作用域thisTo(),thisTo的被调用者是obj对象,所以最后的this到obj对象中,输出2。
function thisTo(){
setTimeout(()=>{
console.log(this.a);
},100);
}
var obj={
a:2
}
var a=3;
thisTo.call(obj); //2
如果不用箭头函数实现相同的输出,可以采用下面这种方式:
function thisTo(){
var self=this; //在当前作用域中捕获this
setTimeout(function(){
console.log(self.a); //传入self代替之前的this
},100);
}
var obj={
a:2
}
var a=3;
thisTo.call(obj); //2
代码风格统一问题:如果既有this风格的代码,还会使用 seft = this 或者箭头函数来否定this机制。
只使用词法作用域并完全抛弃错误this风格的代码;
完全采用this风格,在必要时使用bind(…),尽量避免使用 self = this 和箭头函数。
绑定例外
被忽略的this
把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认规则。
下面两种情况下会传入null
使用apply(…)来“展开”一个数组,并当作参数传入一个函数
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
总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
安全的做法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何副作用。
JS中创建一个空对象最简单的方法是Object.create(null),这个和{}很像,但是并不会创建Object.prototype这个委托,所以比{}更空。
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}
// 我们的空对象
var ø = Object.create( null );
// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3
软绑定
-
硬绑定可以把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。
-
如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改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;
};
}
使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
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