Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

About this #2

Open
LizPeng opened this issue Feb 22, 2018 · 8 comments
Open

About this #2

LizPeng opened this issue Feb 22, 2018 · 8 comments

Comments

@LizPeng
Copy link
Owner

LizPeng commented Feb 22, 2018

exercise 1

var name = 'window'
var person = {
  name: 'person',
  getName: function() {
    return this.name
  }
}
console.log(person.getName()) //
console.log((person.getName)()) //
console.log((person.getName = person.getName)()) 

尚未理解的内容:

  • 2.4.3 软绑定
@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

this提供了一种更优雅的方式隐式“传递”一个对象引用,因此可以将API设计得更加简介并易于复用。

需要明确的是,this在任何情况下都 不指向 函数的词法作用域

this到底是什么

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个活动记录(有时候也成为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

this实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.1 调用位置

因为某些编程模式可能会隐藏真正的调用位置

最重要是要分析 调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

看看什么是调用栈和调用位置:

// 调用栈和调用位置
function baz() {
  // 当前的调用栈是: baz
  //  因此,当前调用位置的是 全局作用域
  console.log('baz')
  bar() // <-- bar的调用位置
}
function bar() {
  // 当前的调用栈是baz -> bar
  // 因为,当前调用位置在baz中
  console.log('bar')
  foo() // <-- foo的调用位置
}

function foo() {
  // 当前的调用栈是 baz -> bar -> foo
  // 因此,当前调用位置在bar中
  console.log("foo")
}
baz() // <-- baz的调用位置

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.2 绑定规则

来看看在函数的执行过程中调用位置如何决定this的绑定对象。
你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。

2.2.1 默认绑定

介绍最常用的函数调用类型: 独立函数调用。可以把这条规则看做是无法应用其他规则时的默认规则。
思考下面的代码:

function foo() {
  console.log(this.a)
}
var a = 2 
foo() // 2

我们可以看到当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this的 默认绑定,因此this指向全局对象。

只有foo()运行在非严格模式下时,默认绑定才能绑定到全局对象。

2.2.2 隐式绑定

调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。
思考下面的代码:

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

当函数引用有上下文对象时,隐式绑定 规则会把函数调用中的this绑定到这个上下文对象。
对象属性引用链中只有最后一层影响调用位置。举例来说:

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

隐式丢失

一个最常见的this绑定问题就是被 隐式绑定 的函数会丢失绑定对象,也就是说他会应用 默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
思考下面代码:

function foo() {  
    console.log( this.a ); 
} 
 
var obj = {  
    a: 2, 
    foo: foo  
}; 
 
var bar = obj.foo; // 函数别名! 
 
var a = "oops, global"; // a 是全局对象的属性 
 
bar(); // "oops, global"

虽然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 = "oops, global"; // a 是全局对象的属性
 
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。

回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为会出乎我们意料:调用回调函数的函数可能会修改this。在一些流行的JavaScript库中事件处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。

无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置。之后我们会介绍如何通过固定this来修复这个问题。

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.2.3 显示绑定

像上一节看到的那样,在分析 _隐式绑定_时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

使用call()和apply()方法,他们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为 显示绑定

思考下面的代码:

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

通过foo.call(...),我们可以在调用foo时强制把它的的this绑定到obj上。
如果你传入了一个原始值(字符串类型,布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(...) 、new Boolean(..)或者new Number(...) )。这通常被称为“装箱”。

从this绑定的角度来说,call和apply是一样的,它们的区别体现在其他的参数上

可惜,显示绑定任然无法解决我们之前提出的 丢失绑定问题。

1. 硬绑定

但是现实绑定的一个变种可以解决这个问题。
思考以下的代码:

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

var obj = {
  a:2
}
var bar = function() {
  foo.call(obj)
}
bar() //2
setTimeout(bar, 100) //2

// 硬绑定的bar不可能再修改它的this
// `bar` hard binds `foo`'s `this` to `obj`
// so that it cannot be overriden
bar.call(window) // 2

我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显示的强制绑定,因此我们称之为硬绑定hard binding

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

function foo(something) {
	console.log( this.a, something );
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = function() {
	return foo.apply( obj, arguments );
};

var b = bar( 3 ); // 2 3
console.log( b ); // 5

Another way to express this pattern is to create a re-usable helper:

function foo(something) {
  console.log(this.a ,something)
  return this.a + something
}

// 简单的辅助绑定函数
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments)
  }
}
var obj = {
  a: 2
}
var bar = bind(foo, obj)
var b = bar(3)  // 2 3
console.log(b)  // 5

