javascript中的this详解及面试题分析

文章参考来源,写博客的目的就是梳理自己对知识的理解,引用了其他作者比较精华的部分,只是为了方便自己复习,或许也能帮助一些小伙伴节约时间。
https://juejin.im/post/5de4fe1d5188255e8b76e1f2
https://juejin.im/post/596a28f6f265da6c360a2716
this面试题

this的初衷:

this设计的初衷是在函数内部使用,用来指代当前的运行环境。为什么这么说呢?
JavaScript中的对象的赋值行为是将地址赋给一个变量,引擎在读取变量的时候其实就是要了个地址然后再从原始地址中读取对象。而JavaScript 允许函数体内部引用当前环境的其他变量,而这个变量是由运行环境提供的。由于函数又可以在不同的运行环境执行(如全局作用域内执行,对象内执行…),所以需要一个机制来表明代码到底在哪里执行!于是this出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

this 的四种绑定规则和箭头函数中的this

在 JavaScript 中,影响 this 指向的绑定规则有四种:
1.默认绑定《》global this
2.隐式绑定《
》function this
3.显式绑定《》call、apply和bind中的this
4.new 绑定《
》构造函数中的this

global this

在浏览器里,在全局范围内:

1.this等价于window对象;
2.用var声明一个变量和给this或者window添加属性是等价的;
3.如果你在声明一个变量的时候没有使用var或者let、const(es6),你就是在给全局的this添加或者改变属性值。

// 1
console.log(this === window); //true
//2
var name = "Jake";
console.log(this.name ); // "Jake"
console.log(window.name ); // "Jake"

//3
 age = 23;
 function testThis() {
   age = 18;
 }
 console.log(this.age ); // 23
 testThis();
 console.log(this.age ); // 18

总结起来就是:在全局范围内this等价于window对象(即指向window),如果你声明一些全局变量(不管在任何地方),这些变量都会作为this的属性。

改造下题目一,看看在严格模式下。
(想要开启严格模式,只要在需要开启的地方写上"use strict")

"use strict";
var a = 10;
function foo () {
  console.log('this1', this)
  console.log(window.a)
  console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();

需要注意的点:
开启了严格模式,只是说使得函数内的this指向undefined,window中的this还是指向window。因此this1中打印的是undefined,而this2还是window对象。另外,它也不会阻止a被绑定到window对象上。
ps:
javascript中的this详解及面试题分析_第1张图片
所以最后的执行结果:

f foo() {...}
'this2' Window{...}
'this1' undefined
10
Uncaught TypeError: Cannot read property 'a' of undefined

题目三

let a = 10
const b = 20

function foo () {
  console.log(this.a)
  console.log(this.b)
}
foo();
console.log(window.a)

复制代码如果把var改成了let 或者 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined

ps:

顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。

function this

对于函数中的this的指向问题,有一句话很好用:

运行时this永远指向最后调用它的那个对象。

举一个栗子

var name = "windowsName";
function sayName() {
var name = "Jake";
console.log(this.name);   // windowsName
console.log(this);    // Window
}
sayName();
console.log(this) // Window

复制代码我们看最后调用 sayName的地方 sayName();,前面没有调用的对象那么就是全局对象 window,这就相当于是window.sayName()。

需要注意的是,对于严格模式来说,默认绑定全局对象是不合法的,this被置为undefined。会报错 Uncaught TypeError: Cannot read property ‘name’ of undefined。

再看下面这个栗子

function foo() {
    console.log( this.age );
}

var obj1 = {
    age : 23,
    foo: foo
};

var obj2 = {
    age : 18,
    obj1: obj1
};

obj2.obj1.foo(); // 23

最后调用foo()的是obj1,所以this指向obj1,输出23。

隐式绑定的隐式丢失问题:

隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。
题目一
第一种情况:使用另一个变量来给函数取别名会发生隐式丢失。

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;

obj.foo();
foo2();

这是因为虽然foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window。
第二题:

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }

obj.foo();
foo2();
obj2.foo2();

