阅读 987

你以为this指向哪儿(一篇到位,不留死角)

this的误解

1.this默认指向函数自己。

--任何情况下,this都不会默认指向函数自己,除非使用bind绑定的方式修改this为函数自己。

2.this指向函数作用域或上下文对象。

--需要明确,任何情况下,this都不默认指向函数的词法作用域或上下文对象,作用域或者说上下文对象确实与对象类似,可见的标识符都是其属性,但是该对象只存在于js引擎内部,无法在js环境下被访问。

this是什么

本质上,作用域工作模型分两种,一种是词法作用域,一种是动态作用域。

1.词法作用域: 词法作用域指的是在词法阶段产生的作用域,由书写者在写代码时所写的变量及作用域的位置所决定。引擎根据这些位置信息来查找标识符即变量的位置。 例如:无论函数在哪里、如何被调用,它的词法作用域都只由被声明时所处的位置决定。

2.动态作用域: 动态作用域是一个在运行时被动态确定的形式,而不是在静态时被确定。动态作用域不关心函数与作用域如何嵌套或何处声明,只关心它们在何处调用,也就是说。它的作用域链是基于调用栈而非作用域嵌套。 例:

function foo(){ 
    console.log(a); 
} 
function bar(){ 
    var a=3; 
    foo(); 
} 
var a=2; 
bar(); 
复制代码

如果是词法作用域,根据作用域规则,最终打印为2; 可是动态作用域会顺着调用栈去寻找变量,所以打印结果为3。

js的作用域规则属于词法作用域规则。

而this的机制与动态作用域的机制相近。this在函数运行时绑定,不在编写时绑定,其上下文取决于调用时的条件。this绑定与函数声明位置无关,取决于函数调用方式。

当一个函数被调用时,创建一个活动记录(也称执行上下文对象),此记录对象包含函数调用栈、调用方式、传入参数等信息,this是这个记录的一个属性。

调用栈

调用栈,其实就是函数的调用链,而当前函数的调用位置就在调用栈的倒数第二个位置(浏览器开发者工具中,给某函数第一行打断点debugger,运行时,可以展示调用列表call stack) 。 示例:

//全局作用域下

function func(val) {
    if(val <= 0) return;
    console.log(val);
    func(val-1);
}
func(5);
复制代码

执行栈用来存储运行时的执行环境。当然,栈遵循先进后出的规则。

上面代码的执行栈如下: 执行创建时:创建全局执行环境 => func(5) => func(4) => func(3) => func(2) => func(1)。

执行完毕销毁时:func(1) => func(2) => func(3) => func(4) => func(5) => 创建全局执行环境。

this的绑定规则(上面的可以完全不记,只要这部分牢记,就完全够用了)

1.默认绑定

产生于独立函数调用时,可以理解为无法应用其他规则时的默认规则。默认绑定下的this在非严格模式的情况下,默认指向全局的window对象,而在严格模式的情况下,则指向undefined。 示例:直接调用函数本身就是默认绑定

function func(){
	console.log('this',this);
}
func();//this,Window...
复制代码
function func(){
    'use strict'
	console.log('this',this);
}
func();//this,undefined
复制代码

ps1:以下规则,都是以函数环境为前提的,也就是说,this是放在函数体内执行的。在非函数环境下,也就是浏览器的全局作用域下,不论是否严格模式,this将一直指向window。一个冷知识:浏览器环境下的全局对象是window,其实除此之外还有一个特别的关键字,globalThis,在浏览器环境下打印该对象,指向window。 (ps1的观点要感谢@茹挺进大佬的特别指出。)

//全局作用域下
console.log(this);//window
复制代码
//全局作用域下
'use strict'
console.log(this);//window
复制代码
function func(){
    console.log(globalThis);
}
func();//window
复制代码

ps2: this所在的词法作用域在编写或声明时添加了"use strict",那么,运行时this指向undefined,但是,如果this所在的函数作用域中并未添加"use strict",而运行或调用该函数的词法作用域里有添加,那么也不影响,依然指向window。

function func(){
    'use strict'
	console.log('this',this);
}
func();//this,undefined
复制代码
function func(){
	console.log('this',this);
}
function bar(){
    'use strict'
    func();
}
bar();//this,window
复制代码