由于 硬绑定 是一种非常常用的模式,所以在ES5中提供了内置的方式Function.prototype.bind,它的用法如下:

function foo(something) {  
    console.log( this.a, something );  
    return this.a + something; 
} 
var obj = {  
    a:2 
}; 
var bar = foo.bind(obj)
var b = bar(3) // 2 3
console.log(b) // 5

bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

2. API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。

For instance:

function foo(el) {
	console.log( el, this.id );
}

var obj = {
	id: "awesome"
};

// use `obj` as `this` for `foo(..)` calls
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach( foo, obj ); // 1 awesome  2 awesome  3 awesome

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.2.4 new绑定

在讲解它之前我们首先需要澄清一个非常常见的关于JavaScript中函数和对象的误解。
在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。通常的形式是这样的:

something = new MyCalss(...)

JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为JavaScript中的new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的语言完全不同。
首先我们重新定义一下JavaScript中的“构造函数”。在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。

举例来说,思考一下Number(..)作为构造函数时的行为,ES5.1中这样描述它:

当Number在new表达式中被调用时,它是一个构造函数:它会初始化新创建的对象

所以,包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为_构造函数调用_。这里有一个重要但非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者是发送构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. the newly constructed object is [[Prototype]]-linked 。
  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是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.3 优先级

如果调用位置可以应用多条规则怎么办?
为了解决这个问题就必须给这些规则设定优先级,这就是我们接下来要介绍的内容。
毫无疑问,默认绑定的优先级是四条规则中最低的,所以我们可以先不考虑它。
显示绑定和隐式绑定哪个优先级更高?我们来测试一下:

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

可以看到,显示绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显示绑定

现在我们需要搞清楚 new 绑定和隐式绑定的优先级谁高谁低:

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

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

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

bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(technically called "partial application", which is a subset of "currying")

For example:

function foo(p1,p2) {  
        this.val = p1 + p2; 
} 
 
// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么 
// 反正使用 new 时 this 会被修改 
var bar = foo.bind( null, "p1" ); 
 
var baz = new bar( "p2" );  
 
baz.val; // p1p2

判断this

可以按照下面的顺序来进行判断:

  1. 函数是否在new中调用(new绑定) ? 如果是的话this绑定的是新创建的对象。
var bar = new foo()
  1. 函数是否通过call、apply(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
  1. 函数是否在某个上下文对象中调用(隐式绑定) ? 如果是的话,this绑定的是哪个上下文对象。
var bar  = obj1.foo()
  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()

对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。
不过。。。。凡事总有例外。

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 23, 2018

2.4 绑定例外

你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

2.4.1 被忽略的this

如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

function foo() {
  console.log(this.a)
}
var a = 2 
foo.call(null) //2

那什么情况下会传入null呢?
一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。
类似地,bind(..)可以对参数进行柯里化(预先设置一下参数),这种方法有时非常有用:

foo.apply(null, [2, 3]) // a:2, b:3

//使用bind(..)进行柯里化
var bar = foo.bind(null, 2)
bar(3) // a:2, b:3

这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null是一个不错的选择。

但是在ES6中,可以使用...操作符来代替apply来“展开”数组,这样可以避免不必要的this绑定。可惜在ES6没有柯里化的相关语法,因此还是需要使用bind(..)

然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了
this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览
器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。
显而易见,这种方式可能会导致许多难以分析和追踪的 bug。

更安全的this

一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序
产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized
zone,非军事区)对象——它就是一个空的非委托的对象(委托在第 5 章和第 6 章介绍)。

