this是什么
在函数调用的时候,会创建一个活动对象(active object,有时也称为执行上下文)。这个记录会包括函数在哪里被调用(调用栈)
、函数的调用方法
、传入的参数等信息
。this
就是活动对象记录在其中的一个属性,会在函数函数执行的过程中用到。
this与作用域的关系
this在任何时候都不指向词法作用域
,作用域确实与对象类似,可见的标识符都是它的属性,但是作用域对象
无法通过js代码访问,它存在于js引擎内部。
来个栗子:
function foo(){
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
注意: this与词法作用域查找混合使用时,这是无法实现的。
this
在函数调用的时候发生绑定,它指向什么完全取决于函数在哪里被调用。
this解决了什么问题
- this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。
来个栗子说明一下:
function identify(){
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identidy.call(this);
}
var me = {
name:"Rose",
}
var you = {
name:"Jack"
}
identify.call(me);
identify.call(you);
speak.call(me);
speak.call(you);
如果不使用this
呢?
function identify(context){
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identidy(context)
}
identify(you) ; Jack
speak(me); //Hello, I'm Rose
大白话就是:使用this
可以让不具备某个方法或属性的对象,访问该属性或调用该方法
this取决于什么?
- this在函数
调用的时候发生绑定
,指向完全取决于函数在哪里执行
。 - 始终坚持一个原理:
this 永远指向最后调用它的那个对象
、this 永远指向最后调用它的那个对象
、this 永远指向最后调用它的那个对象
重要的事情说三遍。
来个测试题看看你对this了解情况: 栗子1:
var number = 5;
var obj = {
number: 3,
fn1: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);
栗子1分析:
1. 声明的obj里面的fn1有自调用函数会先自执行,this.number *= 2;
即为window.number *= 2; 此时 window.number = 10; 自调用申明了一个变量number值为undefined,
nudefined *2 ,还是undefined, 最后为自调用函数内部的number赋值为3.
2. fn1引用的实际上是fn1返回的匿名函数,fn1.call(null),指的是匿名函数里面的this为window,
执行 var num = this.number; 即为 num = window.number ,所以 `输出为10`,this.number *= 2;
此时window.number *= 2; window.number = 20;
3. 匿名函数内部并没有申明number,需要往上层作用域找,上层作用域 number = 3; 执行完 number * 3,
此时将上层作用域的number赋值为9,`输出为9`
4. 自调用函数内申明的number=9;
5. obj.fn1() , 将匿名函数内部的this隐式转为obj, var num = this.number; 即为num = obj.number = 3;
`输出3` ,number *= 3; 由于匿名函数内部没有number,去上层作用域找,number = 9;number *= 3;
所以`输出 27`,
6. 此时window.number = 20;
误解指向自身
栗子2:
function countor(num){
console.log('countor',num)
//记录foo被调用的次数
this.count ++;
}
countor.count = 0;
var i = 0;
for(i = 0; i < 10 ; i++) {
if(i > 5) {
countor(i)
}
}
// foo:6
// foo:7
// foo:8
// foo:9
console.log(countor.count)
-
为什么countor明明调用了4次但是值还是0?
-
如果希望countor可以具备记录函数的调用次数有哪些解决方案? 方法一:
function countor(num){
console.log('countor',num);
countor.count ++;
}
countor.count = 0;
for(i = 0 ;i < 10; i++){
if(i > 5 ){
countor(i)
}
}
console.log(countor.count)
方法二:
function countor(num){
console.log('countor',num);
data.count ++;
}
var data = {
count: 0
}
var i ;
for(i = 0 ;i < 10; i++){
if(i > 5 ){
countor(i)
}
}
console.log(data.count)
虽然解决了问题,但是避开了this
通过直接将 countor.count的执行上下文直接指向当前的函数
即 直接调用 countor.count 替代this
,但还是避开了this
方案三:
function countor() {
console.log('countor',num)
//记录foo被调用的次数
this.count ++;
}
countor.count = 0;
var i = 0;
for(i = 0; i < 10 ; i++) {
if(i > 5) {
//直接改变函数调用时的上下文,将this指向countor本身
countor.bind(countor,i)
}
}
上面的案例说明了一个误区;
this在任何情况下都不指向函数的词法作用域
现在我们来回答为什么countor明明调用了4次但是值还是0?
-
- 从上面的3个案例可以看出,全局函数的
this
执行上下文不是指向函数本身而是window
- 从上面的3个案例可以看出,全局函数的
-
- 可以通过
改变函数调用时的this
指向来改变函数执行上下文
- 可以通过
this调用位置,当前执行上下文的栈顶
根据不同的调用位置,确定使用下面4种绑定规则的哪一种:
绑定规则
默认绑定、隐式绑定、显式绑定、new绑定
默认绑定
全局方法默认绑定为window,严格模式,不能将全局对象用于默认绑定,因此this会绑定到undefined
1.1 题目一
function foo() {
console.log(this.a);
}
var a = 2;
1.2 题目二
function foo(){
'use strict';
console.log(this.a);
}
var a =2;
foo();
严格模式下会报错; TypeError: Cannot read property 'a' of undefined
如果改为调用的地方为'use strict'呢?
function foo() {
console.log(this.a);
}
var a = 2;
(function(){
'use strict';
foo()
})()
结论:默认绑定(非严格模式下this指向全局对象, 严格模式下this会绑定到undefined),注意是申明的时候,而非调用的时候
。
隐式绑定
当函数引用上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
2.1 题目一
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
obj.foo();
2.1 题目二
如果引用2层呢?
function foo() {
console.log(this.a);
}
var obj2 = {
a:2,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo()
隐式丢失:常见在回调函数中
为什么隐式绑定的this会丢失?
3.1 题目一
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo;
var a = "global";
bar();
由于obj.foo引用的是函数本身,bar()调用的位置在全局,所以this.a输出 global
下面这个栗子更微妙、更出乎意料
3.2 题目二
function foo() {
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:'local',
foo:foo
}
var a = "global";
doFoo(obj.foo)
3.3 题目三
现在我们不用window调用doFoo,而是放在对象obj2里,用obj2调用:
function foo(){
console.log(this.a);
}
function doFoo(fn){
console.log(this);
fn();
}
var obj = {a:"local",foo};
var a = "global";
var obj2 = {a:"obj2", doFoo};
obj2.doFoo(obj.foo);
是不是感觉so easy,再来一个有挑战的:
3.4 题目四
var length = 10;
function fn () {
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn) {
console.log(this.length)
fn();
arguments[0]();
}
};
obj.method(fn, 1);
为什么arguments[0]()
的值是2呢?
显示绑定
- 通过
call()
或者apply()
、bind()
方法直接指定this的绑定对象, 如foo.call(obj)
使用显示绑定有三点需要注意:
- 使用
.call()、apply()
的函数会直接执行 - 使用
.bind() 会创建一个新的函数
,需要手动调用才能执行 - .call() 、.apply()用法基本类似,不过
call接受若干个参数
,apply接受一个数组。
写个伪代码实现看看bind是如何实现的:
Function.prototype.call = function (context, ...args ){
const fn = Symbol();
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
}
bind做了4件事:
- 在执行上下文上增加一个唯一属性fn
- 将this指向该属性
- 执行fn函数
- 删除上下文属性fn,并返回fn调用的结果
4.1 题目一
function foo() {
console.log(this.a);
}
var obj = {a: 1};
var a = 2;
foo();
foo.call(obj);
foo.apply(obj);
foo.bind(obj);
foo.bind(obj)
,原因是因为bind创建了一个新函数需要用变量接收并调用,因此此处不会执行。
注意:如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数
。
function foo() {
console.log(this.a);
}
var a = "global";
foo.call();
foo.call(null);
foo.call(undefined);
知道显示绑定后,我们来看一个它的妙用
4.2 题目二
var obj1 = {
a:1
}
var obj2 = {
a:2,
foo1:function(){
console.log(this.a);
},
foo2:function() {
console.log(this);
setTimeout( function (){
console.log(this);
},0)
}
}
var a = "global";
obj2.foo1();
obj2.foo2();
如果希望定时器里this指向 obj1 如何改造?
4.3 题目三
setTimeout(function(){
console.log(this);
}.call(obj1),0)
// 只需要在上例的回调函里面 .bind(obj1)
所以有小伙伴就会问了,我下面的这种写法不可以吗?
obj2.foo2.bind(obj1)
注意⚠️:这种写法实际上是改变了foo2
函数内部的this
, 而 setTimeou
t里的函数this
与 foo2
函数里面的this
是没有关系的,定时器调用里面的this始终都是window
4.4 题目四
OK👌,我们不用定时器,把它干掉,换成一个函数:
var obj1 = {
a:"obj1"
}
var obj2 = {
a:"obj2",
foo1:function() {
console.log(this.a);
},
foo2:function() {
console.log(this.a);
function inner() {
console.log(this.a);
}
inner()
}
}
var a = 3;
obj2.foo1();
obj2.foo2()
如果将 inner() 改为显示绑定呢?
inner.call(obj1)
4.5 题目五
看看下题会输出什么?
function foo() {
console.log(this.a);
}
var obj = {
a:1
}
var a = "global";
foo();
foo.call(obj);
foo().call(obj)
注意⚠️:此处会报错Uncaught TypeError: Cannot read property 'call' of undefined
,因为 call必须要被函数调用。
那如果函数foo里面返回一个函数呢?
4.6 题目六
function foo() {
console.log(this.a);
return function() {
console.log(this.a)
}
}
var obj = {
a:1
}
var a = "global";
foo();
foo.call(obj);
foo().call(obj);
如果把上面的call
换成bind
会如何?
4.7 题目七
function foo() {
console.log(this.a);
return function () {
console.log(this.a)
}
}
var obj = {
a:1
}
var a = "global";
foo();
foo.bind(obj);
foo().bind(obj)
注意⚠️:foo.bind(obj)
不会执行,因为返回的新的函数需要变量接收并调用才可以。
4.8 题目八
函数内层的this与函数外层的this有关系?内层this到底指向谁?我们重要的口诀再来一遍:this由最后一个调用它的对象决定
function foo () {
console.log(this.a)
return function () {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()
如果将上面的函数返回函数放入对象中呢?
4.9 题目九
var obj = {
a:"obj",
foo:function() {
console.log('foo',this.a);
return function() {
console.log('inner',this.a)
}
}
}
var obj2 = { a: "obj2"};
var a = 2
obj.foo();
obj.foo.call(obj2)();
obj.foo().call(obj2)
加个参数玩玩
4.10 题目十
var obj = {
a:1,
foo:function(b) {
b = b || this.a;
return function(c) {
console.log(this.a + b + c)
}
}
}
var a = 2;
var obj2 = { a: 3};
obj.foo(a).call(obj2,1);
obj.foo.call(obj2)(1)
new绑定
new做了什么?
function myNew (Person) {
const context = Object.create(Person);
let result = context.call(context);
if((typeof result !== 'null') && (typeof result === 'object' || typeof result === 'function')) {
return result;
}else {
return context;
}
}
总结4句话:
- new一个新对象,
- 新对象原型指向构造函数
- 并将this指向创建的新对象
- 函数如果没有返回其他对象,则返回新创建的对象,如果构造函数返回的为对象或者函数,则将该对象或函数返回
new绑定的优先级问题
5.1 题目一
function foo() {
console.log(this.a);
}
var obj1 = {
a:2,
foo:foo
}
var obj2 = {
a:3,
foo:foo
}
obj1.foo.call(obj2);
obj2.foo.call(obj1);
可以看到显示绑定优先级更高
下面看看new绑定
与隐式绑定
优先级
5.2 题目二
function foo(something){
this.a = something;
}
var obj1 = {
foo:foo
}
var obj2 = {};
obj1.foo(1);
console.log(obj1.a);
obj1.foo.call(obj2,3);
console.log(obj2.a);
var bar = new obj1.foo(4);
console.log(obj1.a);
console.log(bar.a);
可以看出new绑定比隐式绑定优先级更高
,现在我们看看 new绑定与显示绑定
哪个优先级更高??
5.3 题目三
由于我们无法 通过 new foo.call(obj1)
测试,所以我们间接通过硬绑定实现
function foo(something){
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);
var baz = new bar(3);
console.log(baz.a);
console.log(bar.a);
new bar并没有像我们预计的那样把obj1.a修改为3.
bind的内部实现:会判断是否被new 调用,如果是的话就会使用新创建的this替换硬绑定的this
5.4 题目四
var name = 'window'
function Person (name) {
this.name = name
this.foo = function () {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo.call(person2)()
person1.foo().call(person2)
解题思路:
person1.foo().call(person2)可以理解为 person1.foo()调用返回的匿
名函数的上下文绑定到person2身上
总结,判断this:
根据优先级来判断函数
在某个调用位置应该用的是哪条规则,可以按照下面的顺序来判断
-
函数是否在new中调用(new绑定)?如果是this绑定就是新创建的对象。var bar = new foo():
-
函数是通过call、apply、(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
-
函数是否在某个上下文中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo();
-
如果都不是的话,使用默认绑定,在非严格模式下绑定到undefined,否则绑定到全局对象 var var = foo();
箭头函数
-
对于上面的问题,
this永远指向最后一个绑定它的对象
,但是对于箭头函数就不一样了。 -
箭头函数的this 由外层的作用域决定的
,指向定义时的this,而非执行时。
它里面的this由外层的作用域决定的是什么意思呢?
箭头函数中没有this绑定,必须通过查找作用域链来决定它的值,如果箭头函数被非箭头函数包含,则this就指向
最近的一层非箭头函数的this
, 否则this为undefined
6.1 题目一
来个简单的栗子:
var name = 'window'
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this.name)
}
}
var obj2 = {
name: 'obj2',
foo: () => {
console.log(this.name)
}
}
obj1.foo()
obj2.foo()
6.2 题目2
var obj = {
name:"obj",
foo1:() => {
console.log(this.name);
},
foo2:function (){
console.log(this.name);
return () => {
console.log(this.name);
}
}
}
var name = "galbol";
obj.foo1();
obj.foo2()();
解题:
- 对于
obj.foo1()
函数的调用,它的外层作用域是window,对象obj当然不属于作用域,(作用域只有全局作用域与函数创建的局部作用域),所以输出为galbol
6.3 题目3
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
}
this.foo2 = () => {
console.log(this.name)
}
}
var person2 = {
name: 'person2',
foo2: () => {
console.log(this.name)
}
}
var person1 = new Person('person1')
person1.foo1()
person1.foo2()
person2.foo2()
解题:
1. person1.foo2() 构造函数里面的箭头函数,箭头函数`this由外层作用域决定,且指向定义时而非执行时`,
外层作用域是函数Person,且构造函数new 生成了新对象,所以此时this指向为person1
6.4 题目四
var name = 'window'
var obj1 = {
name: 'obj1',
foo1: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
},
foo2: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.foo1.call(obj2)()
obj1.foo2().call(obj2)
总结一下箭头函数需要注意的点吧:
-
它里面的this由离它最近的外层函数来决定,且
指向函数定义时的this而非执行时
-
字面量创建的对象,作用域是window,如果里面有箭头函数是属性的话,this指向的是window
-
构造函数创建的对象,作用域可以理解为这个构造函数,且这个构造函数
this是指向新创建的对象
-
箭头函数里面的this是无法通过bind、apply、call来修改的,但是可以通过改变作用域中的this指向来间接修改。
优点:
- 箭头函数可以让代码拥有更加简洁的语法
this
由外层作用域决定,所以可以避免写 const that = this;这样的代码
需要避免使用箭头函数的场景
- 使用箭头函数定义对象的方法
let obj = {
value:"Fancy",
getValue:() => console.log(this.value);
}
obj.getValue() //undefined
- 定义原型方法
function Foo(value) {
this.value = value;
}
Foo.prototype.getValue = () => console.log(this.value);
const foo1 = new Foo(1);
foo1.getValue(); //undefined;
- 作为事件的回调函数
const button = document.getElementsById("myButton");
button.addEventListener("click",() =>{
console.log(this === window) ; //true
this.innerHTML = "Clicked button";
})
4.构造函数使用箭头函
const Foo = (value) => {
this.value = value;
}
const foo1 = new Foo(1);
console.log(foo1); // Foo is not a constructor
最后来综合题:
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1()
person1.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)
person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
解题思路;
person1.foo2()
person1.foo2.call(person2)
// 需要注意,在构造函数中,this.foo2 = () => console.log(this.name);
// this由外层作用域决定,且指向函数定义而非执行时,这里的外层作用域是函数Person,且是构造函数,new生了
// person1,所以此时this指向为person1
参考阅读