答案是1,2,3
obj.foo()中的this指向调用者obj
foo2()发生了隐式丢失,调用者是window,使得foo()中的this指向window
foo3()发生了隐式丢失,调用者是obj2,使得foo()中的this指向obj2.
题目三
再就是如果你把一个函数当成参数传递时,也会被隐式赋值.

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

复制代码这里我们将obj.foo当成参数传递到doFoo函数中,在传递的过程中,obj.foo()函数内的this发生了改变,指向了window。

构造函数中的this

所谓构造函数,就是通过这个函数生成一个新对象(object)。当一个函数作为构造器使用时(通过 new 关键字), 它的 this 值绑定到新创建的那个对象。如果没使用 new 关键字, 那么他就只是一个普通的函数, this 将指向 window 对象。
这又是另一个经典话题:new 的过程

var a = new Foo("zhang","jake");

new Foo{
    var obj = {};
    obj.__proto__ = Foo.prototype;
    var result = Foo.call(obj,"zhang","jake");
    return typeof result === 'obj'? result : obj;
}

复制代码若执行 new Foo(),过程如下:
1)创建新对象 obj;
2)给新对象的内部属性赋值,构造原型链(将新对象的隐式原型指向其构造函数的显示原型);
3)执行函数 Foo,执行过程中内部 this 指向新创建的对象 obj(这里使用了call改变this指向);
4)如果 Foo 内部显式返回对象类型数据,则返回该数据,执行结束;否则返回新创建的对象 obj。

var name = "Jake";
function testThis(){
  this.name = 'jakezhang';
  this.sayName = function () {
		return this.name;
	}
}
console.log(this.name ); // Jake

new testThis(); 
console.log(this.name ); // Jake

var result = new testThis();
console.log(result.name ); // jakezhang
console.log(result.sayName()); // jakezhang

testThis();  //此时函数内部指向的是window,修改了name属性
console.log(this.name ); // jakezhang

很显然,谁被new了,this就指向谁。

call、apply和bind中的this

Function.prototype 中的三个方法 call(), apply(), bind() 都可以改变函数的 this 指向到指定的对象,目前所有关于它们的运用,都是基于这一点来进行的。不同之处在于,call() 和 apply() 是立即执行函数,并且接受的参数的形式不同:
call(this, arg1, arg2, …)
apply(this, [arg1, arg2, …])
而 bind() 则是创建一个新的包装函数,并且返回,而不是立刻执行。
bind(this, arg1, arg2, …)
apply() 接收参数的形式,有助于函数嵌套函数的时候,把 arguments 变量传递到下一层函数中。

function foo() {
  console.log(this.a);  // 输出 1
  bar.apply({a: 2}, arguments);//arguments:[3]
}

function bar(b) {
  console.log(this.a + b);  // 输出 5
}

var a = 1;
foo(3);

上面代码中, foo() 内部的 this 遵循默认绑定规则,绑定到全局变量中window。而 bar() 在调用的时候,调用了 apply() 函数,把 this 绑定到了一个新的对象中 {a: 2},而且原封不动的接收 foo() 接收的函数。

箭头函数中的 this

es5中的this要看函数在什么地方调用(即要看运行时),通过谁是最后调用它该函数的对象来判断this指向。但es6的箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。箭头函数的 this 始终指向函数定义时的 this,而非执行时。

 let name = "zjk";
    let o = {
        name : "Jake",
        sayName: function () {
            console.log(this.name)     
        },
        func: function () {
            setTimeout( () => {
                this.sayName()
            },100);
        }

    };
o.func()     // Jake
this指向的是func环境,而func环境中this是o,因为o调用了这个函数。

使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在创建(函数定义)时设置的 this值。

下面看一道面试题

var number = 1;

var obj = {

	number:2,

	showNumber:function(){
    console.log(this.number);//2
	this.number = 3;

	(function(){

	console.log(this.number);//1

})();

	console.log(this.number);//3,被修改了

}

};

obj.showNumber();//1,3

showNumber()里的自调用function会提升到全局范围内,所以它的this是指向window对象,而showNumber()内部的this则是指向obj对象,即{number: 2, showNumber: ƒ}。

