this是什么
当一个函数被调用时,会创立一个执行上下文。这个执行上下文会蕴含函数在哪里被调用(执行栈)、函数的调用形式、传入的参数等信息。this就是这个执行上下文的一个属性,会在函数执行的过程中用到。
调用地位
在了解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的调用地位
绑定规定
this
的绑定规定总共有5种:
- 默认绑定
- 隐式绑定
- 显示绑定
new
绑定- 箭头函数绑定
1.默认绑定
独立函数调用 :能够把这条规定看作是无奈利用其余规定时的默认规定。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
严格模式(strict mode),不能将全局对象用于默认绑定,this会绑定到undefined
。
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined
2.隐式绑定
当函数援用有上下文对象时,隐式绑定规定会把函数中的this
绑定到这个上下文对象。
function foo() {
console.log( this.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
隐式失落
被隐式绑定的函数特定状况下会失落绑定对象,它会利用默认绑定,把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 foo() {
console.log( this.a );
}
function doFoo(fn){
//fn其实援用的是foo
fn();// <-- 调用地位!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo(obj.foo); // "oops, global"
参数传递其实就是一种隐式赋值,咱们传入函数时也会被隐式赋值。
如果把函数传入语言内置的函数而不是传入本人申明的函数,后果是一样的。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout(obj.foo,100); // "oops, global"
// JS环境中内置的setTimeout()函数实现和上面的伪代码相似:
function setTimeout(fn, delay) {
// 期待delay毫秒
fn(); // <-- 调用地位!
}
3.显式绑定
能够应用函数的call(...)
和apply(...)
办法,它们的第一个参数是一个对象,是给this
筹备的,接着在调用函数时将其绑定到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 );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的bar不可能再批改它的this
bar.call( window ); // 2
咱们创立了函数bar()
,并在它的外部手动调用了foo.call(obj)
,因而强制把foo
的this
绑定到了obj
。
无论之后如何调用函数bar
,它总会手动在obj
上调用foo
。咱们称之为硬绑定。
典型利用场景是创立一个包裹函数,负责接管参数并返回值:
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
另一种应用办法是创立一个能够重复使用的辅助函数:
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 ); // 2 3
console.log( b ); // 5
ES5提供了内置的办法Function.prototype.bind
:
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 ); // 2 3
console.log( b ); // 5
bind(...)
会返回一个硬编码的新函数,它会把你指定的参数设置为this
的上下文并调用原始函数。
API调用的“上下文”
第三方库以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(...)
一样,确保你的回调函数应用指定的this
。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
let myArr = [1,2,3]
// 调用foo(..)时把this绑定到obj
myArr.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
这些函数实际上就是通过call(...)
或者apply(...)
实现了显式绑定。
4.new绑定
在Javascript中,构造函数只是一些应用new
操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。
包含内置对象函数(比方Number(...)
)在内的所有函数都能够用new
来调用,这种函数调用被称为结构函数调用。
实际上并不存在所谓的“构造函数”,只有对于函数的“结构调用”。
应用new
来调用函数,或者说产生结构函数调用时,会主动执行上面的操作。
- 创立(或者说结构)一个全新的对象。
- 这个新对象会被执行[[Prototype]]连贯。
- 这个新对象会绑定到函数调用的
this
。 - 如果函数没有返回其余对象,那么
new
表达式中的函数调用会主动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
应用new
来调用foo(...)
时,咱们会结构一个新对象并把它绑定到foo(...)
调用中的this
上。
new
是又一种能够影响函数调用时this
绑定行为的办法,咱们称之为new
绑定。
优先级
function foo1() {
console.log(this.a)
}
function foo2(something) {
this.a = something
}
var obj1 = {
a: 2,
foo: foo1
}
var obj2 = {
a: 3,
foo: foo1
}
var obj3 = {
foo: foo2
}
var obj4 = {}
obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
//可见,显式绑定比隐式绑定优先级高
obj3.foo(4);
console.log(obj3.a); //4
obj3.foo.call(obj4, 5);
console.log(obj4.a); //5
var bar = new obj3.foo(6);
console.log(obj3.a); //4
console.log(bar.a); //6
//可见,new绑定比隐式绑定优先级高
var qux = foo2.bind(obj4);
qux(7);
console.log(obj4.a); //7
var quux = new qux(8);
console.log(obj4.a); //7
console.log(quux.a); //8
//new绑定批改了硬绑定(到obj4的)调用qux(...)中的this。
当初,咱们能够依据优先级来判断函数在某个调用地位利用的是哪条规定。
- 函数是否在
new
中调用(new
绑定),如果是的话this
绑定的是新创建的对象。 - 函数是否通过
call
、apply
(显式绑定)或者硬绑定调用,如果是的话this
绑定的是指定的对象。 - 函数是否在某个上下文对象中调用(隐式绑定),如果是的话
this
绑定的就是那个上下文对象。 -
如果都不是的话,应用默认绑定。
- 如果在严格模式下,就绑定到
undefined
。 - 否则就绑定到全局对象。
- 如果在严格模式下,就绑定到
绑定例外
被疏忽的this
如果你把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被疏忽,理论利用的是默认规定。
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); //2
两种状况会传入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
一种“更平安”得做法是传入一个非凡的对象,把this
绑定到这个对象不会对你的程序产生任何副作用。
在Javascript中创立一个空对象最简略的办法是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
间接援用
另一个须要留神的是,你有可能创立一个函数的“间接援用”,调用这个函数会利用默认绑定规定。
间接援用最容易在赋值时产生:
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
赋值表达式p.foo = o.foo
的返回值是指标函数的援用,因而调用地位是foo()
而不是p.foo()
或者o.foo()
。
软绑定
硬绑定这种形式能够把this
强制绑定到指定的对象(除了应用new
时),避免函数调用利用默认绑定规定。
毛病是硬绑定会大大降低函数的灵活性,应用硬绑定之后就无奈应用隐式绑定或者显式绑定来批改this
。
如果能够给默认绑定指定一个全局对象和undefined以外的值,那就能够实现和硬绑定雷同的成果,同时保留隐式绑定或者显式绑定来批改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;
};
}
除了软绑定之外,softBind(...)
的其它原理和ES5内置的bind(...)
相似。
它会对指定的函数进行封装,首先查看调用时的this
,如果this
绑定到全局对象或者undefined
,那就把指定的默认对象obj
绑定到this
,否则不会批改this
。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
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
能够看到,软绑定版本的foo()
能够手动将this
绑定到obj2
或者obj3
上,但如果利用默认绑定,则会将this
绑定到obj
。
箭头函数
咱们之前介绍的四条规定能够蕴含所有失常的函数。然而ES6
中介绍了一种无奈应用这些规定的非凡函数类型:箭头函数。
箭头函数不应用this
的四种规范规定,而是依据外层(函数或者全局)作用域来决定this
。
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!
foo()
外部创立的箭头函数会捕捉调用时的foo()
的this
。因为foo()
的this
绑定到obj1
,bar
(援用箭头函数)的this
也会绑定到obj1
,箭头函数的绑定无奈被修该。(new
也不行!)
箭头函数罕用于回调函数中,例如事件处理器或者定时器:
function foo(){
setTimeout(()=>{
//这里的this在词法上继承自foo()
console.log(this.a);
},100);
}
var obj = {
a:2
};
foo.call(obj) //2
箭头函数中的this
1.箭头函数没有prototype
(原型),所以箭头函数自身没有this
。
let a = () => {}
console.log(a.prototype) //undefined
2.箭头函数中的this
是从定义它们的上下文继承的(Javascript权威指南第7版P206),继承自外层第一个一般函数的this
。
let foo
let barObj = {
msg: 'bar的this指向'
}
let bazObj = {
msg: 'baz的this指向'
}
bar.call(barObj) //bar的this指向barObj
baz.call(bazObj) //baz的this指向bazObj
function bar() {
foo = () => {
console.log(this, 'this指向定义它们的上下文,外层的第一个一般函数')
}
}
function baz() {
foo()
}
//msg: "bar的this指向" "this指向定义它们的上下文,外层的第一个一般函数"
3.箭头函数的this
无奈通过bind
,call
,apply
来间接批改。
let quxObj = {
msg: '尝试间接批改箭头函数的this指向'
}
function baz() {
foo.call(quxObj)
}
//{msg: "bar的this指向"} "this指向定义它们的上下文,外层的第一个一般函数"
间接批改箭头函数的指向:
bar.call(bazObj) //一般函数bar的this指向bazObj,外部的箭头函数也会指向bazObj
被继承的一般函数的this
指向扭转,箭头函数的this
指向也会跟着扭转。
4.如果箭头函数没有外层函数,this
指向window
var obj = {
i: 10,
b: () => console.log(this.i, this),
c: function() {
console.log( this.i, this)
}
}
obj.b()//undefined, window
obj.c()//10, {i: 10, b: ƒ, c: ƒ}
练习
/**
* Question 1
* 非严格模式下
*/
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1()
person1.show1.call(person2)
person1.show2()
person1.show2.call(person2)
person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()
person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
正确答案如下:
person1.show1() // person1,隐式绑定
person1.show1.call(person2) // person2,显式绑定
person1.show2() // window,箭头函数绑定,没有外层函数,指向window
person1.show2.call(person2) // window,箭头函数绑定,不能间接批改,还是指向window
person1.show3()() // window,高阶函数,person1.show3()返回一个函数ƒ(){ console.log(this.name)}到全局
//从而导致最终函数执行环境是window,所以此时this 指向 var name = 'window'
person1.show3().call(person2) // person2 ,返回函数当前,显式绑定person2,this指向person2对象
person1.show3.call(person2)() // window,高阶函数ƒ(){return function(){console.log(this.name)}} 显式绑定person2,
//也就是高阶函数this指向person2,它的返回值ƒ(){console.log(this.name)}执行环境是window,同上
person1.show4()() // person1,箭头函数绑定,this是从定义函数的上下文继承的,也就是外层函数所在的上下文,外层函数的this指向person1
person1.show4().call(person2) // person1,无奈通过call间接批改箭头函数绑定
person1.show4.call(person2)() // person2,高阶函数,外层函数this显式绑定person2,批改箭头函数的外层函数this指向,能够扭转箭头函数this指向
/**
* Question 2
*/
var name = 'window'
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
var personA = new Person('personA')
var personB = new Person('personB')
personA.show1()
personA.show1.call(personB)
personA.show2()
personA.show2.call(personB)
personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()
personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
正确答案如下:
personA.show1() // personA,new绑定当前,构造函数Person中的this绑定到personA,Person传入的参数personA,所以后果是personA
personA.show1.call(personB) // personB,new绑定当前,构造函数Person中的this绑定到personA,personA.show1就是ƒ(){console.log(this.name)}
// 再显式绑定personB,personA.show1的this指向personB实例对象,所以后果是personB
personA.show2() // personA,new绑定当前,构造函数Person中的this绑定到personA,personA.show2就是()=>console.log(this.name)
//而后箭头函数绑定,调用箭头函数,this指向外层函数的this.name,也就是personA
personA.show2.call(personB) // personA,new绑定当前,构造函数Person中的this绑定到personA,personA.show2就是()=>console.log(this.name)
//箭头函数不能间接批改,所以还是personA
personA.show3()() // window,new绑定当前,构造函数Person中的this绑定到personA,personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局
// 执行环境是window,所以执行当前的后果是var name = 'window',也就是window
personA.show3().call(personB) // personB,new绑定当前,构造函数Person中的this绑定到personA,
// personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局,
// 再显式绑定personB,所以最终后果是personB
personA.show3.call(personB)() // window,new绑定当前,构造函数Person中的this绑定到personA,
//高阶函数ƒ(){return function(){console.log(this.name)}}显式绑定personB,
//返回一个函数ƒ(){console.log(this.name)}到全局
//执行环境是window,所以执行当前的后果是var name = 'window',也就是window
personA.show4()() // personA,new绑定当前,构造函数Person中的this绑定到personA,
// 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name),执行箭头函数
// 箭头函数绑定,继承外层一般函数this,所以后果是personA
personA.show4().call(personB) // personA,new绑定当前,构造函数Person中的this绑定到personA,
// 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name),
// 箭头函数不能间接批改,所以后果还是personA
personA.show4.call(personB)() // personB,new绑定当前,构造函数Person中的this绑定到personA,
// 显式绑定 外层函数 ,所以箭头函数也被批改为 personB
参考
[你不晓得的JavaScript 上卷]
[Javascript权威指南第七版]
从这两套题,重新认识JS的this、作用域、闭包、对象
详解箭头函数和一般函数的区别以及箭头函数的注意事项、不实用场景
发表回复