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

this的误解

1.this默认指向函数本身。数组

--任何状况下,this都不会默认指向函数本身,除非使用bind绑定的方式修改this为函数本身。浏览器

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

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

this是什么

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

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

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

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

若是是词法做用域,根据做用域规则,最终打印为2; 但是动态做用域会顺着调用栈去寻找变量,因此打印结果为3。学习

js的做用域规则属于词法做用域规则。ui

而this的机制与动态做用域的机制相近。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:我欣赏善意的沟通,固然,恶意的攻击也不是不能够,拿出可靠的依据让我服气,不要哗众取宠,缺少内涵。

相关文章
相关标签/搜索