ps3:对于JS代码中没有写执行主体的情况下,非严格模式默认都是window执行的,所以this指向的是window,但是在严格模式下,若没有写执行主体,this指向是undefined;

2.隐式绑定

判断调用位置是否有上下文对象或者说是否有执行主体。简单说,一个对象调用了它所"拥有"的方法,那么,这个方法中的this将指向这个对象(对象属性引用链中只有上一层或者说最后一层才在调用位置中起作用,例:a.b.c.func(),func中的this只会指向c对象)。

var obj = {
    name:'myself',
    func:function (){
        console.log(this.name);
    }
}
obj.func();//myself
复制代码
函数方法并不属于对象

说到对象与其包含的函数方法的关系,通常人们一提到方法,就会认为这个函数属于一个对象 ,这是一个误解,函数永远不会属于某个对象,尽管它是对象的方法。其中存在的关系只是引用关系。 示例1:

//在对象的属性上声明一个函数
var obj = {
    foo:function func(){}
}
复制代码

示例2:

//独立声明一个函数然后用对象的属性引用
function func(){}
var obj = {
    foo:func
}
复制代码

上述两个例子效果是一样的,没有任何本质上的区别,很明显,函数属于它被声明时所在的作用域;我们都知道函数本质上是被存储在堆内存中,而函数的引用地址被存放在栈内存中方便我们取用,那么实际上对象中的属性持有的只是存在栈内存里函数的地址引用。

如果非要把持有引用地址当成一种属于关系的话,一个函数的地址可以被无数变量引用持有,那么这所有的变量都算是拥有这个函数,然而,属于关系是唯一的,所以该观点并不成立。

隐式丢失,即间接引用

示例1:

var b = {
    func:function(){}
}
var a=b.func;  
a();
复制代码

示例2:

var b = {
    func:function(){}
}
function foo(fn){
    fn();
}
foo(b.func)
复制代码

这两种情况下,this指向丢失(不指向对象),而原理在上面的”函数方法并不属于对象“里已经揭露,在这里,不论是a还是fn(而参数传递其实就是一种隐式赋值,传入函数也是),拿到的都只是函数的引用地址。

我们修改下上面的两个示例就一目了然了。

示例1:

function bar(){}
var b = {
    func:bar
}
var a=b.func; //相当于  var a=bar;
a();
复制代码

示例2:

function bar(){}
var b = {
    func:bar
}
function foo(fn){
    fn();
}
foo(b.func) //相当于foo(bar);
复制代码
3.显式绑定

隐式绑定中,方法执行时,对象内部包含一个指向函数的属性,通过这个属性间接引用函数,从而实现this绑定。

显式绑定也是如此,通过call,apply等方法,实现this的强制绑定(如果输入字符串、布尔、数字等类型变量当做this绑定对象,那么这些原始类型会被转为对象类型,如new String,new Boolean,new Number,这种行为叫装箱)。 绑定示例1:

var a = 1;
function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
func.apply(obj);//0
复制代码

绑定示例2:

var a = 1;
function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
func.call(obj);//0
复制代码

然而这依然无法解决可能丢失绑定的问题(比如处理回调函数,由于使用call、apply就会直接调用,而回调函数的调用无法人为介入控制所以回调函数上用不上call、apply)。

示例代码:

var a = 1;
function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
setTimeout(func.call(obj),1000);//立即执行了,无法满足延迟执行的需求
复制代码
显式绑定中的硬绑定

bind是硬绑定,通过使用bind方法的硬绑定处理,将回调函数进行包装,而得到的新函数在被使用时不会丢失绑定(利用了柯理化技术,柯理化技术依托于闭包)。

示例:

var a = 1;
function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
var newFunc = func.bind(obj);
setTimeout(newFunc,1000);//延迟1秒后打印0
复制代码
显式绑定中的软绑定

硬绑定降低了函数的灵活性,无法再使用隐式绑定或显式绑定修改this。

示例:

function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
var o = {
 a:2
}
var newFunc = func.bind(obj);
newFunc.apply(o);//0
复制代码

为了解决灵活性的问题,我们可以在硬绑定的原理基础上尝试shim一个新的绑定方式---软绑定。

示例:

