《你不知道的JavaScript(上)》笔记——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 的调用位置
绑定规则
默认绑定。
最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
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
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象, 或者说是否被某个对象拥有或者包含, 不过这种说法可能会造成一些误导。
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 绑定是非常常见的。
显式绑定
像call, apply, bind这三种可以直接指定 this 的绑定对象的方法,我们称之为显式绑定。
*如果你传入了一个原始值(字符串类型、 布尔类型或者数字类型) 来当作 this 的绑定对象, 这个原始值会被转换成它的对象形式(也就是 new String(..)、 new Boolean(..) 或者new Number(..))。 这通常被称为“装箱”。
new绑定
JavaScript 中 new 的机制实际上和面向类的语言完全不同。
在 JavaScript 中, 构造函数只是一些使用 new 操作符时被调用的函数。 它们并不会属于某个类, 也不会实例化一个类。 实际上,它们甚至都不能说是一种特殊的函数类型, 它们只是被 new 操作符调用的普通函数而已。
使用 new 来调用函数, 或者说发生构造函数调用时, 会自动执行下面的操作:
- 创建(或者说构造) 一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象, 那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
判断this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。 可以按照下面的顺序来进行判断:
- 函数是否在 new 中调用(new 绑定) ? 如果是的话 this 绑定的是新创建的对象。
var bar = new foo() - 函数是否通过 call、 apply(显式绑定) 或者硬绑定调用? 如果是的话, this 绑定的是
指定的对象。
var bar = foo.call(obj2) - 函数是否在某个上下文对象中调用(隐式绑定) ? 如果是的话, this 绑定的是那个上
下文对象。
var bar = obj1.foo() - 如果都不是的话, 使用默认绑定。 如果在严格模式下, 就绑定到 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全面解析的更多相关文章
- 你不知道的JavaScript上卷笔记
你不知道的JavaScript上卷笔记 前言 You don't know JavaScript是github上一个系列文章 初看到这一标题的时候,感觉怎么老外也搞标题党,用这种冲突性比较强的题目 ...
- 读书笔记-你不知道的JavaScript(上)
本文首发在我的个人博客:http://muyunyun.cn/ <你不知道的JavaScript>系列丛书给出了很多颠覆以往对JavaScript认知的点, 读完上卷,受益匪浅,于是对其精 ...
- 你不知道的javascript读书笔记3
概述 这是我看<你不知道的JavaScript(中卷)>中关于类型检查的笔记,供以后开发时参考,相信对其他人也有用. typeof 我们知道js中有七种内置类型:undefined, nu ...
- 《你不知道的JavaScript》笔记(一)
用了一个星期把<你不知道的JavaScript>看完了,但是留下了很多疑惑,于是又带着这些疑惑回头看JavaScript的内容,略有所获. 第二遍阅读这本书,希望自己能够有更为深刻的理解. ...
- 【你不知道的javaScript 上卷 笔记3】javaScript中的声明提升表现
console.log( a ); var a = 2; 执行输出undefined a = 2; var a; console.log( a ); 执行输出2 说明:javaScript 运行时在编 ...
- 《你不知道的javascript(上)》笔记
作用域是什么 编译原理 分词/词法分析 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元 解析/语法分析 词法单元流(数组)转换成一个由元素逐级嵌套所组成 ...
- <你不知道的JavaScript>读书笔记
近几天看了一本不错的 JavaScript 的书,是 Kyle Simpson 写的 <You Don't know JS>.这本书是 Kyle Simpson 在 Github 上的开源 ...
- 【你不知道的javaScript 上卷 笔记7】javaScript中对象的[[Prototype]]机制
[[Prototype]]机制 [[Prototype]]是对象内部的隐试属性,指向一个内部的链接,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototyp ...
- 【你不知道的javaScript 上卷 笔记6】javaScript中的对象相关内容
一.创建一个对象的语法 var myObj = { key: value // ... };//字面量 var myObj = new Object(); //new myObj.key = valu ...
- 【你不知道的javaScript 上卷 笔记5】javaScript中的this词法
function foo() { console.log( a ); } function bar() { var a = 3; foo(); } var a = 2; bar(); 上面这段代码为什 ...
随机推荐
- 微信APP支付(基于Java实现微信APP支付)
步骤: 导入maven依赖 <!--微信支付--> <dependency> <groupId>com.github.wxpay</groupId> & ...
- 二进制部署kubernetes集群(上篇)
1.实验架构 1.1.硬件环境 准备5台2c/2g/50g虚拟机,使用10.4.7.0/24 网络 .//因后期要直接向k8s交付java服务,因此运算节点需要4c8g.不交付服务,全部2c2g足够. ...
- Opencv---零碎记录
OpenCV支持CPU和OpenCL推断,但OpenCL只支持Intel自家GPU,Satya设置了CPU推断模式(cv.dnn.DNN_TARGET_CPU) https://docs.opencv ...
- Clipper库中文文档详解
简介 Clipper Library(以下简称为Clipper库或ClipperLib或Clipper)提供了对线段和多边形的裁剪(Clipping)以及偏置(offseting)的功能 和其他的裁剪 ...
- .NET Core SignalR 和 .NET SignalR 区别
由于要转 .NET Core ,对于以前用到的一些进行迁移. 在迁移 SignalR 的时候发现 .NET Core 下的和 .NET 下的区别还是挺大的. 功能差异 自定重新连接 .NET 下的 S ...
- LightOJ-1008-Fibsieve`s Fantabulous Birthday(推公式)
链接: https://vjudge.net/problem/LightOJ-1008 题意: Fibsieve had a fantabulous (yes, it's an actual word ...
- SCSS 教程
https://www.jianshu.com/p/a99764ff3c41 https://www.sass.hk/guide/ 1. 使用变量; sass让人们受益的一个重要特性就是它为css引入 ...
- 【素数判定/筛法进阶算法】-C++
今天我们来谈一谈素数的判定/筛法. 对于每一个OIer来说,在漫长的练习过程中,素数不可能不在我们的眼中出现,那么判定/筛素数也是每一个OIer应该掌握的操作,那么我们今天来分享几种从暴力到高效的判定 ...
- 005_Python3 运算符
什么是运算符? 本章节主要说明Python的运算符.举个简单的例子 4 +5 = 9 . 例子中,4 和 5 被称为操作数,"+" 称为运算符. Python语言支持以下类型的运算 ...
- 四十五.加密与解密 AIDE入侵检测系统 扫描与抓包
一.加密与解密 1.1 常见的加密算法 对称加密:怎么加密,就怎么解密 DES Date Encryption Standard AES Advance Encryption Standard 非对称 ...