felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

JavaScript 之 this 隐式丢失 #90

Open felix-cao opened 5 years ago

felix-cao commented 5 years ago

《JavaScript 之 this 绑定/指向》 一文中,我们聊了this 绑定的四种场景,在“隐式绑定”场景中,经常会导致 this 丢失的情形,本篇主要来聊一聊这种情况。

隐式丢失是指被隐式绑定的函数丢失绑定对象,这种情况容易出错却又常见,为什么呢?因为 JavaScript 中函数可以作为参数,也可以作为返回值被四处传播,传着传着就丢了 this 指向,被传的晕头转向了。

下面来聊一聊 this 丢失的场景

一、函数别名

我们将 《JavaScript 之 this 绑定/指向》 第四节中给的案例代码改一下:

function getAge() {
  console.log('this.age: ', this.age);
}

var person = {
  age: 18,
  getAge: getAge
}
var getPersonAge = person.getAge;
getPersonAge() // this.age: undefined

我们只是给 person.getAge 换了个 getPersonAge 名字。就是这个换名赋值语句操作出事的,因为在 javascript 中,函数是对象,对象之间是引用传递而不是传值传递,因此这里的赋值实际上是这样的:

二、 函数作为参数进行参数传递

我们知道 JavaScript 的参数传递时传参而不时传值。

var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
bar(obj.foo); // 0

上面的代码 把 obj.foo 当作参数传递给 bar 函数时,在 bar 函数里,有隐式的函数赋值 fn=obj.foo。与上例类似,只是把 foo 函数赋给了 fn,而 fnobj 对象则毫无关系。

//等价于
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});

三、 setTimeout 等内置函数

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0

其实也是把 obj.foo 作为参数传递给 setTimeout() 函数

//等价于
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0

若想获得obj对象中的a属性值,可以将obj.foo函数放置在定时器中的匿名函数中进行隐式绑定

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(function(){
    obj.foo();
},100);//2

或者也可以使用bind方法将foo()方法的this绑定到obj上

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo.bind(obj),100);//2

四、 间接引用

函数的"间接引用"一般都在无意间创建,最容易在赋值时发生,会造成隐式丢失

