前言 误以为 this 指向函数自身 根据 this
的英语语法,很容易将函数中出现的 this 理解为函数自身。在 javascript
当中函数作为一等公民,确实可以在调用的时候将属性值存储起来。但是如果使用方法不对,就会发生与实际预期不一致的情况。具体情况,请看下面代码:
1 2 3 4 5 6 7 8 9 10 function fn (num ) { this .count++; } fn.count = 0 ; for (var i = 0 ; i < 3 ; i++) { fn(i); } console .log(fn.count);
如果 fn
函数里面的 this
指向自身函数,那么 count
属性的属性值就应该产生变化,但实际上却是纹丝不动。对于这个问题,有些人会利用作用域来解决,比如这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 var data = { count: 0 , }; function fn (num ) { data.count++; } for (var i = 0 ; i < 3 ; i++) { fn(i); } console .log(data.count);
又或者更直接的这么写:
1 2 3 4 5 6 7 8 9 10 11 function fn (num ) { fn.count++; } fn.count = 0 ; for (var i = 0 ; i < 3 ; i++) { fn(i); } console .log(fn.count);
虽然这两种方式都输出了正确的结果,但是却避开了 this
到底绑定在哪里的问题。如果对一个事物的工作原理不清晰,就往往会产生头痛治头,脚痛治脚的问题,从而导致代码变得的丑陋,而且维护性也会变得很差。
实际上,this 提供了一种更优雅的方法来隐式’传递’一个对象的引用,因此可以将 API 设计得更加简洁并且易于复用。
绑定规则 默认绑定规则 无论是 全局环境
还是 函数独立调用(包括嵌套函数,IIFE,闭包等)
,this
默认绑定到 window
。在严格模式下,会将函数体里的 this
默认绑定为 undefined
。以避免全局变量的污染。
全局环境 1 console .log(this === window );
函数独立调用 1 2 3 4 function fn ( ) { console .log(window === this ); } fn();
函数 fn
是直接在全局作用域下调用的,没有带其他任何修饰,这种情况下,函数调用的时候使用了 this
的默认绑定,指向了全局对象。
fn
函数中的 this
指向了全局变量,所以 this.count++
相当于 window.count++
(浏览器环境下),当然不会对 fn
函数的 count
属性产生影响。
嵌套函数 1 2 3 4 5 6 7 8 9 10 11 12 var a = 0 ;var obj = { a: 2 , foo: function ( ) { function test ( ) { console .log(this .a); } test(); }, }; obj.foo();
IIFE(立即执行函数) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var a = 0 ;function foo ( ) { (function test ( ) { console .log(this .a); })(); } var obj = { a: 2 , foo: foo, }; obj.foo(); var a = 0 ;var obj = { a: 2 , foo: function ( ) { function test ( ) { console .log(this .a); } test(); }, }; obj.foo();
闭包 1 2 3 4 5 6 7 8 9 10 11 12 var a = 0 ;function foo ( ) { function test ( ) { console .log(this .a); } return test; } var obj = { a: 2 , foo: foo, }; obj.foo()();
严格模式 1 2 3 4 5 6 'use strict' ;function F ( ) { this .a = 1 ; } F();
严格模式下在 函数体
里的 this
不允许指向全局对象。
隐式绑定规则 如果函数在以对象为上下文进行调用,那么 this
会绑定到调用这个函数的对象:
1 2 3 4 5 6 7 8 var obj = { a: 1 , fn: function ( ) { console .log(this .a); }, }; obj.fn();
即使函数声明不在对象当中,this
指向仍会产生变化:
1 2 3 4 5 6 7 8 function fn ( ) { console .log(this .a); } var obj = { a: 1 , fn: fn, }; obj.fn();
由此可见,this
的绑定,不与函数定义的位置有关,而是与调用者和调用方式有关。
在隐式的绑定规则下,有一些特殊的地方,需要注意:
多层对象调用 在多层对象引用下,this
指向的是调用的函数的那个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function fn ( ) { console .log(this .a); } var obj3 = { a: 3 , fn: fn, }; var obj2 = { a: 2 , obj3: obj3, }; var obj = { a: 1 , obj2: obj2, }; obj.obj2.obj3.fn();
隐式丢失 [函数别名] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var a = 0 ;function foo ( ) { console .log(this .a); } var obj = { a: 2 , foo: foo, }; var bar = obj.foo;bar(); var a = 0 ;var bar = function foo ( ) { console .log(this .a); }; bar();
虽然 bar
引用了 obj.foo
,但是函数的调用方式,仍是不带任何修饰的,所以 this
还是绑定在了 window
上。
[参数传递] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var a = 0 ;function foo ( ) { console .log(this .a); } function bar (fn ) { fn(); } var obj = { a: 2 , foo: foo, }; bar(obj.foo); var a = 0 ;function bar (fn ) { fn(); } bar(function foo ( ) { console .log(this .a); });
[内置函数] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 var a = 0 ;function foo ( ) { console .log(this .a); } var obj = { a: 2 , foo: foo, }; setTimeout (obj.foo, 100 ); var a = 0 ;setTimeout (function foo ( ) { console .log(this .a); }, 100 );
setTimeout
是 window
的方法,setTimeout
在调用传入函数的时候,如果这个函数没有指定了的 this
,那么它会做一个隐式的操作 —- 自动地注入全局上下文 window
。
隐式绑定 this
不是一种很推荐的方式,因为很有可能就发生丢失的情况,如果业务当中对 this
的绑定有要求,建议还是使用显示绑定的方式。
显式绑定规则 显示绑定就是利用函数原型上的 apply
与 call
方法来对 this
进行绑定。用法就是把想要绑定的对象作为第一个参数传进去。如果传入了一个原始值(字符串,布尔类型,数字类型),来当做 this
的绑定对象,这个原始值转换成它的对象形式。
如果你把 null
或者 undefined
作为 this
的绑定对象传入 call/apply/bind
,这些值会在调用时被忽略,实际应用的是默认绑定规则。
1 2 3 4 5 6 7 function fn ( ) { console .log(this ); } var obj = {};fn.call(obj);
有些时候会想将函数的 this
绑定在某个对象上,但是不需要立即调用,这样的话,直接利用 call
或者 apply
是无法做的。
1 2 3 4 5 6 7 8 9 10 11 12 13 function fn ( ) { console .log(this ); } function bind (fn ) { fn(); } var obj = { fn: fn, }; bind.call(obj, fn);
上面这个例子,看似好像可以,但实际上是 bind
函数的 this
绑定到了 obj
这个对象,但是 fn
仍然是没有任何修饰的调用,所以 fn
仍然是默认的绑定方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function fn ( ) { console .log(this ); } function bind (fn, obj ) { return function ( ) { fn.apply(obj, arguments ); }; } var obj = { fn: fn, }; var fun = bind(fn, obj);fun();
这样调用,就可以将灵活多变的 this
,牢牢的控制住了,因为 fn
的调用方式为 apply
调用。所以,this
就被绑定在传入的 obj
对象上,在 ES5
当中,函数的原型方法上多了一个 bind
,效果与上面的函数基本一致。
apply/call/bind 实现 注: call 性能比 apply 好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Function .prototype.apply2 = function (context = window ) { if (this === Function .prototype) { return undefined ; } const fn = Symbol (); context[fn] = this ; let result; let args = arguments [1 ]; if (args) { result = context[fn](...args); } else { result = context[fn](); } delete context[fn]; return result; }; Function .prototype.bind2 = function (content ) { if (this === Function .prototype) { throw new TypeError ('Error' ); } let fn = this ; let args = [...arguments].slice(1 ); let resFn = function ( ) { return fn.apply(this instanceof resFn ? this : content, args.concat(...arguments)); }; function tmp ( ) {} tmp.prototype = this .prototype; resFn.prototype = new tmp(); return resFn; };
其他 new new
是一个被很多人误解的一个关键字,但实际上 javascript
的 new
与传统面向对象的语言完全不同。 个人把 new
理解为一种特殊的函数调用,当使用 new
关键字来调用函数的时候,会执行下面操作:
创建一个全新的对象;
将构造函数的作用域赋给新对象(this
就指向了这个新对象),将新对象的 __proto__
指向构造函数的 prototype
,设置新对象的内部属性,可访问性等;
执行构造函数中的代码(为这个新对象添加属性);
如果构造函数返回值为基本类型或者为 this
又或者不返回任何值,那么将会返回这个创建的新对象;如果返回了一个对象,那么则会返回这个对象。
new 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function New (func ) { var res = {}; if (func.prototype !== null ) { res.__proto__ = func.prototype; } var ret = func.apply(res, Array .prototype.slice.call(arguments , 1 )); if ((typeof ret === 'object' || typeof ret === 'function' ) && ret !== null ) { return ret; } return res; } var obj = New(A, 1 , 2 );var obj = new A(1 , 2 );
故构造函数中的 this
指向构造函数创建的对象实例。
1 2 3 4 5 6 7 8 9 10 function fn (a ) { this .a = a; } fn.prototype.hi = function ( ) { console .log('hi' ); }; var obj = new fn(2 );console .log(obj);
class 类和模块的内部,默认就是 严格模式
。上文说到在严格模式下,会将 this
默认绑定为 undefined
。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Logger { printName (name = 'there' ) { this .print(`Hello ${name} ` ); } print (text ) { console .log(text); } } const logger = new Logger();const { printName } = logger;printName();
可以将 this
绑定到实例上:
1 2 3 4 5 6 7 class Logger { constructor ( ) { this .printName = this .printName.bind(this ); } }
class 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function inherit (subType, superType ) { subType.prototype = Object .create(superType.prototype, { constructor : { enumerable: false , configurable: true , writable: true , value: subType, }, }); Object .setPrototypeOf(subType, superType); }
寄生组合式继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function inheritPrototype (subType, superType ) { var prototype = Object .create(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function SuperType (name ) { this .name = name; this .colors = ['red' , 'blue' , 'green' ]; } SuperType.prototype.sayName = function ( ) { console .log(this .name); }; function SubType (name, age ) { SuperType.call(this , name); this .age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function ( ) { console .log(this .age); };
箭头函数 箭头函数本身不创建 this
,在它声明时可以捕获别人的 this
供自己使用。this
一旦被捕获,以后将不再变化,即不会被 call、apply 或 bind
影响。
传统写法:
1 2 3 4 5 6 7 8 9 10 11 12 function fn ( ) { var _this = this ; setTimeout (function ( ) { console .log(_this.a); }, 100 ); } var obj = { a: 2 , }; fn.call(obj);
箭头函数写法:
1 2 3 4 5 6 7 8 9 10 11 12 function fn ( ) { setTimeout (() => { console .log(this .a); }, 100 ); } var obj = { a: 2 , }; fn.call(obj);
如果用 apply
显式绑定:
1 2 3 4 5 6 var a = 1 ;let obj = { a : 2 };let fn = () => { console .log(this .a); }; fn.apply(obj);
事件函数 如果是在事件函数当中,this
的绑定是指向触发事件的 DOM
元素的,
1 2 3 4 5 6 7 $('body' )[0 ].addEventListener( 'click' , function ( ) { console .log(this ); }, false );
点击 body
元素之后,控制台则会显示 body
元素。
小结 如果想判断一个函数的 this
绑定在哪里,首先是找到函数的调用位置,之后是按照规则来判断。
如果函数调用时没有任何修饰条件,那么在严格模式下则会绑定到 undefined
,非严格模式下会绑定到全局 window
。
如果是用对象做上下文,来对函数进行调用,那么则会绑定到调用的这个对象上。
如果是用 call
或者 apply
方法来进行调用的,则会绑定到第一个传入参数上。
如果是使用 new
关键字来调用函数的,则会绑定到新创建的那个对象上.
如果是在事件函数内,则会绑定到触发事件的那个 DOM
元素上。
绑定优先级:显式绑定 > 隐式绑定 > 默认绑定, new 绑定 > 隐式绑定 > 默认绑定