this的默认绑定规则总共与下面五种:
默认绑定(严格/非严格)
隐式绑定
显示绑定
new绑定
箭头函数绑定
调用位置 什么是调用位置?
调用位置: 函数在代码中被调用的位置(而不是函数声明的位置)
查找方法:
分析调用栈: 调用位置就是当前正在执行的函数的前一个调用 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function a ( ) { console .log('a' ) b() } function b ( ) { console .log('b' ) c() } function c ( ) { console .log('c' ) } a();
使用开发者工具 设置断点,展示当前位置的函数调用列表(call stack)即调用栈 ,找到栈中的第二个元素 ,这就是真正的调用位置。
绑定规则 默认绑定
独立函数调用 ,可以把默认绑定看做是无法运用其他规则时的默认规则,this指向全局对象
严格模式 下,不能将全局对象用于默认绑定,this会绑定到undefined。但是在严格模式下进行函数调用 不影响this默认绑定,即this可以绑定到全局对象。只有在非严格模式下,this才能绑定到全局对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function ex ( ) { "use strict" ; console .log(this .a); } var a = 2 ;ex(); function ex ( ) { console .log(this .a); } var a = 2 ;(function ( ) { "use strict" ; ex(); })()
隐式绑定 当函数引用有上下文时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链只有上一层或者说是最后一层在调用中起作用。
1 2 3 4 5 6 7 8 9 10 function ex ( ) { console .log(this .a); } var obj = { a: 2 , ex: ex }; obj.ex();
隐式丢失 被隐式绑定的函数在特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function ex ( ) { console .log(this .a); } var obj = { a: 2 , ex: ex } var exT = obj.ex;var a = "this global" ; exT();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function ex ( ) { console .log(this .a); } function doEx (fn ) { fn(); } var obj = { a: 1 , ex: ex }; var a = "this global" ;doEx(obj.ex); function setTimeout (fn, delay ) { fn(); }
显示绑定 通过call(···)或者apply(···)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this上。因为直接指定this的绑定对象,称之为显示绑定。
1 2 3 4 5 6 7 8 9 function ex ( ) { console .log(this .a); } var obj = { a: 2 }; ex.call(obj);
显示绑定无法解决丢失绑定的问题
解决方案:
硬绑定 创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function foo ( ) { console .log( this .a ); } var obj = { a: 2 }; var bar = function ( ) { foo.call( obj ); }; bar(); setTimeout( bar, 100 ); bar.call( window );
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); console .log( b );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ); console .log( b );
ES5内置了Function.prototype.bind,bind会返回一个硬绑定的新函数,用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 ); console .log( b );
API调用的“上下文” JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)一样,确保回调函数使用指定的this。这些函数实际上通过call(..)和apply(..)实现了显式绑定。
1 2 3 4 5 6 7 8 9 10 11 12 function foo (el ) { console .log( el, this .id ); } var obj = { id: "awesome" } var myArray = [1 , 2 , 3 ]myArray.forEach( foo, obj );
new 绑定
在JS中,构造函数只是使用new操作符时被调用的普通函数,他们不属于某个类,也不会实例化一个类。
包括内置对象函数(比如Number(..))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1、创建(或者说构造)一个新对象。
2、这个新对象会被执行[[Prototype]]连接。
3、这个新对象会绑定到函数调用的this。
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
使用new来调用foo(..)时,会构造一个新对象并把它(bar)绑定到foo(..)调用中的this。
1 2 3 4 5 6 function foo (a ) { this .a = a; } var bar = new foo(2 ); console .log( bar.a );
手写一个new实现 1 2 3 4 5 6 7 8 9 10 11 12 function create ( ) { var obj = new Object (), Con = [].shift.call(arguments ); obj.__proto__ = Con.prototype; var ret = Con.apply(obj, arguments ); return ret instanceof Object ? ret : obj; };
使用这个手写的new
1 2 3 4 5 6 7 function Person ( ) {...}var person = new Person(...) var person = create(Person, ...)
代码原理解析:
1、用new Object()的方式新建了一个对象obj
2、取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments会被去除第一个参数
3、将 obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
4、使用apply,改变构造函数this的指向到新建的对象,这样 obj就可以访问到构造函数中的属性
5、返回 obj
箭头函数绑定 ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this。
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function foo ( ) { return (a ) => { console .log( this .a ); }; } var obj1 = { a: 2 }; var obj2 = { a: 3 } var bar = foo.call( obj1 );bar.call( obj2 );
ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。
1 2 3 4 5 6 7 8 9 10 11 12 function foo ( ) { var self = this ; setTimeout( function ( ) { console .log( self.a ); }, 100 ); } var obj = { a: 2 }; foo.call(obj);
代码风格统一问题:如果既有this风格的代码,还会使用 seft = this 或者箭头函数来否定this机制。
只使用词法作用域并完全抛弃错误this风格的代码;
完全采用this风格,在必要时使用bind(..),尽量避免使用 self = this 和箭头函数
其实大部分情况下可以用一句话来概括,this总是指向调用该函数的对象 。
但是对于箭头函数并不是这样,是根据外层(函数或者全局)作用域(词法作用域 )来决定this。
对于箭头函数的this总结如下:
箭头函数不绑定this,箭头函数中的this相当于普通变量。
箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。
箭头函数的this无法通过bind,call,apply来直接修改(可以间接修改)。
改变作用域中this的指向可以改变箭头函数的this。
eg. function closure(){()=>{//code }},在此例中,我们通过改变封包环境closure.bind(another)(),来改变箭头函数this的指向。
优先级 1 2 3 4 5 6 7 8 9 10 11 new 绑定:this 绑定新创建的对象,var bar = new foo() 显示绑定this :绑定指定的对象, var bar = foo.call(obj2) 隐式绑定:this 绑定上下文对象, var bar = obj1.foo() 默认绑定:函数体严格模式下绑定到undefined ,否则绑定到全局对象。 var bar = foo()
在new中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数(柯里化)。
1 2 3 4 5 6 7 8 9 10 11 function foo (p1, p2 ) { this .val = p1 + p2; } var bar = foo.bind( null , "p1" );var baz = new bar( "p2" );baz.val;
绑定例外 被忽略的this 把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认规则。
下面两种情况下会传入null
使用apply(..)来“展开”一个数组,并当作参数传入一个函数
bind(..)可以对参数进行柯里化(预先设置一些参数)
1 2 3 4 5 6 7 8 9 10 function foo (a, b ) { console .log( "a:" + a + ",b:" + b ); } foo.apply( null , [2 , 3 ] ); var bar = foo.bind( null , 2 );bar( 3 );
总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
更安全的this 安全的做法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何副作用。
JS中创建一个空对象最简单的方法是Object.create(null) ,这个和{}很像,但是并不会创建Object.prototype这个委托,所以比{}更空。
1 2 3 4 5 6 7 8 9 10 11 12 13 function foo (a, b ) { console .log( "a:" + a + ",b:" + b ); } var ø = Object .create( null );foo.apply( ø, [2 , 3 ] ); var bar = foo.bind( ø, 2 );bar( 3 );
间接引用 间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
1 2 3 4 5 6 7 8 9 10 11 function foo ( ) { console .log( this .a ); } var a = 2 ;var o = { a : 3 , foo : foo };var p = { a : 4 };o.foo(); (p.foo = o.foo)();
软绑定
硬绑定可以把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this 。
如果给默认绑定指定一个全局对象和undefined以外的值 ,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (!Function .prototype.softBind) { Function .prototype.softBind = function (obj ) { var fn = this ; 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function foo ( ) { console .log("name:" + this .name); } var obj = { name : "obj" }, obj2 = { name : "obj2" }, obj3 = { name : "obj3" }; var fooOBJ = foo.softBind( obj );fooOBJ(); obj2.foo = foo.softBind( obj ); obj2.foo(); fooOBJ.call( obj3 ); setTimeout( obj2.foo, 10 );
思考题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var num = 1 ;var myObject = { num: 2 , add: function ( ) { this .num = 3 ; (function ( ) { console .log(this .num); this .num = 4 ; })(); console .log(this .num); }, sub: function ( ) { console .log(this .num) } } myObject.add(); console .log(myObject.num);console .log(num);var sub = myObject.sub;sub();
答案有两种情况,分为严格模式和非严格模式。
严格模式下,报错。TypeError: Cannot read property ‘num’ of undefined
非严格模式下,输出:1、3、3、4、4 解答过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var num = 1 ;var myObject = { num: 2 , add: function ( ) { this .num = 3 ; (function ( ) { console .log(this .num); this .num = 4 ; })(); console .log(this .num); }, sub: function ( ) { console .log(this .num) } } myObject.add(); console .log(myObject.num); console .log(num); var sub = myObject.sub;sub();