JS中this指向详解

447 阅读7分钟

相关知识点

  • 函数调用
  • this 的定义
  • this 的指向
  • 如何改变 this 的指向

函数调用

JS(ES5)中有三种函数调用形式,分别为以下几种:

fn(p1, p2)
obj.child.method(p1,  p2)
fn.call(context, p1, p2)

第三种形式才是正常的调用形式

fn.call(context, p1, p2)

其它两种都是语法糖,可以等价的变为 call 形式

  • fn(p1, p2) 等价于 fn.call(undefined, p1, p2)
  • obj.child.method(p1, p2) 等价于 obj.child.method.call(undefined, p1, p2)
var a = 5;
var obj = {
  a: 10,
  foo: function () {
    console.log(this.a);
  }
};
let bar = obj.foo;
obj.foo();
bar();
  • obj.foo() 转化为 call 的形式为 obj.foo.call(obj),所以 this 指向了 obj,最后 this.a 输出为 10;
  • bar() 转化为 call 的形式为 bar.call(),由于没有传 context,如果在浏览器环境下 this 指向 Window 对象,如果是在 Node.js 环境中运行,this 指向 global 对象,在浏览器中输出为 5;在 Node.js 中输出为 undefined

this 的定义

this 就是一个对象,不同情况下 this 的指向不同。

this 的几种指向情况

全局环境(默认绑定)

在全局执行环境中(在任何函数的外部),this 都是指向全局对象,在浏览器环境下,window 对象即是全局对象。

var a = 1;
let b = 2; // let和const定于的数据不会绑定在window上
console.log(window.a); // 1
console.log(window.b); // undefined

this.c = 3;
console.log(c); // 3
console.log(window.c); // 3

如果使用 let 或者 const 定义数据,是不会绑定在 window 上的。

默认绑定的另一种情况(setTimeOut/setInterval)

在函数中一函数作为参数传递,例如 setTimeOutsetInterval 等,这些函数中传递的函数中的 this 指向,在非严格模式下指向的是全局对象。

var name = "tom";
var person = {
  name: "我是person",
  sayHi: sayHi
};
function sayHi() {
  console.log("Hello,", this.name);
}
setTimeout(function () {
  console.log(this); 
  person.sayHi(); 
  sayHi();
}, 200);

输出结果为:

Window {window: Window, self: Window, document: document, name: "tom", location: Location, …}
Hello, 我是person
Hello, tom

对象调用

this 指向该对象(前面谁调用 this 就指向谁),和声明在哪里无关。

var obj = {
  name: "Tom",
  age: "21",
  print: function () {
    console.log(this);
    console.log(this.name + " - " + this.age);
  }
};

obj.print();

根据上面所讲,obj.print() 可以转化为 obj.print.call(obj),所以函数中的 this 指向 obj 对象,最后输出结果为:Tom - 21

当有多层对象嵌套调用某个函数的时候,如对象.对象.函数,this 指向的是最后一层对象。

function sayHi() {
  console.log(this);
  console.log("Hello,", this.name);
}
var person = {
  name: "tom",
  sayHi: sayHi
};
var person1 = {
  name: "jack",
  friend: person
};
person1.friend.sayHi();
console.log(person1.friend);

输出结果为:

{name: "tom", sayHi: ƒ}
Hello, tom
{name: "tom", sayHi: ƒ}

直接调用的函数

直接调用的函数 this 指向的是全局对象。

function print() {
  console.log(this);
}
print();

[] 语法中的 this

我们先看一个简单的 demo:

function fn() {
  console.log(this);
  console.log(this === arr); // true
}
function fn1() {
  console.log(this);
}
var arr = [fn, fn1];
arr[0]();

我们可以把 arr[0]() 想象成 arr.0(),虽然语法上有错误(先忽略错误),但是形式可以和 obj.child.method(p1, p2) 相对应,最后转换成 arr[0]() => arr.0() => arr.0.call(arr) 的形式,这样就可以理解 this 指向 arr 了。

new 构造函数中的 this

  • this 永远指向新创建的对象;
  • 如果构造函数中有 return
    • 如果 return 的值是对象,this 指向返回的对象;
    • 如果 return 的值不是对象,则 this 指向保持原有的规则;
  • null 比较特殊,null 的数据类型为对象,但是 this 指向保持原有的规则;

题目一

function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this); // Person {name: "Tom", age: 22}
}

new Person("Tom", 22);

题目二

function Fn() {
  this.num = 10;
}

Fn.num = 20; // num为Fn构造函数的静态属性
Fn.prototype.num = 30; // num为Fn构造函数的实例属性
Fn.prototype.method = function () {
  console.log(this);
  console.log(this.num);
};

var prototype = Fn.prototype;
var method = prototype.method;

let fn = new Fn();
console.log(fn); // Fn {num: 10}
console.log(Fn.num); // 输出的是静态属性 20
fn.method(); // this 指向 Fn,this.num => 10
prototype.method(); // this 指向 prototype,this.a => 30
method(); // this 指向全局window,this.a => undefined

题目三

function Fn() {
  this.num = 10;
  // return "";
  // return {};
  // return {
  //   num: 20
  // };
  // return null;
  return undefined;
}

var fn = new Fn();
console.log(fn.num);
  • return "",结果输出为:10
  • return {},结果输出为:undefined
  • return { num: 20 },结果输出为:20
  • return null,结果输出为:10
  • return undefined,结果输出为:10

