你不知道的javascript上 (this部分)

第1章——this预览

1. this指向

人们很容易误以为this指向函数自身,不过我们看一下这个例子。

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

foo.count = 0;

var i;

for(i = 0; i < 10; i++){
  if(i > 5){
    foo(i)
  }
}

// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo被调用了多少次?
console.log(foo.count) // 0 

console.log语句产生了4条输出,证明foo(…)确实被调用了4次,但是foo.count是0。

实际上,这段代码无意中在全局创建了一个变量count,他的值为NaN,因为undefined++ = NaN

setTimeout(funciton(){
 // 匿名函数,无法指向自身,除非使用arguments.callee
})

var test = 0;
setTimeout(function(){
  test++
  console.log(test)
  test < 5 && arguments.callee()
},10)

如何让例子1中的代码,输入如我们所愿的结果呢(输出4)

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

foo.count = 0;

var i;

for(i = 0; i < 10; i++){
  if(i > 5){
    foo.call(foo, i) // 将这里进行修改,使用fn.call将指针指向foo函数
  }
}

this到底是什么?

学习this的第一步是明白this既指向函数自身,也不指向函数的此法作用域。

this是在运行时进行绑定,而不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。

this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,引擎会创建一个执行上下文,这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。

第二章——this全面解析

调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。

最重要的是对调用站进行分析,我们可以看一个例子

function baz(){
  // 当前调用栈是:baz
  // 因此,当前调用位置是全局作用域
  console.log("baz")
  bar(); // bar的调用位置
}

function bar(){
  // 当前的调用栈是 baz -> bar
  // 因此当前调用位置在baz中
  console.log('bar')
  foo() // foo的调用位置
}
window.a = 'a'
function foo(){
  // 我们可以在这里加个断点看下
  debugger 
  // 当前调用栈 baz -> bar -> foo
  // 因此,当前调用位置在bar中
  console.log('foo')
}

baz() // baz的调用位置

这便是我们的**调用栈(call stack)**⬇️
在这里插入图片描述

什么是调用栈呢?

根据MDN的解释:

调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

  • 每调用一个函数,解释器就会把该函数添加进调用栈(栈顶)并开始执行。

  • 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。

  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。

  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。

栈是一种后进先出的数据结构

举个例子:

function f1(){
  console.log('Hi by f1!')
}
function f2(){
  f1()
  console.log('Hi by f2!')
}
f2()

这段代码的执行过程是

  1. f2函数被调用,因此全局作用域会创建一个执行上下文,并将这个上下文推进执行栈的底部。
  2. f2执行过程,发现f1函数被调用,因此会将执行上下文推进执行栈,在全局上下文的上面
  3. f1执行,也创建了一个执行上下文,在f2函数执行上下文的上面
  4. f1执行完毕,f1的执行上下文被释放,出栈
  5. f2执行完毕,f2的执行上下文被释放,出栈

在这里插入图片描述

this的绑定规则(重点)

this绑定分为4类,他的最终指向取决于他所属分类的优先级。

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

1. 默认绑定

例子:

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

var a = 2; // 在全局作用域中声明的属性,相当于window.a = 2

foo(); // 2

这段代码中,foo函数执行,打印出的结果是2,也就是全局作用域中的a(this.a -> window.a)。

为什么foo执行后,this指针指向了全局呢?

在这个例子中,函数调用使用了this的默认绑定,我们可以分析foo函数的调用位置,foo()是直接使用不带任何修饰的函数引用(不通过xxx.foo()调用,不通过foo.call, new foo()等…)进行调用的,因此只能使用默认绑定,无法应用其他规则。

而如果在严格模式下,this.a会打印出undefined

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

var a = 2;
foo() // TypeError: this is undefined

2.隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。

思考代码:

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

var obj = {
  a: 2,
  foo: foo
}
obj.foo() // 2

在这个例子中,调用位置会使用obj上下文来引用函数,当foo()调用,它的指针指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

现在我们将刚刚的例子进行一个改造:

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

var obj = {
  a: 2,
  foo: foo
}
window.a = 123
var bar = obj.foo
obj.foo()
bar()