Function.prototype.softBind = function(self){
    var func = this;
    var oldArg = [...arguments].slice(1)
    return function (){
        var newArgs = oldArg.concat([...arguments]);
        var _this = (!this || this === window) ? self : this;
        func.apply(_this,newArgs)
    }
}
function func(){
    console.log(this.a);
}
var obj = {
    a:0
}
var o = {
 a:2
}
var newFunc = func.softBind(obj);
newFunc();//0
newFunc.apply(o);//2
复制代码

核心代码:

var _this = (!this || this === window)?self:this;
//如果this绑定到全局或者undefined时,那么就保持包装函数softBind被调用时的绑定,否则修改this绑定到当前的新this。
复制代码

ps:js的许多内置函数都提供了可选参数,用来实现绑定上下文对象,例:数组的forEach、map、filter等方法,第一个参数为回调函数,第二个为将绑定的上下文对象。

4.new绑定

传统语言中,构造函数是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。而js中的所谓"构造函数"其实只是普通的函数,它们不属于某个类,也不会实例化一个类。实际上js中并不存在构造函数,只有对于函数的构造调用。 使用new调用函数(构造调用) 时,

  • 执行函数;
  • 创建一个全新对象(若未返回其他对象时,那么new表达式中的函数调用会自动返回这个新对象,若返回了其他对象,则this将绑定在返回的对象上);
  • 新对象会被执行原型连接 ;
  • 新对象会绑定到函数调用的this。
function func(name){
    this.name = name;
    this.printName = function(){
        console.log(this.name);
    }
}
var instance = new func('myself');
instance.printName();//myself
复制代码
function func(name){
    this.name = name;
    this.printName = function(){
        console.log(this.name);
    }
    return {
        name:'yourself',
        printName:function(){
            console.log(this.name);
        }
    }
}
var instance = new func('myself');
instance.printName();//yourself
复制代码
优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

隐式绑定 > 默认绑定
var name = window;
function func(){
    console.log(this.name);
}
var obj = {
    name:'obj',
    printName:func
}
obj.printName();//obj
复制代码
显式绑定 > 隐式绑定
var obj = {
    name:'obj',
    printName:function (){
        console.log(this.name);
    }
}
var other = {
    name:'other'
}
obj.printName.apply(other);//other
obj.printName.call(other);//other
复制代码
new绑定 > 显式绑定
function func(name){
    this.name = name;
}
var obj = {};

var foo = func.bind(obj);
foo('obj');
console.log(obj.name);//obj

var bar = new foo('instance');
console.log(obj.name);//obj
console.log(bar.name);//instance
复制代码
箭头函数this绑定

根据该函数所在词法作用域决定,简单来说,箭头函数中的this绑定继承于该函数所在作用域中this的绑定。

var name = 'window';
var obj = {
    name:'obj',
    printName:()=>{
        console.log(this.name);
    }
}
obj.printName();//window
复制代码

箭头函数没有自己的this,所以使用bind、apply、call无法修改其this指向,其this依然指向声明时继承的this。

var name = 'window';
var printName = () => {
        console.log(this.name);
    }
var instance = {
    name:'instance'
}
var callIns = printName.bind(instance);
callIns();//'window'

printName.apply(instance);//'window'

printName.call(instance);//'window'
复制代码

虽然bind不能修改其this指向,但是依然可以实现预参数的效果;而apply与call的参数传递也是生效的。

var func = (param) => {
    console.log(this);
    console.log(param);
}
var obj = {}
var foo = func.bind(obj,'hellow');
foo();
//window
//'hellow'
复制代码

ps:箭头函数不只没有自己this,也没有arguments对象。

var func = (param) => {
    console.log('arguments',arguments);
}
func('param');//Error,arguments is not defined
复制代码

写在最后

需要声明的一点是,我不是一个教授者,我只是一个分享者、一个讨论者、一个学习者,有不同的意见或新的想法,提出来,我们一起研究。分享的同时,并不只是被分享者在学习进步,分享者亦是。

知识遍地,拾到了就是你的。

既然有用,不妨点赞,让更多的人了解、学习并提升。

ps:我欣赏善意的沟通,当然,恶意的攻击也不是不可以,拿出可靠的依据让我服气,不要哗众取宠,缺乏内涵。