箭头函数的this无法通过bind、call、apply来直接修改,但是可以通过改变作用域中this的指向来间接修改。

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.foo1().call(obj2)
obj1.foo2.call(obj2)()
obj1.foo2().call(obj2)

在这里插入图片描述
obj1.foo1.call(obj2)()执行之后第一层函数的this就已经改变了,第二层的箭头函数和第一层的一样。
obj1.foo1.call(obj2)()第一层为普通函数,并且通过.call改变了this指向为obj2,所以会打印出obj2,第二层为箭头函数,它的this和外层作用域中的this相同,因此也是obj2。
在这道题中,obj1.foo1.call(obj2)()就相当于是通过改变作用域间接改变箭头函数内this的指向。

优先级:

// 显式绑定 > 隐式绑定

function foo() {
    console.log(this.a);
}

let obj1 = {
    a: 2,
    foo,
}

obj1.foo();     // 输出 2
obj1.foo.call({a: 1});      // 输出 1

// new 绑定 > 显式绑定

function foo(a) {
    this.a = a;
}

let obj1 = {};

let bar = foo.bind(obj1);
bar(2);
console.log(obj1); // 输出 {a:2}

let obj2 = new bar(3);
console.log(obj1); // 输出 {a:2}
console.log(obj2); // 输出 foo { a: 3 }

所以优先级顺序为:
「new 绑定」 > 「显式绑定」 > 「隐式绑定」 > 「默认绑定。」
综合题:

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)

javascript中的this详解及面试题分析_第2张图片
这题区别就是,函数是放在其中的一个叫obj的对象里面。
person1.obj.foo1()()返回的是一个普通的匿名函数,调用它的是window,所以打印出window。
person1.obj.foo1.call(person2)()中是使用.call(person2)改变第一层函数中的this,匿名函数和它没关系,依旧是window调用的,所以打印出window。
person1.obj.foo1().call(person2)是通过.call(person2)改变匿名函数内的this,所以绑定有效,因此打印出person2。
person1.obj.foo2()()第一层为普通函数,第二层为匿名箭头函数。首先让我们明确匿名箭头函数内的this是由第一层普通函数决定的,所以我们只要知道第一层函数内的this是谁就可以了。而这里,第一层函数最后是由谁调用的呢 ️?是由obj这个对象,所以打印出obj。
person1.obj.foo2.call(person2)()中使用.call(person2)改变了第一层函数中的this指向,所以第二层的箭头函数会打印出person2。
person1.obj.foo2().call(person2)中使用.call(person2)想要改变内层箭头函数的this指向,但是失败了,所以还是为外层作用域里的this,打印出obj。

题目:

function foo() {
  console.log( this.a );
}
var a = 2;
(function(){
  "use strict";
  foo();
})();

使用了"use strict"开启严格模式会使得"use strict"以下代码的this为undefined,也就是这里的立即执行函数中的this是undefined
但是调用foo()函数的依然是window,所以foo()中的this依旧是window,所以会打印出2。如果你是使用this.foo()调用的话,就会报错了,因为现在立即执行函数中的this是undefined。或者将"use strict"放到foo()函数里面,也会报错。
自己的理解:函数foo是在严格模式范围之外声明的,所以说this是window,而立即执行函数的在use strict之下的内容this为undefined.
所以下面的代码证明了这点,放在严格模式内声明this就是undefined.

var a = 2;
(function(){
  "use strict";
  function foo() {
  console.log( this.a );//Cannot read property 'a' of undefined
}
  foo();
})();

总结:

1.this 并不是在编写的时候绑定的,而是在运行时绑定的。
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。它指向什么完全取决于函数在哪里被调用。(箭头函数除外,函数定义时确定)
2.在浏览器里,在全局范围内this 指向window对象;
在函数中,this永远指向最后调用他的那个对象;
构造函数中,this指向new出来的那个新的对象;
call、apply、bind中的this被强绑定在指定的那个对象上;
箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
3.apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
引用的图:
javascript中的this详解及面试题分析_第3张图片

你可能感兴趣的:(js,面试秃破瓶颈)