js中this指向的问题与联系深入探究
1.0 前言
JavaScript 中最大的一个安全问题,也是最令人困惑的一个问题,就是在某些情况下this 的值是如何确定的。有js基础的同学面对这个问题基本可以想到:this 的指向和函数调用的方式相关。这当然是正确的,然而,这几种方式有什么联系吗?这是我接下来要说明的问题。
2.0 this从哪里来
this 是js的一个关键字,和arguments 类似,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。这句话似乎与认知不同,我们在函数体外部即全局作用域下也能使用this 。// 直接在全局作用域下输出this console.log(this); // 输出window
但是不要忘记,即便是全局作用域,依旧是运行在window 下的,我们写的代码都在window 的某个函数中。而这也催生了一种理解this 指向的方法:this 永远指向调用者(非箭头函数中)。
3.0 作为普通函数调用
函数作为普通函数直接调用(也称为自执行函数)的时候,无论函数在全局还是在另一个函数中,this 都是指向window 。function fn() { this.author = "Wango"; } fn(); console.log(author); // Wango
这很好理解,但又不是很好理解,因为在代码中省略了window ,补全后就好理解了:this 指向的是调用者。
function fn() { this.author = "Wango"; } window.fn(); console.log(window.author); // Wango
而在内部函数中,自执行函数中的this 依旧指向全局作用域,我们 无法通过window.foo() 调用函数,但并不妨碍我们先这样理解(具体参见本文最后一部分this 的强制转型) 。
function fn() { function foo() { console.log(this); } foo(); // Window window.foo(); // TypeError } fn();
4.0 作为构造函数调用
在构造函数中,this 指向new 生成的新对象,即构造函数是通过new 调用的,构造函数内部的this 当然就应该指向new 出来的对象。
function Person(name, age) { this.name = name; this.age = age; console.log(this); // Person { name: "Wango", age: 24 } } new Person("Wango", 24); 构造函数中的this与构造函数的返回值类型无关,下列代码中p指向了构造函数返回的对象,而不是new出来的对象。当然,这是构造函数的特性,与本主题关系不大。
function Person(name, age) { console.log(this); // Person {} this.name = name; this.age = age; console.log(this); // Person { name: "Wango", age: 24 } return { name: "Lily", age: 25 } } Person.prototype.sayName = function() { return this.name + " " + this.age } const p = new Person("Wango", 24); console.log(p.sayName()); // TypeError: p.sayName is not a function 5.0 作为对象方法调用
通过对象方法调用时,this 指向应该是最明晰的了。与其他面向对象语言的this 行为相同,指向该方法的调用者。
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = fn; function fn() { return this.name + " " + this.age } const p = new Person("Wango", 24); console.log(p); // Person { name: "Wango", age: 24 } console.log(p.sayName()); // Wango 24
5.1 通过【】调用对象方法
通常,我们对于对象方法是通过. 语法调用,但通过[] 也可以调用对象方法,在这种情况下的this 指向常常会被我们混淆、忽略。
function fn() { console.log(this); } const arr = [fn, 1]; arr[0](); // [Function: fn, 1] function fn2() { arguments[0](); } fn2(fn, 1); // [Arguments] { "0": [Function: fn], "1": 1 }
在上例中,无论是数组还是伪数组,其本质上都是对象,在通过 [] 获取函数元素并调用的时候,会改变函数中的this 指向,this 指向这个数组或伪数组,与对象调用函数的行为一致。
6.0 通过call、apply调用function fn() { console.log(this.name); } const author = { name: "Wango" } fn.call(author); // Wango
这似乎与this 永远指向调用者相违背,但一旦我们明白了call函数的实现机制就会明白,这不仅不是违背,反而是佐证。对call 、apply 、bind 下面截取call 简要说明。
// 保存一个全局变量作为默认值 const root = this; Function.prototype.myCall = function(context, ...args) { if (typeof context === "object") { // 如果参数是null,使用全局变量 context = context || root; } else { // 参数不是对象的创建一个空对象 context = Object.create(null); } // 使用Symbol创建唯一值作为函数名 let fn = Symbol(); context[fn] = this; context[fn](...args); delete context[fn]; }
call 函数最核心的实现在于context[fn] = this; 和context[fn](...args); 这两行。实际上就是将没有函数调用者的普通函数挂载到指定的对象上,这时this 指向与对象调用方法的一致。而delete context[fn]; 是在调用后立即解除对象与函数之间的关联。
7.0 严格模式下的不同表现
7.1 this强制转型
使用函数的apply() 或call() 方法时,在非严格模式下null 或undefined 值会被强制转型为全局对象。在严格模式下,则始终以指定值作为函数this 的值,无论指定的是什么值。这也是为何在严格模式下,自执行函数的this 不再指向window ,而是指向undefined 的根本原因。
// 定义一个全局变量 color = "red"; function displayColor() { console.log(this.color); } // 在非严格模式下使用call修改this指向,并指定null,或undefined, displayColor.call(null); displayColor.call(); // red
// 修改指向无效,传入null或undefined被转换为了window
实际上,我们也可以将自执行函数,如fn() ,看作是fn.call() 的语法糖,在普通模式下,第一个参数默认为undefined ,但被强制转换为window 。这也就解释了为何所有自执行函数中this 都指向window 但无法通过window 调用的问题(函数在call 函数中挂载到window 对象上,执行后被立即删除,所以无法再次通过window 访问)。
apply() 或call() 方法在严格模式下传入简单数据类型作为第一个参数时,该简单数据类型会被转换为相应的包装类,而非严格模式不会如此转换。function foo() { console.log(this); } foo.call(); // Window {} foo.call(2); // Number {2} function foo() { console.log(this); } foo.call(); // undefined foo.call(2); // 2
8.0 箭头函数的this指向
在箭头函数中, this 引用的是定义箭头函数的上下文。即箭头函数中的this 不会随着函数调用方式的改变而改变。
function Person(name) { this.name = name; this.getName = () => console.log(this.name); } const p = new Person("Wango"); p.getName(); // Wango const getName = p.getName; getName(); // Wango getName.call({name: "Lily"}); // Wango