为什么要使用this?什么是this?

来看一段代码

function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是KYLE
speak.call( you ); // Hello, 我是 READER

如果不用this的话,我们就需要显式地传入一个上下文对象

function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是KYLE

通过这个我们就可以了解到this的作用:隐式地传递上下文对象,避免代码耦合

说完这个后,我们可以来描述下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 的调用位置
}
function foo() {
// 当前调用栈是baz -> bar -> foo
// 因此,当前调用位置在bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

this的绑定规则

现在你知道了如何找到调用位置,这时候你还需要了解关于this绑定的四条规则


1.默认绑定

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

声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的.。

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

在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

或者我们可以这么理解,foo()是被全局函数调用的,如window.foo()

当函数的执行上下文环境是全局环境,那么就会使用默认绑定,即绑定到全局对象上

不过,在严格模式下,就没有默认绑定了,this此时为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(); //

首先需要注意的是foo() 的声明方式,及其之后是如何被当作引用属性添加到obj 中的。但是无论是直接在obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。然而,调用位置会使用obj 上下文来引用函数,因此你可以说函数被调用时obj 对象“拥有”或者“包含”它

当foo() 被调用时,它的落脚点确实指向obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this 绑定到这个上下文对象。因为调用foo() 时this 被绑定到obj,因此this.a 和obj.a 是一样的。也就是这里会查找foo时,会经过obj这个上下文对象,会把obj上下文对象保存下来,因此,这里的this指向的就是obj上下文对象。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:

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

你可以这么理解:obj1=>obj2=>foo()。因此this找到了上下文对象后(obj2),就没必要继续去查找了,类似作用域链中查找变量。

隐式丢失:

一个最常见的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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

或者可以这么理解,bar()函数指向的是一个匿名函数的引用,这时候已经和obj没有任何关系了,也就不存在obj上下文对象的引用了。

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

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

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"

我们知道,参数传递其实是一种隐式赋值,也就是fn = obj.foo,所以结果和之前的例子一样。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:

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

你可以这么理解:setTimeOut()把你的回调函数丢进去了任务队列中,然后JS引擎拿出来执行,这个执行环境的上下文其实就是全局上下文环境,因此也是使用默认绑定。

就像我们看到的那样,回调函数丢失this 绑定是非常常见的。除此之外,还有一种情况this 的行为会出乎我们意料:调用回调函数的函数可能会修改this。

在一些流行的JavaScript 库中事件处理器常会把回调函数的this 强制绑定到触发事件的DOM 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。

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


3.显式绑定

这个比较简单,就是使用call()和apply()函数。

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

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

(1)硬绑定:

显示绑定仍然可能存在着丢失this绑定的问题,因此我们需要采用硬绑定,也就是:创建要给函数,在函数内部再显示绑定,如这里的bar()

function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); //
setTimeout( bar, 100 ); //
// 硬绑定的bar 不可能再修改它的this
bar.call( window ); //

这个常用来创建包裹函数,用于包括所有接受到的值

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 ); //

或者说创建一个绑定的辅助函数,也就是bind

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 ); //

当然这个bind函数比起正式的bind()有很多不足,正是因为硬绑定很常用,所以才有了ES5的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 ); //

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

我们可以看下MDN是怎么实现的,当然这这只是一个polyfill版本的,因此还是会有.prototype,而ES5的bind()是没有.prototype的

if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
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(this instanceof fNOP
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
}; if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP(); return fBound;
};
}

(2)API中的上下文

很多函数比如迭代函数,都提供了一个参数用于传入函数上下文来绑定this

function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..) 时把this 绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数在内部其实用到了call或者apply();


4.new绑定

使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1). 创建(或者说构造)一个全新的对象。
2). 这个新对象会被执行[[ 原型]] 连接。
3). 这个新对象会绑定到函数调用的this。
4). 如果函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。

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

当然你也可以这么理解,其实new内部机制也是使用了call或者apply函数,我们可以尝试实现New方法

//实现一个new方法
function New() {
let obj = new Object(),
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
let ret = Constructor.apply(obj, arguments);
return typeof ret === 'object' ? ret : obj; };
function foo(a) {
this.a = a;
}
var bar = New(foo,2);
console.log( bar); //foo { a: 2 }
console.log( bar.a ); //

