this
很多人觉得this很迷,但实际上不是,只是没有了解到this指向的真正核心之处。
其实一般来讲,this指向只有三种情况:
- 对于普通函数而言,
this指向在运行时确定。 - 对于对象的方法而言,
this指向对象的实例。 - 对于箭头函数而言,
this在定义时确定。(实际上箭头函数不存在this)
普通函数中的 this
function foo() {
console.log(this);
}
foo(); // window
对于普通函数而言,如果不强制绑定的话,正常模式下,this指向全局对象,浏览器环境下是window,而Node环境下就是global。
对于严格模式,this指向undefined。
'use strict'
function foo() {
console.log(this);
}
foo(); // undefined
对象方法中的 this
let obj = {
a: 666,
foo() {
console.log(this.a);
}
}
obj.foo(); // 666
let f = obj.foo;
f(); // undefined
对于对象中的方法,实际上很容易确定this:谁调用就指向谁。上面的代码中,我们执行obj.foo()语句,也就是说,obj对象调用foo函数,因此,this指向对象obj,然后输出了obj.a的值。
如果我们把obj.foo赋值给一个变量,那么,由于函数是引用类型,因此,上述代码中,变量f实际上就是和obj.foo指向同一个函数。由于我们直接调用f(),而不是通过对象obj,因此,函数f中的this指向了全局对象,因此输出window.a。
箭头函数中的 this
对于箭头函数而言,有一种说法是箭头函数的this是在定义时确定的。实际上,这种方法并不准确,我们看看下面的代码:
var obj = {
foo: () => {
console.log(this);
},
};
obj.foo(); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
如果this是在定义时确定的,那么我们调用obj.foo()时,输出的应该是obj对象,而不是window对象。这样的话,我们可以知道,这种说法并不准确。
那箭头函数的this究竟是如何确定的呢?
实际上箭头函数是没有this的。
箭头函数中的this,实际上只是它上层函数作用域中的this罢了。
let a = 888;
let obj = {
a: 666,
foo() {
console.log(this);
return () => {
console.log(this.a);
}
},
};
let obj2 = {
a: 777,
};
obj.foo()(); // 666
obj2.foo = obj.foo;
obj2.foo()(); // 777
let f = obj.foo;
f()(); // undefined
当foo函数中的this不断更换时,箭头函数的this也在跟随着变化,并且跟外部指向相同的this。因此可以说明,箭头函数实际上是不存在自己的this,如果在箭头函数中有this,那么,这个this实际上就是它上层作用域的this。
this 总结
对于this的指向,实际上是这样的:
- 谁调用就指向谁
- 如果没有对象调用,普通执行,就指向全局
- 箭头函数没有
this,它的this是上层作用域的
手动绑定 this
在JavaScript中,支持显示绑定this的指向。
对于显示绑定this,提供了call、apply、bind三个API作为支持。
call 和 apply
之所以把call和apply放到一起讲,是因为它们作用基本一样,只是在调用是传参有点区别。
function foo(x, y) {
console.log(this.a, this.b, x, y);
}
let obj = {
a: 1,
b: 2,
};
foo.call(obj, 666, 777); // 1 2 666 777
foo.apply(obj, [666, 777]); // 1 2 666 777
可以看到,它们的第一个参数是this的指向,可以为null。
然后它们的唯一区别就是,call的参数是通过参数列表的形式传入的,而apply是通过数组的形式来给函数传参的。
实际上我们也可以自己实现这两个API,实现起来的非常简单:
// call
function.prototype.myCall = function(context, ...args) {
let _ctx = context || null;
let _args = args || [];
let _fn = Symbol();
_ctx[_fn] = this;
let result = _ctx[_fn](..._args);
_ctx[_fn] = null;
return result;
}
// apply
function.prototype.myApply = function(context, args) {
let _ctx = context || null;
let _args = args || [];
let _fn = Symbol();
_ctx[_fn] = this;
let result = _ctx[_fn](..._args);
_ctx[_fn] = null;
return result;
}
bind
通过使用bind,我们可以绑定this到某个函数上,并返回一个函数。跟call和apply不同,执行bind返回的是绑定了this的函数,而不会执行结果。
function foo(x, y) {
console.log(this.a, this.b, x, y);
}
let obj = {
a: 1,
b: 2,
foo
};
let fn = obj.foo;
fn.bind(obj)(666, 777); // 1 2 666 777
fn.bind(obj, 666, 777)(); // 1 2 666 777
fn(666, 777); // undefined undefined 666 777
另外,我们通过bind返回的函数,还可以作为一个构造函数。
function People(sex, name) {
this.sex = sex;
this.name = name;
}
let Boy = People.bind(null, '男');
let Girl = People.bind(null, '女');
let b = new Boy('Hello');
let g = new Gril('world');
console.log(b instanceof Boy && b instanceof People); // true
console.log(g instanceof Girl && g instanceof People); // true
实际上这就是使用了偏函数的知识,也称为柯里化。对于柯里化,我的理解就是,我们可以定义一个函数,对它不断地进行调用,可以分批地传入一些参数;与此同时,柯里化也可以提高代码的复用率。
比如我们定义两个数的加法:
function add(a, b) {
return a + b;
}
如果是三个数呢?
function add(a, b, c) {
return a + b + c;
}
如果是N个数,如果按照这样写,那么我们又要把代码重新写一遍了。这时候柯里化的好处就出现了,我们可以编写一个通用的函数A,然后通过传入一个参数去指定是多少个数的加法,然后返回这个函数B。我们可以分批给函数传入参数,直到参数的数量可以满足函数的执行。
function curryAdd(n) {
n = n || 2;
return function (...args) {
let _args = args || [];
let add = function(...args) {
_args.push(...args);
if (_args.length >= n) {
_args = _args.slice(0, n);
return _args.reduce((prev, cur) => prev + cur, 0);
} else {
return add;
}
};
if(_args.length >= n) {
_args = _args.slice(0, n);
return _args.reduce((prev, cur) => prev + cur, 0);
}
return add;
};
}
let add2 = curryAdd(2);
console.log(add2(2, 3)); // 5
let r = add2(2)(3);
console.log(r); // 5
let add5 = curryAdd(5);
// 因为我们规定了5个数值相加,因此,计算传入更多的参数,也是无效的。
console.log(add5(1)(1)(2)(2, 2, 2)); // 8
console.log(add5(1, 2, 3, 4, 5, 6)); // 15
对于函数的柯里化,我们一般可以构造一个生成器函数,把对应的函数作为函数传入,然后就可以控制这个函数的参数传入,当参数到达指定数量后便会执行。
function currying(fn, ...args) {
let _agrs = [].concat(args);
let len = fn.length;
return function curry(...args) {
_agrs.push(...args);
if(_agrs.length >= len) {
return fn.apply(this, _agrs);
}
return curry;
}
}
function add(a, b, c, d) {
return a + b + c + d;
}
let curryAdd = currying(add);
console.log(curryAdd(1,2)(3,4)); // 10
bind 实现
bind的实现主要在于两点:
- 绑定
this但不执行。 - 通过
bind绑定后的函数可以当做构造函数,并且保证原型链的正确。
针对第一点,可以通过返回一个函数,借用apply方法实现。
针对第二点,可以对返回的新函数原型通过Object.create继承原函数的原型。
Function.prototype.myBind = function(context, ...args) {
let fn = this;
let ctx = context || null;
let allAgrs = [].concat(args);
let fBound = function(...args) {
allAgrs = allAgrs.concat(args);
return fn.apply(this instanceof fn ? this : ctx, allAgrs);
};
fn.prototype && (fBound.prototype = Object.create(fn.prototype));
return fBound;
}