结果为
// 2
// 123

为什么第二次打印出window的a,而不是obj的a?

隐式丢失

隐式绑定的函数会丢失绑定对象,他会应用默认绑定,从而把this绑定到全局对象或者undefined(非严格模式)上。

虽然bar是obj.foo的一个引用,但是实际上,他引用的是foo函数本身,因为此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

在使用回调函数时,也会发生这种情况

例子:

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

function doFoo(fn) {
  // fn引用的是foo
  fn(); // 调用位置
}

var obj = {
  a: 2,
  foo: foo
}
var a = 'global...'
doFoo(obj.foo) // global...

这里例子中,我们期望打印出obj.a,但结果却是window.a,其实和上个例子一样。

在doFoo(obj.foo)这行代码执行的时候,向doFoo函数传递了obj.foo这个实参,形参赋值的过程就相当于fn = obj.foo,这时候就发生了和上面例子一样的情况,出现了隐式丢失。

setTimeout,setInterval也会出现this的丢失

function foo() {
  console.log( this.a );
}
var obj = {
  a: 2,
  foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

回调函数丢失 this 绑定是非常常见的。

隐式绑定的一个注意点

对象属性引用链只有在最顶层或者最后一层会影响调用位置。

例子:

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

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

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

obj1.obj2.foo() // 42

打印出的是obj2的a

3.显示绑定

如果我们希望将this强制绑定在某个对象上,我们可以使用函数的call,apply,bind方法。

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

var obj = {
  a: 2
}

foo.cal( obj ) //2

通过foo.call(…),我们可以在调用foo时强制把它的this绑定到obj上。

如果传入了一个原始值(字符串,布尔类型)来当做this的绑定对象,这个原始值会被转为它的对象形式(也就是 new String(…),new Boolean(…))。这通常被称为“装箱”。

硬绑定

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

var obj = {
  a: 2
}

var bar = foo.bind(obj)

bar() // 2
setTimeout(bar,100) // 2
window.a = '123'
bar.call(window) // 2

我们通过bind,在声明bar的时候强行将foo指向了obj,后面再执行的时候就不会发生隐式丢失了。

bind其他用法
function add(...args){
  return args.reduce((sum, cur) => sum+cur, 0)
}

var newAdd = add.bind(null, 1,2) 
newAdd(3,4)

当我们要使用add函数,且使用时经常用到1,2这两个参数时,我们可以利用bind,让新声明的函数默认有1,2这两个参数,这么做在不改变add函数的基础上,让函数的调用更方便。

4.new绑定

说说new

在传统的面向类的语言中,“构造函数”是类中的一种特殊方法,使用new初始化类时会调用类中的构造函数。通常形式是:

var something = new MyClass(...)

JavaScript也有new,但是JavaScript中new的机制实际上和面向类的语言完全不同。

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。他们不属于某个类,也不会实例化一个类,他们只是被new操作符调用的普通函数而已。

当函数通过new来调用时,这种函数调用被称为构造函数调用。有一个重要但是非常细微的注意点:

实际上并不存在所谓的“构造函数”,只有对函数的“构造调用”。

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建一个全新的对象
  2. 这个新对象会被执行[[原型]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象

例子:

function foo(a) {
  this.a = a;
}

var bar = new foo(2);
console.log(bar.a) // 2

使用new来调用foo时,会构造新对象并把它绑定到foo(…)调用的this上。称为new绑定。

4种this绑定的优先级

例子1:

function foo() {
  console.log( this.a );
}
var obj1 = {
  a: 2,
  foo: foo
};
var obj2 = {
  a: 3,
  foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

从上面例子中可以看到,obj1.foo为隐式绑定,obj1.foo.call为显示绑定。最后obj1.foo.call(obj2)结果为3,说明

显式绑定优先级 > 隐式绑定

现在我们比较一下new绑定和隐式绑定优先级:

function foo(sth){
  this.a = sth
}

var obj1 = {
  foo: foo
}

var obj2 = {}

obj1.foo(2)
console.log(obj1.a) // 2

var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4

可以看到new绑定比隐式绑定优先级高,但是new绑定和显示绑定优先级谁更高呢?

function foo(sth){
  this.a = sth
}

var obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2

var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3

bar被硬绑定到obj1上,但是new bar(3)并没有像我们预计的那样把obj1.a修改为3。相反,new修改了硬绑定(到obj1的)调用bar(…)中的this。因为使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值是3。

看一下bind的polyfill实现

 if (!Function.prototype.bind) {
        Function.prototype.bind = function(oThis) {
          if (typeof this !== "function") {
            // 与 ECMAScript 5 最接近的
            // 内部 IsCallable 函数
            throw new TypeError(
              "Function.prototype.bind - what is trying " +
              "to be bound is not callable"
            );
          }
          var aArgs = Array.prototype.slice.call( arguments, 1 ),
          fToBind = this,
          fNOP = function(){},
          fBound = function(){
            return fToBind.apply(
            ( // 这里是new修改this的相关代码
              this instanceof fNOP &&
              oThis ? this : oThis
            ),
              aArgs.concat(
                Array.prototype.slice.call( arguments )
              )
            )};
            fNOP.prototype = this.prototype;
            fBound.prototype = new fNOP();
            return fBound;
        };
    }

下面是 new 修改 this 的相关代码:

this instanceof fNOP &&
oThis ? this : oThis
// ... 以及:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

这段代码会判断绑定函数是否是被new调用,如果是的话就使用新创建的this替换硬绑定的this。

总结判断this的顺序

1.函数是否在new中调用(var bar = new foo())
2.是否通过call,apply,bind绑定调用(var bar = foo.call(obj2))
3.是否在上下文对象中调用(var bar = obj1.foo())
4.以上都不是,使用默认绑定,非严格模式下绑定到全局

箭头函数中的this

function foo() {
  // 返回一个箭头函数
  return a => {
    // this继承自foo()
    console.log(this.a)
  }
}

var obj1 = {
  a: 2
}

var obj2 = {
  a: 3
}

var bar = foo.call(obj1)
bar.call(obj2) // 2, 不是3!

foo内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new 也不行)

拓展——软绑定

硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。如果可以给默认绑定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

Function.prototype.softBind = function(obj) {
  var fn = this;
  // 捕获所有 curried 参数
  var curried = [].slice.call( arguments, 1 );
  var bound = function() {
    return fn.apply(
      (!this || this === (window || global)) ?
        obj : this
     curried.concat.apply( curried, arguments )
    );
  };
  bound.prototype = Object.create( fn.prototype );
  return bound;
};


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

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!

fooOBJ.call( obj3 ); // name: obj3 <---- 看!

setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定

this的面试题

转载自:
【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)

题1:

考察了在全局作用域中开启严格模式,对this有哪些改变。

"use strict";
var a = 10;
function foo () {
  console.log('this1', this)
  console.log(window.a)
  console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();

var a = 10 依然是相当于执行代码 window.a = 10,开启严格模式只使得函数中的this指向undefined,并不会改变全局中this的指向。因此this2 还是window对象。

题2:

let a = 10
const b = 20
function foo(){
  console.log(this.a)
  console.log(this.b)
}
foo();
console.log(window.a)

解析:

如果把var改成let或const,变量不会被绑定到window上。

结果:

undefined
undefined
undefined

题3:

var a = 1;
function foo(){
  var a = 2;
  function inner() {
    console.log(this.a)
  }
  inner()
}
foo()

答案:1
这里相当于执行

题4

var a = 1;
function foo(){
  this.a = 2;
  function inner() {
    console.log(this.a)
  }
  inner()
}
foo()

答案: 2

题5:

var obj = {
  a: 1,
  b: {
    a: 2,
    getA: getA
  },
  c:{
    getA: getA
  }
}

obj.b.getA()
obj.c.getA()

答案:

2
undefined

题6

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }

obj.foo();
foo2();
obj2.foo2();

解析foo2()obj2中的foo都发生了隐式丢失,容易忽略的是obj2.foo2()this在丢失后,由于是obj2调用了foo2,所以this指针又指向了obj2

结果

1
2
3

未完待续…

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值