绑定规则的优先级

优先级:new绑定>显式绑定>隐式绑定>默认绑定

注:ES6的箭头函数在四个规则以外,箭头函数的this值为词法作用域中的this值。

判断this

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

一些插曲:

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

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

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

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

然而这种传入null的方式对于使用一些第三方库时可能产生副作用(把this绑到全局对象了),所以

一种“更安全”的做法是传入一个特殊的对象,把this 绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized

zone,非军事区)对象——它就是一个空的非委托的对象,比如我们可以ø = Object.create(null)创建一个空对象,以保护全局对象。

//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。

if (!Function.prototype.softBind) {
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;
};
}

你不知道的JS(3)来聊聊this的更多相关文章

  1. 翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  2. 翻译连载 | 第 10 章:异步的函数式(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  3. 翻译连载 | 第 10 章:异步的函数式(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  4. 翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  5. 翻译连载 | 附录 A:Transducing(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  6. 翻译连载 | 附录 A:Transducing(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  7. 翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  8. 翻译连载 | 附录 C:函数式编程函数库-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  9. 你不知道的JS之 this 和对象原型(一)this 是什么

     原文:你不知道的js系列 JavaScript 的 this 机制并没有那么复杂 为什么会有 this? 在如何使用 this 之前,我们要搞清楚一个问题,为什么要使用 this. 下面的代码尝试去 ...

  10. 你不知道的JS之作用域和闭包 附录

     原文:你不知道的js系列 A 动态作用域 动态作用域 是和 JavaScript中的词法作用域 对立的概念. 动态作用域和 JavaScript 中的另外一个机制 (this)很相似. 词法作用域是 ...

随机推荐

  1. [ipsec][strongswan] 使用wireshark查看strongswan ipsec esp ikev1 ikev2的加密内容

    一,编译,启用strongswan的save-keys plugin ./configure --prefix=/root/OUTPUT --exec-prefix=/root/OUTPUT --en ...

  2. Linux服务器在SSH客户端如何实现免密登录

    一.SSH客户端Setting 配置 key ,  创建生成公钥导出文件. 二.服务器 master 上生成密钥 通过执行命令 ssh-keygen -t rsa 来生成我们需要的密钥. ssh-ke ...

  3. golang的包管理---vendor/dep等

    首先关于vendor 1 提出问题 我们知道,一个工程稍大一点,通常会依赖各种各样的包.而Go使用统一的GOPATH管理依赖包,且每个包仅保留一个版本.而不同的依赖包由各自的版本工具独立管理,所以当所 ...

  4. js实现图片变化

    CSS .home{ position: relative; width: 100%; height: 900px; overflow: hidden; } .home #tup{ position: ...

  5. 使用pushstate,指定回退地址

    history.pushState(null,"testname", window.location.href); window.addEventListener('popstat ...

  6. abap test seam 和 TEST-INJECTION

    TEST-SEAM 和 TEST-INJECTION 一块儿使用 可以模拟出调用方法的return,exporting,chaning值.  例如: 1: 假设有一个类zcl_demo_input,该 ...

  7. iPhone手机屏幕尺寸(分辨率)

    第一代iPhone2G屏幕为3.5英吋,分辨率为320*480像素,比例为3:2. 第二代iPhone3G屏幕为3.5英吋,分辨率为320*480像素,比例为3:2. 第三代iPhone3GS屏幕为3 ...

  8. Ch07 包和引入 - 练习

    1. 编写示例程序,展示为什么  package com.horstmann.impatient  不同于 package com package horstmann package impatien ...

  9. Xamarin.Forms 自定义 TapGestureRecognizer 附加属性

    While creating Xamarin.Forms applications, definitely you are going to need TapGestureRecognizer oft ...

  10. 拼多多(7pdd)微信跳转h5页面打开app跳转任意url关注技术weixin://dl/business/?ticket

    拼多多微信跳转接口利用了微信官方的weixin://dl/business/?ticket技术,此类接口可以在官方接口中找到,分析代码如下: <title>拼多多</title> ...