这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
- var length = 10;
- function fn () {
- return this.length + 1;
- }
- var obj = {
- length: 5,
- test1: function () {
- return fn();
- }
- }
- obj.test2 = fn;
- console.log(obj.test1()); // 11
- console.log(fn() === obj.test2()); // false
看上面这段代码,这就是字节面试官准备的一道面试题,在面试过程中答得不是很好也没有完全做对。主要还是由于前两道题答得不是很好影响到后面答这道题时大脑是懵逼状态,再加上紧张让自己一时不知道这道题的考点是什么,进而影响到后续的面试状态。
其实这题不难,就是考JS
中的基础:this的指向问题,也就是这篇文章要聊的主题。
this的原理
this指向的值总是取决于它的执行环境。执行环境是代码执行时当前的环境或者作用域,在运行时,JavaScript
会维护一个执行环境栈;最顶部的那个是调用时所使用的执行环境,当执行环境变化时,this
的值也会改变。
其实理解this
的指向是非常重要的,至少它能让你在实际开发中少走弯路,少写bug,从而更好地掌握JavaScript
这门语言。接下来将从一些栗子🌰来介绍this
在各个场景下的指向问题,便于更好地去理解this
这个在面试中经常遇见的基础问题。
一、全局环境的this
在JavaScript
中,如果this
是在全局环境定义的,默认就是指向全局对象。而对于浏览器来说,全局对象就是window对象,所以this
就是指向就是window对象
。那如果是Node.js
呢?由于Node.js
没有window对象
,所以this
不能指向window对象
;但是Node.js
有global对象
呀,也就是说在Node.js
的全局作用域中this
的指向将会是global对象
。
console.log(this === window); // true
二、函数上下文调用
2.1 函数直接调用
普通函数内部的this
指向有两种情况:严格模式和非严格模式。
非严格模式下,this
默认指向全局对象,如下:
- function fn1() {
- console.log(this);
- }
- fn1(); // window
严格模式下,this
为undefined
。如下:
- function fn2() {
- "use strict"; // 严格模式
- console.log(this);
- }
- fn1(); // undefined
2.2 对象中的this
在对象方法中,this
的指向就是调用该方法的所在对象。此时的this
可以访问到该方法所在对象下的任意属性,如下:
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- console.log(this);
- console.log(this.name);
- }
- }
- obj.fn();
代码中定义了一个对象,对象中有个fn()
方法,调用这个方法打印结果如下:
可以看到,打印出来的this
的值为obj
这个对象,也就是fn()
所在的对象,进而此时的this
就能访问到该对象的所有属性。
如果fn()
方法中返回的是一个匿名函数,在匿名函数中访问this.name
,结果是什么呢?
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- return function() {
- console.log(this);
- console.log(this.name);
- }
- }
- }
- const fn = obj.fn();
- fn();
匿名函数的执行环境是全局作用域
,那这里this
打印出来的值就是window对象
,而this.name
则是window对象
下的name
属性,即this.name
的值为'张三'
。结果如下图:
- const obj = {
- fn: function() {
- return this.a + this.b;
- }
- }
- const obj1 = Object.create(obj);
- obj1.a = 1;
- obj1.b = 2;
-
- obj1.fn(); // 3
可以看到,obj1
中没有属性fn
,当执行obj1.fn()
时,会去查找obj1
的原型链,找到fn()
方法后就执行代码,这与函数内部this
指向对象obj1
没有任何关系。但是这里的this
指向就是obj1
,所以只需要记住谁调用就指向谁。
2.4 构造函数中的this
构造函数中,如果显式返回一个值并且返回的是个对象,那么this就指向这个返回的对象;如果返回的不是一个对象,那么this指向的就是实例对象。如下:
- function fn() {
- this.name = '张三';
- const obj = {};
- return obj;
- }
-
- const fn1 = new fn();
- console.log(fn1); // {}
- console.log(fn1.name); // undefined
上述代码将会打印结果为undefined
,此时fn1
是返回的对象obj
,如下:
- function fn() {
- this.name = '张三';
- return 1;
- }
-
- const fn1 = new fn();
- console.log(fn1);
- console.log(fn1.name);
上述代码则会打印结果为'张三'
,此时的fn1
是返回的fn
实例对象的this
。
2.5 call/apply/bind改变this
call
、apply
、bind
作用是改变函数执行时的上下文,也就是改变函数运行时的this
指向。
2.5.1 call
call
接受两个参数,第一个参数是this
的指向,第二个参数是函数接收的参数,以参数列表形式传入。call
改变this
指向后原函数会立即执行,且只是临时改变this
指向一次。
- function fn(...args) {
- console.log(this);
- console.log(args);
- }
- let obj = {
- name:"张三"
- }
- fn.call(obj, 1, 2);
- fn(1, 2);
可以看到,call
让this
的指向变成了obj
,而直接调用fn()
方法this
的指向是window对象
。
如果把第一个参数改为null
、undefined
,那结果是怎样的呢?答案:this
的指向是window对象
。如下:
- fn.call(null, 1, 2);
- fn.call(undefined, 1, 2);
2.5.2 apply
apply
第一个参数也是this
的指向,第二个参数是以数组形式传入。同样apply
改变this
指向后原函数会立即执行,且只是临时改变this
指向一次。
- function fn(...args) {
- console.log(this);
- console.log(args);
- }
- let obj = {
- name:"张三"
- }
- fn.apply(obj, [1, 2]);
- fn([1, 2]);
可以看到,apply
让this
的指向变成了obj
,而直接调用fn()
方法this
的指向是window对象
。
同样,如果第一个参数为null
、undefined
,this
默认指向window
。
apply
和call
唯一的不同就是第二个参数的传入形式:apply
需要数组形式,而call
则是列表形式。
2.5.3 bind
bind
第一参数也是this
的指向,第二个参数需要参数列表形式(但是这个参数列表可以分多次传入)。bind
改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数。
- function fn(...args) {
- console.log(this);
- console.log(args);
- }
- let obj = {
- name:"张三"
- }
- const fn1 = fn.bind(obj);
- fn1(1, 2);
- fn(1, 2);
2.5.4 小结
从上面可以看到,apply
、call
、bind
三者的区别在于:
- 都可以改变函数的
this
对象指向; - 第一个参数都是
this
要指向的对象,如果没有这个参数或参数为undefined
或null
,则默认指向window对象
; - 都可以传参,
apply
是数组,call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入; bind
是返回绑定this
之后的函数,apply
、call
则是立即执行。
三、箭头函数中的this
箭头函数本身没有this的,而是根据外层上下文作用域来决定的。箭头函数的this
指向的是它在定义时所在的对象,而非执行时所在的对象,故箭头函数的this是固定不变的。还有就是无论箭头函数嵌套多少层,也只有一个this
的存在,这个this
就是外层代码块中的this
。如下:
- const name = '张三';
- const obj = {
- name: '李四',
- fn: () => {
- console.log(this); // window
- console.log(this.name); // 打印李四?错的,是张三
- }
- }
- obj.fn();
看上述代码,都会以为fn
是绑定在obj对象
上的,但其实它是绑定在window对象
上的。为什么呢?
fn
所在的作用域其实是最外层的js
环境,因为没有其他函数的包裹,然后最外成的js
环境指向的是window对象
,故这里的this
指向的就是window对象
。
那要作何修改使它能永远指向obj对象
呢?如下:
- const name = '张三';
- const obj = {
- name: '李四',
- fn: functon() {
- let test = () => console.log(this.name); // 李四
- return test; // 返回箭头函数
- }
- }
- const fn1 = obj.fn();
- fn1();
上述代码就能让其永远的绑定在obj对象
上了,如果想用call
来改变也是改变不了的,如下:
- const obj1 = {
- name: '王二'
- }
-
- fn1.call(obj1); // 李四
- fn1.call(); // 李四
- fn1.call(null, obj1); // 李四
最后是使用箭头函数需要注意以下几点:
- 不能使用
new
构造函数 - 不绑定
arguments
,用rest
参数...
解决 - 没有原型属性
- 箭头函数不能当做
Generator
函数,不能使用yield
关键字 - 箭头函数的
this
永远指向其上下文的this
,任何方法都改变不了其指向,如call
,bind
,apply
四、globalThis
来看看MDN是怎么描述globalThis
的:
globalThis
提供了一个标准的方式来获取不同环境下的全局this
对象(也就是全局对象自身)。不像window
或者self
这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用globalThis
,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的this
就是globalThis
。
尽管如此,还是要看看globalThis
在现有各种浏览器上的兼容情况,如下图:
可以看到兼容情况还是可以,除了IE
浏览器。。。
下面就演示实现一个手写call()
方法,其中就使用到globalThis
来获取全局this
对象,如下:
- function myCall(context, ...args) {
- if (context === null) context = globalThis;
- if (context !== 'object') context = new Object(context);
- const key = symbol();
- const res = context[key](...args);
- delete context[key];
-
- return res;
- }
五、this的优先级
this
的绑定规则有4种:默认绑定,隐式绑定,显式绑定和new绑定。那下面分别来说说:
5.1 默认绑定
- var name = '张三';
- function fn1() {
- // 'use strict';
- var name = '李四';
- console.log(this.name); // 张三
- }
- fn1(); // window
默认绑定一般是函数直接调用。函数在全局环境调用执行时,this就代表全局对象Global,严格模式下就绑定的是undefined
5.2 隐式绑定
隐式绑定就是谁调用就是指向谁,如下:
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- console.log(this.name); // 李四
- }
- }
- obj.fn(); // obj调用就是指向obj
接着看下面两个栗子🌰,如下:
- // 链式调用
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- console.log(this.name);
- }
- }
-
- const obj1 = {
- name: '王二',
- foo: obj
- }
-
- const obj2 = {
- name: '赵五',
- foo: obj1
- }
-
- obj2.foo.foo.fn(); // 打印:李四(指向obj,本质还是obj调用的)
再来看一个,就可以更清楚了,如下:
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- console.log(this.name);
- }
- }
-
- const obj1 = {
- name: '王二',
- foo: obj.fn
- }
-
- obj1.foo(); // 打印:王二(指向obj1)
5.3 显式绑定
call
、apply
、bind
对 this
绑定的情况就称为显式绑定。
- const name = '张三';
- const obj = {
- name: '李四',
- fn: function() {
- console.log(this); // obj
- console.log(this.name); // 李四
- }
- }
-
- const obj1 = {
- name: '王二'
- }
-
- obj.fn.call(obj1); // obj1 王二
- obj.fn.apply(obj1); // obj1 王二
- const fn1 = obj.fn.bind(obj1);
- fn1(); // obj1 王二
5.4 new绑定
执行new
操作的时候,将创建一个新的对象,并且将构造函数的this
指向所创建的新对象。
- function foo() {
- this.name = '张三';
- }
- let obj = new foo();
- console.log(obj.name); // 张三
其实new
操作符会做以下工作:
- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
手动实现new
代码,如下:
- function myNew(fn, ...args) {
- // 1.创建一个新对象
- const obj = {};
- // 2.新对象原型指向构造函数原型对象
- obj.__proto__ = fn.prototype;
- // 3.将构建函数的this指向新对象
- let result = fn.apply(obj, args);
- // 4.根据返回值判断
- return result instanceof Object ? result : obj;
- }
测试一下,如下:
- function Animal(name, age) {
- this.name = name;
- this.age = age;
- }
- Animal.prototype.bark = function() {
- console.log(this.name);
- }
- let dog = myNew(Animal, 'maybe', 4);
- console.log(dog);
- dog.bark();
5.5 小结
this
的优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
至此,this
的相关原理知识就介绍完了,现在转过头再去看字节的那道面试题是不是就可以轻松作答了。this
是javascript
中最基础的知识点,往往就是这些最基础的知识让我们在很多面试过程中一次次倒下,所以基础知识真的很有必要再去深入了解,这样不管是在以后的工作还是面试中都能够让我们自信十足,不再折戟沉沙。
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。