一. 引言
在js中,this存在的意义就是为了让对象中的函数有能力访问对象自己的属性,同时this可以减少上下文参数的传递,显著提升代码质量。
观察以下两段代码;
通过运行输出俩段代码的值都是一样的输出Hello,I amTOM,但是下面相比上面在代码上在相对简洁,减少了函数之间的参数传递,比较优雅。虽然this使用起来很优雅,但是学起来也很简单。
首先需知道this 是一个代词,在js 中永远代指某一个域,且 this 只存在于域中才有意义(写在对象中无意义),this 在全局下指向的是 window。
二. this的指向
搞清楚this的指向只需要理解四个绑定原则:默认绑定,隐式绑定,显示绑定和new绑定。
2.1 默认绑定
默认绑定也就是当函数独立调用时,this 指向 windwow(也就是全局)。
独立调用也就是不作为对象的方法被调用,非独立调用就是通过对象.函数名()调用。
function foo() {
let person = {
name: '阿美',
age: 18
}
console.log(this);
}
foo()
如上代码,this是在foo函数作用域里的,那么this会指向什么呢?结果如下:指向widow。
function foo() {
let a = 1
function bar() {
console.log(this);
}
bar()
}
foo()
再看这段代码,在foo函数里面调用bar函数,this是在bar函数作用域里面,结果如下:还是指向window。
这就说明this的指向与它在哪个函数作用域里无关,与它是怎么被调用的有关。
2.2 隐式绑定
隐式绑定就是当函数的引用有上下文对象(当函数被某一个对象所拥有且调用),this 指向该上下文对象。
function foo() {
console.log(this); //{ a: 1, fun: [Function: foo] }
}
const obj = {
a: 1,
fun: foo
}
obj.fun() //非独立调用,是由对象 obj 调用
在对象obj里面的一个属性为fun,值为foo函数体(没有调用,foo()才是调用)。所以当前foo函数就被obj这个对象所拥有且调用(如果不调用的话这个函数就跟没有一样),this就指向这个obj对象。
当这种情况下呢?obj拥有并调用foo,而obj又被obj2拥有,那么这个this指向谁呢?通过编译输出结果为 1,也就是 obj 中的 a 值,则说明this指向的是obj对象,即 obj2 不拥有foo函数(也称隐式丢失),也就是当函数的引用有一连串的上下文对象,this 指向最近的那个对象。
2.3 显示绑定
显示绑定:call apply bind显示地将函数的 this 绑定到一个对象上。
function foo() {
console.log(this.a);
}
var obj = {
a: 1
}
foo.call(obj) //将 foo函数强行绑定到 obj对象上
使用call内置方法可以将 foo函数强行绑定到 obj对象上,与此还有apply bind也可以。分别为“foo.apply(obj)”, “先 let bar = foo.bind(obj); 再 bar();”,bind有点不一样,它则需要用一个变量接收然后再用该变量调用才能绑定。
当函数有参数应该如何调用呢?如下;
2.4 new 绑定
new 绑定:this 指向实例对象
function Person() {
this.name = '阿炜'
this.age = 18
}
let p = new Person()
console.log(p); // Person { name: '阿炜', age: 18 }
分析以上代码,容易得出:当使用 new调用构造函数创建一个实例对象时,构造中的函数会自动绑定在该实例对象上。
所以 new的执行过程究竟是怎么样的呢?
- 创建一个空对象
- 将构造函数里的this指向该对象
- 正常运行构造函数里的代码
- 该对象的隐式原型等于构造函数的显示原型(
obj.__proto__=Person.prototype) - 返回该对象
注意::在返回该对象前,会判断原构造函数本身有没有return返回语句,如果本身有return且返回的是引用类型的数据(数组,对象等),则返回原构造函数本身的值,否则返回创建的对象。
所以new的执行过程应该是:
三. 箭头函数
为什么要讲箭头函数呢?
因为箭头函数里面没有this,它的this与它最近一层非箭头函数环境中的this保持一致。
const word = 'window hello'
const obj = {
word: 'obj hello',
fn: function () {
setTimeout(() => {
console.log(this.word);
})
}
}
const globalFn = obj.fn
globalFn() //输出 undefined
const obj2 = {
word: 'obj2 hello',
fn: obj.fn
}
obj2.fn() //输出 obj2 hello
为什么这样输出呢?
因为箭头函数中的this是与它最近一层非箭头函数环境中的this保持一致,也就是与对象的obj.fn中this保持一致,而obj.fn又赋值给了globalFn(obj.fn=globalFn),而对于globalFn来说,是由window调用,所以obj.Fn的this指向window,箭头函数中this又与它保持一致,所以也指向了window。
但是既然指向window,为啥不是输出window hello呢?
这是因为 word 是一个在全局作用域中定义的变量,而不是全局对象的属性。在全局作用域中,this 通常不指向变量,而是指向全局对象(如 window)。由于 word 是一个变量,而不是 this 的属性,所以 this.word 是 undefined。
下面obj2.fn()同理,调用fn时fn中的this指向obj2,内部函数中this与它最近一层非箭头函数环境中的this保持一致,所以也指向了obj2,此时输出obj2 hello。
因为箭头函数中没有this,构造函数时由 new调用,而new执行需要将构造函数里的this指向obj,所以箭头函数不能当作构造函数来使用。
四. 手敲一个call源码
call函数可以将一个函数强行绑定到一个对象上,如下;
function foo(x) {
console.log(this.a, x); // 输出 1 2
}
const obj = {
a: 1
}
foo.call(obj, 2)
call的原理其实就是前面提到的隐式绑定,当要将一个函数的this指向一个对象时,就让该对象拥有且调用该函数就可以了。因为call是在Function.prototype上的,所以我们在Function.prototype新增一个手敲的源码。因为call可以传很多参数,所以我们用...args来作为形参。如下;
Function.prototype.myCall = function (...args) {
const context = args[0]
const arg = args.slice(1)
context.fn = this
const res = context.fn(...arg)
delete context.fn
return res
}
总的来就是将foo引用到 obj 上,让 obj 调用foo,移除 obj 上的 foo。
详细如下: