欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【THE LAST TIME】this:call、apply、bind

程序员文章站 2023-11-14 18:07:46
前言 The last time, I have learned 【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。 也是给自己的查缺补漏和技术分享。 欢迎大家多多评论指点吐槽。 系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见 "Nealyang/persona ......

前言

the last time, i have learned

【the last time】一直是我想写的一个系列,旨在厚积薄发,重温前端。

也是给自己的查缺补漏和技术分享。

欢迎大家多多评论指点吐槽。

【THE LAST TIME】this:call、apply、bind

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见nealyang/personalblog。目录皆为暂定

讲道理,这篇文章有些拿捏不好尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,但是往往这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。

the last time 系列

this

相信使用过 javascript 库做过开发的同学对 this 都不会陌生。虽然在开发中 this 是非常非常常见的,但是想真正吃透 this,其实还是有些不容易的。包括对于一些有经验的开发者来说,也都要驻足琢磨琢磨~ 包括想写清楚 this 呢,其实还得聊一聊 javascript 的作用域和词法

this 的误解一:this 指向他自己

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);

通过运行上面的代码我们可以看到,foo函数的确是被调用了十次,但是this.count似乎并没有加到foo.count上。也就是说,函数中的this.count并不是foo.count

this 的误解二:this 指向他的作用域

另一种对this的误解是它不知怎么的指向函数的作用域,其实从某种意义上来说他是正确的,但是从另一种意义上来说,这的确是一种误解。

明确的说,this不会以任何方式指向函数的词法作用域,作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说他是对的,但是javascript代码不能访问这个作用域“对象”,因为它是引擎内部的实现

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined

全局环境中的 this

既然是全局环境,我们当然需要去明确下宿主环境这个概念。简而言之,一门语言在运行的时候需要一个环境,而这个环境的就叫做宿主环境。对于 javascript 而言,宿主环境最为常见的就是 web 浏览器。

如上所说,我们也可以知道环境不是唯一的,也就是 javascript 代码不仅仅可以在浏览器中跑,也能在其他提供了宿主环境的程序里面跑。另一个最为常见的就是 node 了,同样作为宿主环境node 也有自己的 javascript 引擎:v8.

  • 浏览器中,在全局范围内,this 等价于 window 对象
  • 浏览器中,用 var 声明一个变量等价于给 this 或者 window 添加属性
  • 如果你在声明一个变量的时候没有使用var或者let(ecmascript 6),你就是在给全局的this添加或者改变属性值
  • 在 node 环境里,如果使用 repl 来执行程序,那么 this 就等于 global
  • 在 node 环境中,如果是执行一个 js 脚本,那么 this 并不指向 global 而是module.exports{}
  • 在node环境里,在全局范围内,如果你用repl执行一个脚本文件,用var声明一个变量并不会和在浏览器里面一样将这个变量添加给this
  • 如果你不是用repl执行脚本文件,而是直接执行代码,结果和在浏览器里面是一样的
  • node环境里,用repl运行脚本文件的时候,如果在声明变量的时候没有使用var或者let,这个变量会自动添加到global对象,但是不会自动添加给this对象。如果是直接执行代码,则会同时添加给globalthis

这一块代码比较简单,我们不用码说话,改为用图说话吧!

【THE LAST TIME】this:call、apply、bind

函数、方法中的 this

很多文章中会将函数和方法区分开,但是我觉得。。。没必要啊,咱就看谁点了如花这位菇凉就行

当一个函数被调用的时候,会建立一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。

函数中的 this 是多变的,但是规则是不变的。

你问这个函数:”老妹~ oh,不,函数!谁点的你?“

”是他!!!“

那么,this 就指向那个家伙!再学术化一些,所以!一般情况下!this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境。this 与声明无关!

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

记住上面说的,谁点的我!!! => foo() = windwo.foo(),所以其中this 执行的是 window 对象,自然而然的打印出来 2.

需要注意的是,对于严格模式来说,默认绑定全局对象是不合法的,this被置为undefined。

function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

虽然这位 xx 被点的多了。。。但是,我们只问点他的那个人,也就是 ojb2,所以 this.a输出的是 42.

注意,我这里的点!不是你想的那个点哦,是运行时~

构造函数中的 this

恩。。。这,就是从良了

还是如上文说到的,this,我们不看在哪定义,而是看运行时。所谓的构造函数,就是关键字new打头!

谁给我 new,我跟谁

其实内部完成了如下事情:

  • 一个新的对象会被创建
  • 这个新创建的对象会被接入原型链
  • 这个新创建的对象会被设置为函数调用的this绑定
  • 除非函数返回一个他自己的其他对象,这个被new调用的函数将自动返回一个新创建的对象
foo = "bar";
function testthis(){
  this.foo = 'foo';
}
console.log(this.foo);
new testthis();
console.log(this.foo);
console.log(new testthis().foo)//自行尝试

call、apply、bind 中的 this

恩。。。这就是被包了

在很多书中,call、apply、bind 被称之为 this 的强绑定。说白了,谁出力,我跟谁。那至于这三者的区别和实现以及原理呢,咱们下文说!

function dialogue () {
  console.log (`i am ${this.heroname}`);
}
const hero = {
  heroname: 'batman',
};
dialogue.call(hero)//i am batman

上面的dialogue.call(hero)等价于dialogue.apply(hero)``dialogue.bind(hero)().

其实也就是我明确的指定这个 this 是什么玩意儿!

箭头函数中的 this

箭头函数的 this 和 javascript 中的函数有些不同。箭头函数会永久地捕获 this值,阻止 apply或 call后续更改它。

let obj = {
  name: "nealyang",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); //   1 2
let func_ = func.bind(obj);
func_(1,2);//  1 2
func(1,2);//   1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);//  1 2

箭头函数内的 this值无法明确设置。此外,使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在创建时设置的 this值。

箭头函数也不能用作构造函数。因此,我们也不能在箭头函数内给 this设置属性。

class 中的 this

虽然 javascript 是否是一个面向对象的语言至今还存在一些争议。这里我们也不去争论。但是我们都知道,类,是 javascript 应用程序中非常重要的一个部分。

类通常包含一个 constructor , this可以指向任何新创建的对象。

不过在作为方法时,如果该方法作为普通函数被调用, this也可以指向任何其他值。与方法一样,类也可能失去对接收器的跟踪。

class hero {
  constructor(heroname) {
    this.heroname = heroname;
  }
  dialogue() {
    console.log(`i am ${this.heroname}`)
  }
}
const batman = new hero("batman");
batman.dialogue();

构造函数里的 this指向新创建的 类实例。当我们调用 batman.dialogue()时, dialogue()作为方法被调用, batman是它的接收器。

但是如果我们将 dialogue()方法的引用存储起来,并稍后将其作为函数调用,我们会丢失该方法的接收器,此时 this参数指向 undefined 。

const say = batman.dialogue;
say();

出现错误的原因是javascript 类是隐式的运行在严格模式下的。我们是在没有任何自动绑定的情况下调用 say()函数的。要解决这个问题,我们需要手动使用 bind()将 dialogue()函数与 batman绑定在一起。

const say = batman.dialogue.bind(batman);
say();

this 的原理

咳咳,技术文章,咱们严肃点

我们都说,this指的是函数运行时所在的环境。但是为什么呢?
【THE LAST TIME】this:call、apply、bind

我们都知道,javascript 的一个对象的赋值是将地址赋值给变量的。引擎在读取变量的时候其实就是要了个地址然后再从原地址读出来对象。那么如果对象里属性也是引用类型的话(比如 function),当然也是如此!

【THE LAST TIME】this:call、apply、bind

而javascript 允许函数体内部,引用当前环境的其他变量,而这个变量是由运行环境提供的。由于函数又可以在不同的运行环境执行,所以需要个机制来给函数提供运行环境!而这个机制,也就是我们说到心在的 this。this的初衷也就是在函数内部使用,代指当前的运行环境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

【THE LAST TIME】this:call、apply、bind

obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行.

总结

  • 函数是否在new中调用,如果是的话this绑定的是新创建的对象
var bar = new foo();
  • 函数是否通过call、apply或者其他硬性调用,如果是的话,this绑定的是指定的对象
var bar = foo.call(obj);
  • 函数是否在某一个上下文对象中调用,如果是的话,this绑定的是那个上下文对象
var bar = obj.foo();
  • 如果都不是的话,使用默认绑定,如果在严格模式下,就绑定到undefined,注意这里是方法里面的严格声明。否则绑定到全局对象
var bar = foo();

小试牛刀

var number = 2;
var obj = {
  number: 4,
  /*匿名函数自调*/
  fn1: (function() {
    var number;
    this.number *= 2; //4

    number = number * 2; //nan
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2; //6
      console.log(num);
      number *= 3; //9
      alert(number);
    };
  })(),

  db2: function() {
    this.number *= 2;
  }
};

var fn1 = obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);

评论区留下你的答案吧~

call & applay

上文中已经提到了 callapplybind,在 mdn 中定义的 apply 如下:

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

语法:

fun.apply(thisarg, [argsarray])

  • thisarg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsarray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ecmascript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容。

如上概念 apply 类似.区别就是 apply 和 call 传入的第二个参数类型不同。

call 的语法为:

fun.call(thisarg[, arg1[, arg2[, ...]]])

需要注意的是:

  • 调用 call 的对象,必须是个函数 function
  • call 的第一个参数,是一个对象。 function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
  • 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 function 对应的第一个参数上,之后参数都为空。

apply 的语法为:

function.apply(obj[,argarray])

需要注意的是:

  • 它的调用者必须是函数 function,并且只接收两个参数
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 function 中,并且会被映射到 function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。

记忆技巧:apply,a 开头,array,所以第二参数需要传递数据。

请问!什么是类数组?

核心理念

借!

对,就是借。举个栗子!我没有女朋友,周末。。。额,不,我没有摩托车