如果我们在忽略 this 绑定时总是传入一个 DMZ 对象,那就什么都不用担心了,因为任何
对于 this 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)
( 详 细 介 绍 请 看 第 5 章 )。Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object.
prototype 这个委托,所以它比 {}“更空”:

function foo(a,b) { 
    console.log( "a:" + a + ", b:" + b ); 
} 
 
// 我们的 DMZ 空对象 
var ø = Object.create( null ); 
 
// 把数组展开成参数 
foo.apply( ø, [2, 3] ); // a:2, b:3 
 
// 使用 bind(..) 进行柯里化 
var bar = foo.bind( ø, 2 );  
bar( 3 ); // a:2, b:3

使用变量名 ø 不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为 ø 表示
“我希望 this 是空”,这比 null 的含义更清楚。不过再说一遍,你可以用任何喜欢的名字
来命名 DMZ 对象。

2.4.2 间接引用

另一个需要注意的是,你有可能(有意或无意地)创建了一个函数的“简介引用”,在这种情况下,调用这个函数会应用默认绑定规则。
间接引用 最容易在赋值时发生:

function foo() {
  console.log(this.a)
}
var a = 2 
var o = {a: 3, foo: foo}
var p = {a: 4}
o.foo() // 3
(p.foo = o.foo)() // 2

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置的foo()而不是p.foo()或o.foo()。这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

2.4.3 软绑定

硬绑定可以把this强制绑定到指定对象(除了使用new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

It would be nice if there was a way to provide a different default for default binding (not global or undefined),(如果可以给默认绑定指定一个全局对象和 undefined 以外的值),那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

可以通过一种被称为软绑定的方法来实现我们想要的效果:

if (!Function.prototype.softBind) {
  Function.prototype.softBind = function (obj) {
    var fn = this,
      curried = [].slice.call(arguments, 1),
      bound = function bound() {
        return fn.apply(
          (!this ||
            (typeof window !== "undefined" &&
              this === window) ||
            (typeof global !== "undefined" &&
              this === global)
          ) ? obj : this,
          curried.concat.apply(curried, arguments)
        );
      };
    bound.prototype = Object.create(fn.prototype);
    return bound;
  };
}

除了软绑定之外,softBind(..) 的其他原理和 ES5 内置的 bind(..) 类似。它会对指定的函
数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把
指定的默认对象 obj 绑定到 this,否则不会修改 this。此外,这段代码还支持可选的柯里
化(详情请查看之前和 bind(..) 相关的介绍)。

下面我们看看 softBind 是否实现了软绑定功能:

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   <---- 应用了软绑定

可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默
认绑定,则会将 this 绑定到 obj。

@LizPeng
Copy link
Owner Author

LizPeng commented Feb 24, 2018

2.5 this词法

我们之前介绍的四条规则已经可以包含所有正常的函数。但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用function关键字定义的,而是使用被称为"胖箭头"的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
我们来看看箭头函数的词法作用域:

function foo() {
  // return an arrow function
  return (a) => {
    // `this` here is lexically adopted from `foo()`
    console.log(this.a);
  };
}

var obj1 = {
  a: 2
};

var obj2 = {
  a: 3
};

var bar = foo.call(obj1);
bar.call(obj2); // 2, not 3!

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new 也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() {  
    setTimeout(() => { 
 // 这里的 this 在此法上继承自 foo() 
        console.log( this.a );  
    },100); 
} 
 
var obj = {  
    a:2 
}; 
 
foo.call( obj ); // 2

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo() { 
    var self = this; // lexical capture of this  
    setTimeout( function(){ 
        console.log( self.a ); 
    }, 100 ); 
}  
 
var obj = { 
    a: 2 
}; 
 
foo.call( obj ); // 2

虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替
代的是 this 机制。

2.6 小结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面四条规则来判断this的绑定对象。

  1. 由new调用?绑定到新创建的对象。
  2. 由call、apply、bind调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑
定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。
ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant