前言
在 JavaScript 中,this
关键字是一个非常重要且容易让人困惑的概念,它的值在不同的执行上下文中会有所不同,很多初学者(包括小管),刚开始接触的时候总会一脸懵,今天我们就来简单谈谈this
的“前世今生”
为什么要有this
提到this
,我们必须要知道为什么要有this。我们先来看一段代码。
function identify(context){
return context.name.toUpperCase()
}
function speak(context){
var greetting = 'Hello, I am '+ identify(context)
console.log(greetting)
}
var me ={
name:'Tom'
}
speak(me)
在这段代码里面identify
和speak
函数都得携带一个context参数才能正常运行,当我们写了更多的需要携带参数的函数,就需要携带更多的参数,越写越混乱,而this
存在的意义就是可以自动地引用合适的上下文对象。不需要再显式地携带参数。
this
我们用一句话概括this:this 是一个代词,在js中永远代指某一个域,且this只存在于域中时,才有意义。
tip: js中的作用域有: 1.全局作用域 (this在全局下指向的是window) 2.函数作用域 3.块级作用域
this的指向
1. 默认绑定:
一句话先概况:
默认绑定:当函数是独立调用时,this
指向的是window
话不多说,先上代码
function foo(){
let person ={
name :'熊',
age : 6
}
console.log(this);
}
foo()
很明显,这里的this
存在于foo
这个函数的作用域里,是foo
的。那我们来执行这段代码
问:这段代码的执行结果是什么?(小管:我猜是执行结果是foo)
答:
很明显,猜错了,这里的this指向的就是node环境中的全局。也就是说,这个this指向了全局。
为什么呢?继续上代码
function foo(){
let person ={
name:'小管',
age:19
}
console.log(this);
}
function bar(){
let person ={
name :'熊',
age : 3
}
foo()
}
bar()
我们让foo这个函数在bar函数中调用,此时的执行结果会是什么呢?
不好意思,还是全局。
我们又要问为什么了,这里就不得不先提到一个概念:函数的独立调用和非独立调用。
function bar(){
console.log(this)
}
function foo(){
console.log(this)
}
const obj ={
a:1,
foo:foo
}
obj.foo()
bar()
- 独立调用:函数作为单独个体直接被调用,不依赖于某个特定对象,例如
bar()
。 - 非独立调用:函数作为对象的方法被调用,通过对象来触发,如
obj.foo()
。我们可以把函数看为一个小孩子,当“小孩子”被“家长”拉着调用,就叫非独立调用。
结合上面三个例子,我们发现,当函数被独立调用而不以对象方法的形式调用时,this就指向了window。没错,这就是默认绑定
2.隐式绑定
隐式绑定:当函数的引用有上下文对象时(当函数被一个对象拥有且调用时),this指向该上下文对象
什么意思呢,我们来看一段代码
function foo(){
console.log(this)
}
const obj ={
a:1,
foo:foo
}
obj.foo()
这里的foo
函数作为obj
中的对象调用,所以foo
中的this
就指向调用foo
的obj
。执行结果如下
再来一个
function foo(){
console.log(this)
}
const obj ={
a:1,
foo:foo
}
const obj2 ={
a:2,
obj:obj
}
obj2.obj.foo()
这里到底是obj2
调用的foo
还是obj
调用的foo
呢?
一句话:就近原则!也就是说这里的foo
就是被obj
调用了,this
也就指向obj
。
执行结果:
官方给这种情况取了一个名字:隐式丢失。
隐式丢失:当函数的引用有一连串的上下文对象,this指向最近的那个对象。
3.显式绑定
显式绑定 :call()
apply()
bind()
显示的将函数的this
绑定到一个对象上
看一段代码:
function foo(){
console.log(this)
}
var obj = {
a:1
}
foo()
想让foo绑定到obj上,但是又不想在obj中加上foo,我们就可以用call()
apply()
bind()
方法绑定,方法如下
- call()
function foo(){
console.log(this)
}
var obj = {
a:1
}
foo.call(obj)
这里的
call()
是在foo()
上的方法,挂载在Function.prototype
上,且第八行代码是call()
来调用foo()
当foo()
传入参数,就需要把参数传入call()
,如下
function foo(x,y){
console.log(this.a,x+y)
}
var obj = {
a:1
}
foo.call(obj,2,3)
- apply()
和
call()
的唯一区别就是传参的时候传入的是数组,如下
function foo(x,y){
console.log(this.a,x+y)
}
var obj = {
a:1
}
foo.apply(obj,[2,3])
- bind()
bind()
一定会返回一个函数体,必须将返回出的函数调用,且它收到参数时是零散的接收的,参数可以放在bind()
上,也可以放在返回出来的函数体上(注意:若bind()
和返回的函数体都携带参数时,先用bind()
上携带的参数,再用返回额函数体上携带的参数)
function foo(x,y){
console.log(this.a,x+y)
}
var obj = {
a:1
}
let bar = foo,bind(1)
bar(3)//打印出1,4
3.new绑定
我们先来聊聊new:
我们来创建一个函数
function Person(){
this.name ='小管'
this.age =20
}
let p= new Person()
console.log(p)
执行结果:
请注意,这是在Person()
这个函数本身没有返回值时的情况,一旦这个函数有了一个引用类型的数据时,new的执行结果就是Person()
返回出来的引用类型的数据
所以我们可以总结一下new的执行原理:
-
- 创建一个新对象
-
- 往这个新对象里加key
-
- 将函数的this绑定到这个对象中
-
- 此对象的隐式原型等于构造函数中的显示原型
-
- 返回这个对象(判断这个函数的值如果是引用类型,则采用)
function Person(){
this.name ='小管'
this.age =20
return [123]
}
let obj= new Person()
Person.call(obj)
obj.__proto__ = Person.prototype
return Person() instanceof Object ? Person() : obj
其实new中的绑定还是用的是this的绑定
4.箭头函数
先说结论:箭头函数中没有this
function foo(){
let bar = function() {
let baz =()=>{
let fn =()=>{
console.log(this)
}
fn()
}
baz()
}
bar()
}
foo()
易知,baz() 和 fn() 都是箭头函数,没有this,所以代码中的this是箭头函数外的非箭头函数的。
注意:
this
是谁的,和指向谁是不同的概念
那如果我们这么写一段代码呢
let Foo =()=>{
this.name='廖总'
}
let foo = new Foo()
我们想以Foo()为构造函数,new出一个实例对象,当我们运行时我们就会发现
Foo is not a constructor
Foo不是一个构造器
由此我们又可以得出一个结论: 箭头函数不能作为构造函数使用
手搓一个call
废话不多说,手搓一个call
function foo(x,y) {
console.log(this.a, x,y);
return x+y;
}
let obj = {
a:1,
}
foo.mycall(obj,1,2)
- 假设有一个mycall可以实现call的全部功能
function foo(x,y) {
console.log(this.a, x,y);
return x+y;
}
Function.prototype.mycall = function (){
}
let obj = {
a:1,
}
foo.mycall(obj,1,2)
-
- 先把
mycall
挂载在Function
的显示原型上,让所有的函数能访问到这个方法。
- 先把
function foo(x,y) {
console.log(this.a, x,y);
return x+y;
}
Function.prototype.mycall = function (...args) {
const context = args[0] || window
const arg = args.splice(1)
}
let obj = {
a:1,
}
foo.mycall(obj,1,2)
-
- 将
mycall
中携带的参数用...args
变为一个数组,且将数组中的第一个参数为this上下文
- 将
-
- 将剩下的参数,放入
arg
数组中
- 将剩下的参数,放入
function foo(x,y) {
console.log(this.a, x,y);
return x+y;
}
Function.prototype.mycall = function (...args) {
const context = args[0] || window
const arg = args.splice(1)
context.fn=this
}
let obj = {
a:1,
}
foo.mycall(obj,1,2)
-
- 在
context
上挂载一个fn
,因为mycall
是因为foo
触发的非独立调用,所以这里的this
就指向调用mycall
的函数foo
,也就是说,我们把函数foo
挂载到了context
身上。
- 在
function foo(x,y) {
console.log(this.a, x,y);
return x+y;
}
Function.prototype.mycall = function (...args) {
const context = args[0] || window
const arg = args.splice(1)
context.fn=this
const res = context.fn(...arg)
delete context.fn
return res
}
let obj = {
a:1,
}
foo.mycall(obj,1,2)
- 5.
context.fn
就代表foo
,再解构arg
数组,将解构后的参数传入context.fn
中,返回的结果保存到res
中 -
- 删除挂载到
context
身上的foo
- 删除挂载到
-
- 返回
res
- 返回
大功告成,恭喜你,打造了属于自己的mycall
结语
js中的this指向问题,谈这么多就够了。js之路道阻且长,还得好好努力啊!