箭头函数中的 this

  • 箭头函数没有 thisarguments
  • 箭头函数中的 this 在定义的时候就已经决定了,后期使用 callapplybind 都不能改变 this 的指向;
  • 由于箭头函数没有单独的 this 值,所以箭头函数中的 this 与声明所在的上下文相同;
  • 调用箭头函数的时候,不会隐士的调用 this 参数,而是定义时的函数继承上下文;
  • 箭头函数没有构造函数;
    • 箭头函数与正常的函数不同,箭头函数没有构造函数 constructor,因为没有构造函数,所以也不能使用 new 来调用,如果我们直接使用 new 调用箭头函数,会报错;
let fun = () => {};
let funNew = new fun();
// 报错内容 TypeError: fun is not a constructor
  • 箭头函数没有原型;
    • 原型 prototype 是函数的一个属性,但是在箭头函数中不存在 prototype
let fun = () => {};
console.log(fun.prototype); // undefined

let fun2 = function () {};
console.log(fun2.prototype); // {constructor: ƒ}
  • 箭头函数中没有 super
    • 由于箭头函数中没有原型,连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 thisargumentsnew.target 一样,这些值由外围最近一层非箭头函数决定;
  • 对象不能形成独立的作用域;
const obj = {
  a: () => {
    console.log(this);
  },
  b: function () {
    console.log(this);
    console.log(this === obj); // true
  },
  c() {
    console.log(this);
    console.log(this === obj); // true
  }
};

obj.a(); // Window
obj.b(); // {a: ƒ, b: ƒ, c: ƒ}
obj.c(); // {a: ƒ, b: ƒ, c: ƒ}
var length = 10;
function fn() {
  console.log(this);
  console.log(this.length);
}
var obj = {
  length: 5,
  method: function (fn) {
    fn();
    arguments[0]();
  }
};
obj.method(fn, 1);

输出结果为:

Window  10
Arguments  2
var name = "tom";
var obj = {
  name: "jack",
  prop: {
    name: "rose",
    getName: () => {
      return this.name;
    }
  }
};
console.log(obj.prop.getName());
var test = obj.prop.getName;
console.log(test());

使用 obj.prop.getName() 方式调用情况下,this 与声明所在的上下文相同,也就是 Window,所以 this.name => Window.name,结果为 tom

输出结果为:

tom,tom

如果把 getName 改成普通函数的话:

var name = "tom";
var obj = {
  name: "jack",
  prop: {
    name: "rose",
    getName: function () {
      return this.name;
    }
  }
};
console.log(obj.prop.getName());
var test = obj.prop.getName;
console.log(test());

输出结果为:

rose,tom

自执行函数

什么是自执行函数?自执行函数在我们在代码只能够定义后,无需调用,会自动执行。代码例子如下:

(function () {
  console.log(this); // Window
  console.log("我是fn");
})();

或者

(function() {
  console.log(this); // Window
	console.log("我是fn");
}());

但是如果使用了箭头函数简化一下就只能使用第一种情况了,使用第二种情况简化会报错。

// 正确
(() => {
  console.log("我是fn");
})();

// 报错,Uncaught SyntaxError: Unexpected token '('
(() => {
  console.log("我是fn");
}());

以一个例题解释一下自执行函数中的 this 指向:

function Fn() {
  this.name = "tom";
  (() => {
    console.log(this); // Fn {name: "tom"}
  })();
  (function () {
    console.log(this); // Window
  })();
}

let fn = new Fn();

(() => {
  console.log(this); // Window
})();

(function () {
  console.log(this); // Window
})();

如何改变 this 的指向

我们可以通过调用函数的 callapplybind 来改变 this 的指向。

var obj = {
  name: "tom",
  age: "22",
  info: "我叫Tom,22岁"
};

function print() {
  console.log(this); // 打印this的指向
  console.log(arguments); // 打印传递的参数
}

// 通过 call 改变 this 指向
print.call(obj, 1, 2, 3);

// 通过 apply 改变 this 指向
print.apply(obj, [1, 2, 3]);

// 通过 bind 改变 this 的指向,不会立即执行,会返回一个函数
let fn = print.bind(obj, 1, 2, 3);
fn();
  • 共同点:
    • 三者都能改变 this 指向,且第一个传递的参数都是 this 指向的对象;
    • 三者都采用的后续传参的形式;
  • 不同点:
    • call 的传参是单个传递的(数组也可以,不报错),而 apply 后续传参的参数数组形式(传单个值会报错),而 bind 没有规定,传递值和数组都可以;
    • callapply 函数的执行是立即执行的,而 bind 函数会返回一个函数,通过调用函数才会执行;

如果使用上边的方法改变箭头函数的 this 指针,会发生什么情况呢?能否进行改变呢?

由于箭头函数没有自己的 this 指针,通过 call()apply() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。

var obj = {
  name: "tom",
  age: "22"
};

const print = (...args) => {
  console.log(this); // 都是指向 Window
  console.log(args); // [1, 2, 3]
};

// 通过 call 改变 this 指向
print.call(obj, 1, 2, 3);

// 通过 apply 改变 this 指向
print.apply(obj, [1, 2, 3]);

// 通过 bind 改变 this 的指向,不会立即执行,会返回一个函数
let fn = print.bind(obj, 1, 2, 3);
fn();