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

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

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. mysql案例分析

    工作中,需要设计一个数据库存储,项目的需求大致如下: (1)对于每个用户,需要存储一个或多个库, 每个库, 由一个用户标识来标识,这里成为clientFlag. (2) 对于每一个库,结构如下: 1) ...

  2. AttributeError: 'NoneType' object has no attribute 'extend'

    Python使用中可能遇到的小问题 AttributeError: 'NoneType' object has no attribute 'extend' 或者AttributeError: 'Non ...

  3. python_面向对象——多态

    1.同一接口,多种形态 class Document: def __init__(self,name): self.name = name def show(self): # 异常处理:提示子类必须把 ...

  4. uni-app之导航配置pages.json

    1.基础配置,各个页面都要在这里边引入. 2.基础配置,头部导航左右上脚的buttons设置. 3.如果没有权限展示底部导航的需求,可以直接在此文件配置底部导航.

  5. POJ1185 炮兵阵地 和 POJ2411 Mondriaan's Dream

    炮兵阵地 Language:Default 炮兵阵地 Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 34008 Accepted ...

  6. [笔记] 三元环 && 四元环计数

    Thanks to i207M && iki9! 三元环计数 无向图的三元环计数 我们首先需要对无向边按一定规则定向: 设 \(in[u]\) 表示 \(u\) 的度数 若 \(in[ ...

  7. 代码 | 自适应大邻域搜索系列之(7) - 局部搜索LocalSearch的代码解析

    前言 好了小伙伴们我们又见面了,咳咳没错还是我.不知道你萌接连被这么多篇代码文章刷屏是什么感受,不过,酸爽归酸爽.今天咱们依然讲代码哈~不过今天讲的依然很简单,关于局部搜索LocalSearch的代码 ...

  8. Centos7.x 安装 Supervisord

    [环境] 系统:Centos 7.3 软件:supervisord [安装Supervisord] yum install epel-release yum install -y supervisor ...

  9. fatal: 'origin' does not appear to be a git repository

    git push时报以下错误: fatal: 'origin' does not appear to be a git repository fatal: Could not read from re ...

  10. Django基础之创建admin账号

    1. 首先我们要新建一个用户名, 用来登录管理网站,可以使用如下命令: python manage.py createsuperuser 2. 输入想要使用的用户名: Username(leave b ...