function foo() {
  console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
// 将 o.foo 函数赋值给 p.foo 函数,然后立即执行。相当于仅仅是 foo() 函数的立即执行
(p.foo = o.foo)(); // 2
function foo() {
  console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
// 将 o.foo 函数赋值给 p.foo 函数,之后 p.foo 函数再执行,是属于 p 对象的 foo 函数的执行
p.foo = o.foo;
p.foo();//4

五、其他

5.1、理解一下对象的存储

javascript 引擎内部,objobj.foo 储存在两个内存地址,简称为 M1M2。只有 obj.foo() 这样调用时,是从 M1 调用 M2,因此 this 指向 obj。但是,下面三种情况,都是直接取出 M2 进行运算,然后就在全局环境执行运算结果(还是 M2),因此this指向全局环境

var a = 0;
var obj = {
  a : 2,
  foo:foo
};
function foo() {
  console.log( this.a );
};

(obj.foo = obj.foo)(); // 0

(false || obj.foo)(); // 0

(1, obj.foo)(); // 0

5.2、document.getElementById

document.getElementById 这个方法名有点长,程序员有个通病:喜欢用个短的函数来代替它

var getId = document.getElementById;
getId('box');

现在,让下面的代码在浏览器中跑一把试试:

<html>
  <body>
    <div id='box'>我是BOX</div>
  </body>

  <script>
  var getId = document.getElementById;
  getId('box');
  </script>
</html>

结果我们发现,这段代码抛出了异常。

这是因为 JavaScript 引擎的 document.getElementById 方法的内部实现中需要用到 this,这个 this 本来是被期望指向 document,当 getElementById 作为 document 对象的属性被调用时,方法内部的 this 确实是指向 document,但当用 getId 来引用 document.getElementById 方法之后,再调用 getId ,情况就不一样了,this 被丢失了,函数内部的 this 指向了 window,而不是 document。说白了,就是 函数别名 引起的。

现在知道原因了,我们利用 applydocument 当做 this 传入 getId函数,从而修正 this 指向

document.getElementById = (function( fn ){ 
  return function(){ 
    return fn.apply( document, arguments ); 
  }
})( document.getElementById );

var getId = document.getElementById; 
var div = getId( 'box' );
fwt-1025 commented 3 years ago

为什么call 和 apply不能解决this的隐式丢失?而bind可以,是因为bind返回一个绑定好this的函数吗?请帮忙解决一下这个疑问,谢谢

felix-cao commented 3 years ago

为什么call 和 apply不能解决this的隐式丢失?而bind可以,是因为bind返回一个绑定好this的函数吗?请帮忙解决一下这个疑问,谢谢

谢邀,特殊原因,回复慢了。 JavaScriptFunction.property.call()Function.property.apply()Function.property.bind(),存在的意义都是改变 this 的指向,这是函数式编程的非常重要的特点,函数式编程就是以函数为抽象单元和行为单元,有了这三个,使得 JavaScript 的函数式编程灵活强大。

call, applybind 的区别为:

您所提的问题并没这个说法。有没有‘场景’,可复现?

fwt-1025 commented 3 years ago

为什么call 和 apply不能解决this的隐式丢失?而bind可以,是因为bind返回一个绑定好this的函数吗?请帮忙解决一下这个疑问,谢谢

谢邀,特殊原因,回复慢了。 JavaScriptFunction.property.call()Function.property.apply()Function.property.bind(),存在的意义都是改变 this 的指向,这是函数式编程的非常重要的特点,函数式编程就是以函数为抽象单元和行为单元,有了这三个,使得 JavaScript 的函数式编程灵活强大。

call, applybind 的区别为:

  • call , apply 修正 this 指向后立即执行函数
  • bind 返回一个函数,而不立即执行

您所提的问题并没这个说法。有没有‘场景’,可复现?

sorry,是我问的问题有些偏差,还请谅解,我看到 《你不知道的JavaScript上卷》中第二部分第二章2.2.3显示绑定小节中介绍到,显示绑定并不能解决this丢失绑定的问题,而硬绑定可以解决这个问题,这里不是很清楚,所以资讯一下您的见解。

  • 我所理解的显示绑定
    function foo () {
    console.log(this.name)
    }
    var obj = {
    name: 'objName'
    }
    foo.call(obj) // 显示绑定,  虽然绑定了this但是我还可以随便改变this的指向,所以作者在这里说不能解决this丢失绑定的问题。
  • 硬绑定
    
    function foo () {
    console.log(this.name)
    }
    var obj = {
    name: 'objName'
    }
    var bar = function () {
    foo.call(obj)
    }
    bar() // 这时候bar无论怎么绑定都会执行foo.call(obj),也就是绑死了,所以this就不会发生丢失问题,是不是这个意思。
    bar.call(null) // objName

// 还有bind的实例 function foo () { console.log(this.name) } var obj = { name: 'objName' } var bar = foo.bind(obj) bar.call(null) // 还是会打印objName bar() // objName 这两种一个意思吧, 您在文中提到,bind返回一个绑定好this的函数,而上面的例子也是返回了一个绑定好了this的函数。所以这就是硬绑定 能解决this丢失的问题吧。

felix-cao commented 3 years ago

@ComponentTY 谢谢,非常好的一个 Case, 您的例子更加说明了“JavaScript 是一门非常灵活的语言,以至于你永远都不知道是否已经真的掌握了这么语言”

我觉得要结合词法作用域和执行上下文栈去理解,把您的例子更改一下,方便理解

function foo () {
  console.log(`this in ${arguments.callee.name}:`, this.name)
}
var obj = {
   name: 'objName'
}
var bar = function () {
   console.log(`this in ${arguments.callee.name}:`, this.name);
   foo.call(obj) // 改变 foo 中的 this,注意这里访问了 obj, 指向的是 window.obj
}
var name = 'widowName'; //  JavaScript 顶层变量的属性与全局变量是等价的, 即等价于 window.name
foo() // code1 不改变this 指向时
bar() // code2, 不改变bar 中的 this, 但改变 foo 中的 this
bar.call(null) // code3 改变 bar 中的 this, 也改变 foo 中的 this

再改下看看(主要想说明 bar 内部的变量 obj):

function foo () {
       console.log(`this in ${arguments.callee.name}:`, this.name)
   }
   var obj = {
        name: 'objName'
   }
   var bar = function () {
       obj = {
        name: 'objName in bar'
       }
       console.log(`this in ${arguments.callee.name}:`, this.name);
       foo.call(obj) // 改变 foo 中的 this, 这里 `bar` 函数内部有自己定义的 `obj`
   }
   var name = 'widowName';
  foo()
  bar()
  bar.call(null)

再改下看看:

function foo () {
   console.log(`this in ${arguments.callee.name}:`, this.name)
}
var bar = function () {
   console.log(`this in ${arguments.callee.name}:`, this.name);
   foo.call(this)
}
var name = 'widowName';
foo()
bar()
bar.call(null)