首先要理解调用位置: 调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

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

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 的调用位置
绑定规则
  1. 默认绑定。

    最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

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

如果使用严格模式(strict mode), 那么全局对象将无法使用默认绑定, 因此 this 会绑定到 undefined:

function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
  1. 隐式绑定

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

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

首先需要注意的是 foo() 的声明方式, 及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性, 这个函数严格来说都不属于obj 对象。

然而, 调用位置会使用 obj 上下文来引用函数, 因此你可以说函数被调用时 obj 对象“拥有” 或者“包含” 它。

当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

隐式丢失:一个最常见的 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 绑定是非常常见的。

  1. 显式绑定

    像call, apply, bind这三种可以直接指定 this 的绑定对象的方法,我们称之为显式绑定。

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

  1. new绑定

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

在 JavaScript 中, 构造函数只是一些使用 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
判断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()
绑定例外

在某些场景下 this 的绑定行为会出乎意料, 你认为应当应用其他绑定规则时, 实际上应用的可能是默认绑定规则。

  • 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、 apply 或者 bind, 这些值在调用时会被忽略, 实际应用的是默认绑定规则:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

那么什么情况下你会传入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

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

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

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

在 JavaScript 中创建一个空对象最简单的方法都是 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
  • 间接应用

另一个需要注意的是, 你有可能(有意或者无意地) 创建一个函数的“间接引用”, 在这种情况下, 调用这个函数会应用默认绑定规则。

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 会被绑定到全局对象。

软绑定

感慨一下这个究极艺术,起因硬绑定会大大降低函数的灵活性, 使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

如果可以给默认绑定指定一个全局对象和 undefined 以外的值, 那就可以实现和硬绑定相同的效果, 同时保留隐式绑定或者显式绑定修改 this 的能力。这个就是软绑定。

实现如下:

if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this; // fn就是调用的函数 // 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 ); var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments ) //这里的argements是bound的arguments,也就是说在softBind的时候可以传参一次,后面可以再传一次,参数会在这里合并起来
);
};
bound.prototype = Object.create( fn.prototype ); // 原型链继承过来
return bound;
};
}

首先检查调用时的 this, 如果 this 绑定到全局对象或者 undefined, 那就把指定的默认对象 obj 绑定到 this, 否则不会修改 this。

应用场景:

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。

《你不知道的JavaScript(上)》笔记——this全面解析的更多相关文章

  1. 你不知道的JavaScript上卷笔记

    你不知道的JavaScript上卷笔记 前言 You don't know JavaScript是github上一个系列文章   初看到这一标题的时候,感觉怎么老外也搞标题党,用这种冲突性比较强的题目 ...

  2. 读书笔记-你不知道的JavaScript(上)

    本文首发在我的个人博客:http://muyunyun.cn/ <你不知道的JavaScript>系列丛书给出了很多颠覆以往对JavaScript认知的点, 读完上卷,受益匪浅,于是对其精 ...

  3. 你不知道的javascript读书笔记3

    概述 这是我看<你不知道的JavaScript(中卷)>中关于类型检查的笔记,供以后开发时参考,相信对其他人也有用. typeof 我们知道js中有七种内置类型:undefined, nu ...

  4. 《你不知道的JavaScript》笔记(一)

    用了一个星期把<你不知道的JavaScript>看完了,但是留下了很多疑惑,于是又带着这些疑惑回头看JavaScript的内容,略有所获. 第二遍阅读这本书,希望自己能够有更为深刻的理解. ...

  5. 【你不知道的javaScript 上卷 笔记3】javaScript中的声明提升表现

    console.log( a ); var a = 2; 执行输出undefined a = 2; var a; console.log( a ); 执行输出2 说明:javaScript 运行时在编 ...

  6. 《你不知道的javascript(上)》笔记

    作用域是什么 编译原理 分词/词法分析 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元 解析/语法分析 词法单元流(数组)转换成一个由元素逐级嵌套所组成 ...

  7. <你不知道的JavaScript>读书笔记

    近几天看了一本不错的 JavaScript 的书,是 Kyle Simpson 写的 <You Don't know JS>.这本书是 Kyle Simpson 在 Github 上的开源 ...

  8. 【你不知道的javaScript 上卷 笔记7】javaScript中对象的[[Prototype]]机制

    [[Prototype]]机制 [[Prototype]]是对象内部的隐试属性,指向一个内部的链接,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototyp ...

  9. 【你不知道的javaScript 上卷 笔记6】javaScript中的对象相关内容

    一.创建一个对象的语法 var myObj = { key: value // ... };//字面量 var myObj = new Object(); //new myObj.key = valu ...

  10. 【你不知道的javaScript 上卷 笔记5】javaScript中的this词法

    function foo() { console.log( a ); } function bar() { var a = 3; foo(); } var a = 2; bar(); 上面这段代码为什 ...

随机推荐

  1. Spring Cloud 之 全局配置

    在微服务架构中,全局配置的重要性不言而喻.SpringCloud的全局配置存储主要基于 Git 来实现,即配置信息存储在Git服务器,以统一的方式对外提供访问.在使用上分为 ConfigServer和 ...

  2. linux网络编程之socket编程(八)

    学习socket编程继续,今天要学习的内容如下: 先来简单介绍一下这五种模型分别是哪些,偏理论,有个大致的印象就成,做个对比,因为最终只会研究一个I/O模型,也是经常会用到的, 阻塞I/O: 先用一个 ...

  3. 匿名函数、sorted()、filter()、map()、递归

    一.匿名函数 1.lambda 匿名函数 方法 lambda 参数:返回值 (函数名统一叫lambda) def func(n): return n**2 print(func(3)) #这是一个普通 ...

  4. 【python】使用openpyxl解析json并写入excel(xlsx)

    目标: 将json文本解析并存储到excel中 使用python包 openpyx import simplejsonmport codecsimport openpyxl import os # d ...

  5. 云计算(1)-为什么要使用cloud

    云计算-为什么要使用cloud Many cloud providers(一些提供云服务的商家) EC2:提供computing services S3:提供the ability to store ...

  6. 聊聊MVCC多版本并发控制

    一.介绍 MVCC只在RR和RC 2个隔离级别下才能工作.MySQL的大多数事务存储引擎实现的都不是简单的行级锁机制.基于提升并发性能的考虑,它们一般都同时实现了MVCC. 通俗的来讲,MVCC是行级 ...

  7. hive 常用操作

    参考:https://www.cnblogs.com/jonban/p/10779938.html Hive 启动:hive 退出:hive>quit; show databases; use  ...

  8. Django中的Session与Cookie

    1.相同与不同 Cookie和Session都是为了记录用户相关信息的方式, 最大的区别就是Cookie在客户端记录而Session在服务端记录内容. 2.Cookie和Session之间的联系的建立 ...

  9. 判断字符串是否是IP地址

    #include <stdio.h>#include <string.h> bool isIP(const char* str); int main(){ char str[] ...

  10. yum安装mysql(指定版)

    首先需要删除已经存在的mysql,不然后面会报错: 快速删除: yum remove mysql mysql-server mysql-libs mysql-server 查找残余文件: rpm -q ...