JS 基础:神奇的 this
前言
误以为 this 指向函数自身
根据 this
的英语语法,很容易将函数中出现的 this 理解为函数自身。在 javascript
当中函数作为一等公民,确实可以在调用的时候将属性值存储起来。但是如果使用方法不对,就会发生与实际预期不一致的情况。具体情况,请看下面代码:
function fn(num) {
this.count++;
}
fn.count = 0;
for (var i = 0; i < 3; i++) {
fn(i);
}
console.log(fn.count); // 0
如果 fn
函数里面的 this
指向自身函数,那么 count
属性的属性值就应该产生变化,但实际上却是纹丝不动。对于这个问题,有些人会利用作用域来解决,比如这么写:
var data = {
count: 0,
};
function fn(num) {
data.count++;
}
for (var i = 0; i < 3; i++) {
fn(i);
}
console.log(data.count); //3
又或者更直接的这么写:
function fn(num) {
fn.count++;
}
fn.count = 0;
for (var i = 0; i < 3; i++) {
fn(i);
}
console.log(fn.count); //3
虽然这两种方式都输出了正确的结果,但是却避开了 this
到底绑定在哪里的问题。如果对一个事物的工作原理不清晰,就往往会产生头痛治头,脚痛治脚的问题,从而导致代码变得的丑陋,而且维护性也会变得很差。
实际上,this 提供了一种更优雅的方法来隐式'传递'一个对象的引用,因此可以将 API 设计得更加简洁并且易于复用。
绑定规则
默认绑定规则
无论是 全局环境
还是 函数独立调用(包括嵌套函数,IIFE,闭包等)
,this
默认绑定到 window
。在严格模式下,会将函数体里的 this
默认绑定为 undefined
。以避免全局变量的污染。
全局环境
console.log(this === window); //true
函数独立调用
function fn() {
console.log(window === this); //浏览器环境
}
fn(); //true
函数 fn
是直接在全局作用域下调用的,没有带其他任何修饰,这种情况下,函数调用的时候使用了 this
的默认绑定,指向了全局对象。
fn
函数中的 this
指向了全局变量,所以 this.count++
相当于 window.count++
(浏览器环境下),当然不会对 fn
函数的 count
属性产生影响。
嵌套函数
//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
a: 2,
foo: function () {
function test() {
console.log(this.a);
}
test();
},
};
obj.foo(); //0
IIFE(立即执行函数)
var a = 0;
function foo() {
(function test() {
console.log(this.a);
})();
}
var obj = {
a: 2,
foo: foo,
};
obj.foo(); //0
//等价于上例
var a = 0;
var obj = {
a: 2,
foo: function () {
function test() {
console.log(this.a);
}
test();
},
};
obj.foo(); //0
闭包
var a = 0;
function foo() {
function test() {
console.log(this.a);
}
return test;
}
var obj = {
a: 2,
foo: foo,
};
obj.foo()(); //0
严格模式
'use strict';
function F() {
this.a = 1; // 这种指向全局的 this 不对
}
F();
//Uncaught TypeError: Cannot set property 'a' of undefined
严格模式下在 函数体
里的 this
不允许指向全局对象。
隐式绑定规则
如果函数在以对象为上下文进行调用,那么 this
会绑定到调用这个函数的对象:
var obj = {
a: 1,
fn: function () {
console.log(this.a);
},
};
obj.fn(); //1
即使函数声明不在对象当中,this
指向仍会产生变化:
function fn() {
console.log(this.a);
}
var obj = {
a: 1,
fn: fn,
};
obj.fn(); //1
由此可见,this
的绑定,不与函数定义的位置有关,而是与调用者和调用方式有关。
在隐式的绑定规则下,有一些特殊的地方,需要注意:
多层对象调用
在多层对象引用下,this
指向的是调用的函数的那个对象。
function fn() {
console.log(this.a);
}
var obj3 = {
a: 3,
fn: fn,
};
var obj2 = {
a: 2,
obj3: obj3,
};
var obj = {
a: 1,
obj2: obj2,
};
obj.obj2.obj3.fn(); //3
隐式丢失
[函数别名]
var a = 0;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar(); //0
//等价于
var a = 0;
var bar = function foo() {
console.log(this.a);
};
bar(); //0
虽然 bar
引用了 obj.foo
,但是函数的调用方式,仍是不带任何修饰的,所以 this
还是绑定在了 window
上。
[参数传递]
var a = 0;
function foo() {
console.log(this.a);
}
function bar(fn) {
fn();
}
var obj = {
a: 2,
foo: foo,
};
//把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo); //0
//等价于
var a = 0;
function bar(fn) {
fn();
}
bar(function foo() {
console.log(this.a);
});
[内置函数]
var a = 0;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
setTimeout(obj.foo, 100); //0
//等价于
var a = 0;
setTimeout(function foo() {
console.log(this.a);
}, 100); //0
setTimeout
是 window
的方法,setTimeout
在调用传入函数的时候,如果这个函数没有指定了的 this
,那么它会做一个隐式的操作 —- 自动地注入全局上下文 window
。
隐式绑定 this
不是一种很推荐的方式,因为很有可能就发生丢失的情况,如果业务当中对 this
的绑定有要求,建议还是使用显示绑定的方式。
显式绑定规则
显示绑定就是利用函数原型上的 apply
与 call
方法来对 this
进行绑定。用法就是把想要绑定的对象作为第一个参数传进去。如果传入了一个原始值(字符串,布尔类型,数字类型),来当做 this
的绑定对象,这个原始值转换成它的对象形式。
如果你把 null
或者 undefined
作为 this
的绑定对象传入 call/apply/bind
,这些值会在调用时被忽略,实际应用的是默认绑定规则。
function fn() {
console.log(this);
}
var obj = {};
fn.call(obj); //{}
有些时候会想将函数的 this
绑定在某个对象上,但是不需要立即调用,这样的话,直接利用 call
或者 apply
是无法做的。
function fn() {
console.log(this);
}
function bind(fn) {
fn();
}
var obj = {
fn: fn,
};
bind.call(obj, fn); //window
上面这个例子,看似好像可以,但实际上是 bind
函数的 this
绑定到了 obj
这个对象,但是 fn
仍然是没有任何修饰的调用,所以 fn
仍然是默认的绑定方式。
function fn() {
console.log(this);
}
function bind(fn, obj) {
return function () {
fn.apply(obj, arguments);
};
}
var obj = {
fn: fn,
};
var fun = bind(fn, obj);
fun(); //obj
这样调用,就可以将灵活多变的 this
,牢牢的控制住了,因为 fn
的调用方式为 apply
调用。所以,this
就被绑定在传入的 obj
对象上,在 ES5
当中,函数的原型方法上多了一个 bind
,效果与上面的函数基本一致。
apply/call/bind 实现
注: call 性能比 apply 好。
Function.prototype.apply2 = function (context = window) {
if (this === Function.prototype) {
return undefined; // 用于防止 Function.prototype.apply2() 直接调用
}
const fn = Symbol();
context[fn] = this;
let result;
//let args = [...arguments].slice(1); // call 则仅仅是参数不同
let args = arguments[1];
// 判断是否有第二个参数
if (args) {
result = context[fn](...args);
} else {
result = context[fn]();
}
delete context[fn];
return result;
};
Function.prototype.bind2 = function (content) {
if (this === Function.prototype) {
throw new TypeError('Error');
}
let fn = this;
let args = [...arguments].slice(1);
let resFn = function () {
return fn.apply(this instanceof resFn ? this : content, args.concat(...arguments));
};
function tmp() {}
tmp.prototype = this.prototype;
resFn.prototype = new tmp();
return resFn;
};
其他
new
new
是一个被很多人误解的一个关键字,但实际上 javascript
的 new
与传统面向对象的语言完全不同。 个人把 new
理解为一种特殊的函数调用,当使用 new
关键字来调用函数的时候,会执行下面操作:
- 创建一个全新的对象;
- 将构造函数的作用域赋给新对象(
this
就指向了这个新对象),将新对象的__proto__
指向构造函数的prototype
,设置新对象的内部属性,可访问性等; - 执行构造函数中的代码(为这个新对象添加属性);
- 如果构造函数返回值为基本类型或者为
this
又或者不返回任何值,那么将会返回这个创建的新对象;如果返回了一个对象,那么则会返回这个对象。
new 实现
function New(func) {
var res = {};
if (func.prototype !== null) {
res.__proto__ = func.prototype;
}
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
if ((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {
return ret;
}
return res;
}
var obj = New(A, 1, 2);
// equals to
var obj = new A(1, 2);
故构造函数中的 this
指向构造函数创建的对象实例。
function fn(a) {
this.a = a;
}
fn.prototype.hi = function () {
console.log('hi');
};
var obj = new fn(2);
console.log(obj);
class
类和模块的内部,默认就是 严格模式
。上文说到在严格模式下,会将 this
默认绑定为 undefined
。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
可以将 this
绑定到实例上:
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
class 实现
function inherit(subType, superType) {
subType.prototype = Object.create(superType.prototype, {
constructor: {
enumerable: false,
configurable: true,
writable: true,
value: subType,
},
});
Object.setPrototypeOf(subType, superType);
}
//ES6 的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式,通过 Object.create 方法创造一个空对象,并将这个空对象继承 Object.create 方法的参数,再让子类(subType)的原型对象等于这个空对象,就可以实现子类实例的原型等于这个空对象,而这个空对象的原型又等于父类原型对象(superType.prototype)的继承关系
//而 Object.create 支持第二个参数,即给生成的空对象定义属性和属性描述符/访问器描述符,我们可以给这个空对象定义一个 constructor 属性更加符合默认的继承行为,同时它是不可枚举的内部属性(enumerable:false)
//而 ES6 的 class 允许子类继承父类的静态方法和静态属性,而普通的寄生组合式继承只能做到实例与实例之间的继承,对于类与类之间的继承需要额外定义方法,这里使用 Object.setPrototypeOf 将 superType 设置为 subType 的原型,从而能够从父类中继承静态方法和静态属性
寄生组合式继承
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
箭头函数
箭头函数本身不创建 this
,在它声明时可以捕获别人的 this
供自己使用。this
一旦被捕获,以后将不再变化,即不会被 call、apply 或 bind
影响。
传统写法:
function fn() {
var _this = this;
setTimeout(function () {
console.log(_this.a);
}, 100);
}
var obj = {
a: 2,
};
fn.call(obj); //2
箭头函数写法:
function fn() {
setTimeout(() => {
//this 来源于 fn 函数的作用域
console.log(this.a);
}, 100);
}
var obj = {
a: 2,
};
fn.call(obj); //2
如果用 apply
显式绑定:
var a = 1;
let obj = { a: 2 };
let fn = () => {
console.log(this.a);
};
fn.apply(obj);
事件函数
如果是在事件函数当中,this
的绑定是指向触发事件的 DOM
元素的,
$('body')[0].addEventListener(
'click',
function () {
console.log(this);
},
false,
);
点击 body
元素之后,控制台则会显示 body
元素。
小结
如果想判断一个函数的 this
绑定在哪里,首先是找到函数的调用位置,之后是按照规则来判断。
- 如果函数调用时没有任何修饰条件,那么在严格模式下则会绑定到
undefined
,非严格模式下会绑定到全局window
。 - 如果是用对象做上下文,来对函数进行调用,那么则会绑定到调用的这个对象上。
- 如果是用
call
或者apply
方法来进行调用的,则会绑定到第一个传入参数上。 - 如果是使用
new
关键字来调用函数的,则会绑定到新创建的那个对象上. - 如果是在事件函数内,则会绑定到触发事件的那个
DOM
元素上。 - 绑定优先级:
显式绑定 > 隐式绑定 > 默认绑定, new 绑定 > 隐式绑定 > 默认绑定