this 是 JavaScript 中比较复杂的机制之一, 本篇文章希望可以带大家了解 this 相关的知识. 本文内容来自书籍你不知道的 JavaScript(上卷), 只是自己稍微整理一下.
为什么使用 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
复制代码
这段代码可以在不同的上下文对象 (me 和 you) 复用函数, 并且代码中使用了 this, 如果不使用 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, 由于它的语义性的问题, 会带来很多的误解:
误区一: 指向自身
- function foo(num) {
- console.log( "foo:" + num );
- this.count++;
- }
- foo.count = 0;
- var i;
- for (i=0; i<10; i++) {
- if (i> 5) {
- foo( i );
- }
- }
- // foo: 6
- // foo: 7
- // foo: 8
- // foo: 9
- // foo 被调用了多少次?
- console.log( foo.count ); // 0
复制代码
运行后我们发现 foo.count 仍然是 0, 说明 this 并没有指向 foo 自身.
误区二: 指向它的作用域
在某种情况下这个说法是正确的, 而在某些情况下这个说法又是错误的, 但是要注意!!this 在任何情况下都不指向函数的词法作用域!! 为什么这么说呢?
- function bar() {
- console.log(1);
- }
- this.bar(); // 1
复制代码
在上例中, this 指向了全局作用域, 但是只是特殊情况, 因此会有这个说法是正确的, 而在某些情况下这个说法又是错误的结论
- function foo() {
- var a = 2;
- this.bar();
- }
- function bar() {
- console.log( this.a );
- }
- foo();
复制代码
上文 this.a 视图引用 foo 词法作用域定义的变量 a, 这是永远也不可能实现的
this 到底是什么
说了它的使用方式以及误区, 那么 this 到底是什么呢? 首先明确一点: this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式.
调用位置
this 是在调用时被绑定的, 完全取决于函数的调用位置, 因此要搞清楚函数的调用位置, 但是某些编程模式会隐藏函数的调用位置, 最重要的分析它的调用栈(就是为了达到当前运行位置的所有调用函数)
- function baz() {
- // 当前调用栈是: 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 绑定的 4 种规则, 下次看到 this 出现时, 便可以使用这些规则
默认绑定
这是比较常见的函数调用类型: 独立函数调用
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- foo(); // 2
复制代码
如何判断应用了默认绑定呢? foo 是直接使用不带任何修饰符的函数进行引用调用的
注意: 如果使用了严格模式, this 会绑定到 undefined
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- foo(); // TypeError: this is undefined
复制代码
隐式绑定
当函数引用有上下文对象时(严格来说函数被对象 "拥有" 或者 "包含"), 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象
- function foo() {
- console.log( this.a );
- }
- var obj = {
- a: 2,
- foo: foo
- };
- obj.foo(); // 2
复制代码
严格来说, foo 不属于 obj 对象, 但是落脚点却指向 obj 对象, 因此你可以说函数被调用时 obj 对象 "拥 有" 或者 "包含" 它.
隐式丢失
一个最常见的问题就是: 隐式绑定会丢失绑定对象, 从而执行默认绑定
- 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"
复制代码
综上所述: 有两种情况会导致隐式绑定的绑定丢失.
进行引用赋值 var bar = obj.foo;
进行传递参数 doFoo( obj.foo );
显式绑定
- function foo() {
- console.log( this.a );
- }
- var obj = {
- a:2
- };
- foo.call( obj ); // 2
复制代码
通过 foo.call(..)可以在调用时强制把 this 绑定到 obj 上, 但是这样的方式也无法解决掉丢失绑定问题
- var a = 0;
- function foo() {
- console.log(this.a);
- }
- var obj1 = {
- a:1
- };
- var obj2 = {
- a:2
- };
- foo.call(obj1);// 1
- foo.call(obj2);// 2
复制代码
我们发现 this 随着调用一直在改变, 即 this 丢失.
我们可以通过以下方式解决:
硬绑定
创建一个包裹函数, 传入所有的参数并返回接收到的所有值
- 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.call( window ); // 2
复制代码
API 调用的上下文
许多内置函数都提供了一个可选参数, 通常被称为上下文 context, 其作用和 bind 一样, 确保你的回调 函数使用指定的 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
复制代码
new 绑定
JavaScript 中的 new 机制与面向对象的语言完全不同, 实际上, 在 JavaScript 中并不存在所谓的 "构造函数", 只有对与函数的 "构造调用"
使用 new 来调用函数, 或者说发生构造函数调用时的流程:
创建(构造一个全新的对象)
这个新对象会被执行 [[原型]] 连接
这个新对象会被绑定到函数调用的 this
如果函数没有返回其他对象, 那么 new 表达式中的函数调用会自动返回这个新对象
- function foo(a) {
- this.a = a;
- }
- var bar = new foo(2);
- console.log( bar.a ); // 2
复制代码
优先级
上面介绍了 this 的 4 种绑定规则, 那么它们的优先级谁高谁低呢, 首先, 确认一点的是默认绑定的优先级最低
比较隐式绑定和显示绑定
- 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 );
- console.log( obj1.a ); // 2
- obj1.foo.call( obj2, 3 );
- console.log( obj2.a ); // 3
- // new 和 隐式绑定同时存在, obj1 的 a 是 2, 而 this 指向了 bar
- var bar = new obj1.foo( 4 );
- console.log( obj1.a ); // 2
- console.log( bar.a ); // 4
复制代码
可以看出来 new 绑定高于隐式绑定
比较 new 绑定和显示绑定
由于 new 和 call/apply 无法一起使用, 我们可以使用硬绑定测试优先级
- function foo(something) {
- this.a = something;
- }
- var obj1 = {};
- var bar = foo.bind( obj1 );
- bar( 2 );
- console.log( obj1.a ); // 2
- var baz = new bar(3);
- console.log( obj1.a ); // 2
- console.log( baz.a ); // 3
复制代码
首先 bar 被强制绑定到 obj1 上, 但是 new bar(3)没有预期把 obj1.a 修改为 3 因此 new 的优先级大于硬绑定.
但是使用刚开始的裸 bind
- function foo(something) {
- this.a = something;
- }
- function bind(obj, fn) {
- return function() {
- fn.apply(obj. arguments);
- }
- }
- var obj1 = {};
- var bar = bind( obj1, foo );
- bar( 2 );
- console.log( obj1.a ); // 2
- var baz = new bar(3);
- console.log( obj1.a ); // 3
- console.log( baz.a ); // undefined
复制代码
会惊奇地发现, new bar(3)把 obj1.a 修改为 3 因此内置 bind 的实现是非常复杂的, 不在此进行研究, 既然这么复杂, 为什么还要使用呢?
这种做法称为 "部 分应用", 是 "柯里化" 的一种, 它的主要目的是预设函数的一些参数, 这样在使用 new 进行初始化时就可以只传入其余的参数.
- 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:
函数是否在 new 中调用(new 绑定)?
函数是否通过 call,apply(显式绑定)或者硬绑定调用
函数是否在某个上下文对象中调用(隐式绑定)
如果都不是的话, 使用默认绑定
绑定例外
规则总有例外, 当你认为应用了其他规则时, 有可能只应用了默认规则
被忽略的 this
如果我们把 null 或者 undefined 作为 this 的绑定对象传递入 call,apply, 或者 bind, 会使用默认绑定规则
- function foo() {
- console.log( this.a );
- }
- var a = 2;
- foo.call( null ); // 2
复制代码
那么什么情况下会使用这种方式呢? 利用 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
复制代码
es6 中可以使用... 来代替 ``apply(...)```, 但是 ES6 中没有柯里化的相关方法
忽略 this 会存在一个问题, 比如第三方库的函数真的使用了 this, 我们这种方式把 this 绑定到了全局作用域, 会存在问题, 需要使用更安全的 this, 创建空的非委托的对象
- Object.create( null )
- 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 的能力
这种叫做软绑定.
- if (!Function.prototype.softBind) {
- Function.prototype.softBind = function(obj) {
- var fn = this;
- console.log('fn', this);
- // 捕获所有 curried 参数
- var curried = [].slice.call( arguments, 1 );
- var bound = function() {
- console.log('this', this);
- return fn.apply(
- (!this || this === (window || global)) ? obj : this,
- curried.concat.apply( curried, arguments )
- );
- }
- bound.prototype = Object.create( fn.prototype );
- return bound;
- }
- }
- 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 <---- 应用了软绑定
复制代码
this 词法
最后介绍 es6 中的箭头函数, 箭头函数不使用 this 的四种规则, 而是根据外层 (函数或者全局) 作用域来决定 this
- function foo() {
- // 返回一个箭头函数
- return (a) => {
- //this 继承自 foo()
- console.log( this.a );
- };
- }
- var obj1 = {
- a:2
- };
- var obj2 = {
- a:3
- };
- var bar = foo.call( obj1 );
- bar.call( obj2 ); // 2, 不是 3 !
复制代码
foo 的 this 绑定到了 obj1,bar 引用箭头函数的 this 也会绑定到 obj1, 箭头函数的绑定无法被修改
来源: https://juejin.im/post/5b45aeebe51d45195a710721