JavaScript 中的 this
在 JavaScript 中 this 给人的感觉总是飘忽不定的,很多没有编程基础的或者有其他语言基础的小伙伴都会觉得JavaScript 中的 this 的指向很迷,明明同一个函数这里调用能输出自己想要的东西,换个地方却又不生效了。其实 this 的指向在函数定义的时候是还确定不了的,只有函数执行的时候才能确定 this 到底指向谁。在绝大多数情况下,函数的调用方式决定了 this 的值。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。总的来说确定 this 指向能大致分为下面几种情况:
- 全局中执行时的 this
- 函数中执行时的 this
- 作为对象的方法被调用时的 this
- 作为构造函数时的 this
- 作为一个DOM处理函数时的 this
- 作为一个内联处理函数时的 this
- 箭头函数的 this
- 使用 apply、call、bind 时的 this
JavaScript 中的 this,全局中执行时
1.在浏览器中,全局 this 指向的是 [object Window]
console.log(this === window); // true console.log(this); // Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage, sessionStorage: Storage, webkitStorageInfo: DeprecatedStorageInfo…}
2.在 Node.js 中,全局 this 指向默认为一个空对象(实际指向 module.exports),函数中的 this 指向全局对象 global
JavaScript 中的 this,函数中执行时
1.在函数中,this的值取决于函数被调用的方式
// 在全局调用函数时,this指向全局对象 function fn(){ return this; } //在浏览器中: fn() === window; // true //在Node中: fn() === global; // true
2. 严格模式: 'use strict'
在严格模式下,如果 this 没有被执行环境(execution context)定义,那它将保持为 undefine
// 在严格模式的情况下函数调用,这里的 `this` 并不会指向全局,而是 `undefined`,这样的做法是为了消除 js 中一些不严谨的行为 'use strict'; function fn() { return this; }; fn() === undefined; // true // 而通过全局对象window进行函数调用,this 会指向 Window window.f2() // Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
放在自执行函数中会更好,可以有效避免污染全局:
(function (){ 'use strict'; console.log(this); }()); // undefined
JavaScript 中的 this,作为对象的方法被调用时
当函数作为对象里的方法被调用时,它们的 this 是调用该函数的对象
var man = { name: 'gwg', sayName: function() { console.log(this.name); } } man.sayName(); // 'gwg'
这时候,'this' 指向当前的这个对象 man;
当然,我们还可以这么写,并增加其他对象对 myName 方法调用:
function myName() { console.log(this.name); } var man ={}; man.name = 'gwg'; man.sayName = myName; var boy = {name:'elf'} boy.sayName = myName; window.name = man.name; window.sayName = boy.sayName; man.sayName(); // 'gwg' boy.sayName(); // 'elf' window.sayName(); // 'gwg' window.boy.sayName(); // 'elf'
无论怎么写,只要是 某个对象 去调用 这个方法,那么 this 必然指向 这个对象,this 不受函数定义方式或位置的影响
如果把对象的方法赋值给一个变量,然后直接调用这个变量:
var o= { n: 'gwg', f: function() { console.log(this); } } var fn = o.f; fn();// Window {parent: Window, opener: null, top: Window, length: 2, frames: Window, …}
因为fn是全局变量,fn 就相当于 window.fn,所以此时这个函数的 this 的指向是 Window
this永远指向的是最后调用它的对象
function fn() { console.log(this.a) } var t1 = { a: 1, f: fn, t2: { a:2, f:fn, t3: { a:3, f:fn } } } t1.f(); // 1 t1.t2.f(); // 2 t1.t2.t3.f(); // 3
JavaScript 中的 this,作为构造函数时
当一个函数用作构造函数时(使用new关键字),它的 this 被绑定到正在构造的新对象。
function Person(name) { this.name = name; console.log(this); } var p = new Person('gwg'); // Person {name: "gwg"}
我们可以看到当作构造函数调用时,'this' 指向了这个构造函数调用时候实例化出来的对象;
当然,构造函数其实也是一个函数,如果我们把它当作一个普通函数执行,这个 'this' 仍然执行全局:
function Person(name) { this.name = name; console.log(this); } var p = Person('gwg'); // Window
虽然构造器返回的默认值是this所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this对象)。
JavaScript 中的 this,作为一个DOM处理函数时
当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非addEventListener的函数动态添加监听函数时不遵守这个约定)。
// 被调用时,将关联的元素变成天蓝色 function bluify(e){ console.log(this === e.currentTarget); // 总是 true // 当 currentTarget 和 target 是同一个对象时为 true console.log(this === e.target); this.style.backgroundColor = 'skyblue'; } // 获取文档中的所有元素的列表 var elements = document.getElementsByTagName('*'); // 将bluify作为元素的点击监听函数,当元素被点击时,就会变成天蓝色 for(var i=0 ; i<elements.length ; i++){ elements[i].addEventListener('click', bluify, false); }
JavaScript 中的 this,作为一个内联处理函数时
当代码被内联 on-event 处理函数调用时,它的this指向监听器所在的 DOM 元素:
<button onclick="console.log(this.tagName.toLowerCase());"> Show this </button>
会打印出 button。注意只有外层代码中的this是这样设置的:
<button onclick="console.log((function(){return this})());"> Show inner this </button>
在这种情况下,没有设置内部函数的 this,所以它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。
JavaScript 中的 this,箭头函数
使用箭头函数主要有两大优点:
更简短
names = ['gwg','titan','magicdark','elfpower','king'] // 不使用箭头函数的写法 names.map(function (name) { return name.length; }) // [3, 5, 9, 8, 4] // 改成箭头函数 names.map((name) => { return name.length; }) // [3, 5, 9, 8, 4] // 箭头函数只有单个参数时,可省略参数括号 names.map( name => { return name.length; }) // [3, 5, 9, 8, 4] // 箭头函数的函数体只有一个返回语句是,可以省略return 和 花括号 // 注意不要带 ";" names.map( name => name.length) // [3, 5, 9, 8, 4] // 使用参数解构 // 需要注意的是字符串 `"length"` 是我们想要获得的属性的名称,而 `myNamesLen` 则只是个变量名, // 可以替换成任意合法的变量名 names.map( ({'length':myNamesLen}) => myNamesLen) // [3, 5, 9, 8, 4]
不绑定 this
使用 setInterval 时,想要获取 this,但和自己想要实现的效果并不一样
function Person() { // Person() 构造函数定义 `this`作为它自己的实例. this.age = 0; setInterval(function growUp() { // 注意在非严格模式时, growUp()函数定义 `this`作为全局对象, // 与在 Person()构造函数中定义的 `this`并不相同. console.log(this.age); // NAN this.age++; }, 1000); } var p1 = new Person();
不用箭头函数额解决办法:(不是唯一办法,但这种办法相对简洁)
function Person() { var that = this; that.age = 0; setInterval(function growUp() { // 回调引用的是`that`变量, 其值是预期的对象. console.log(that.age);// 可打印出不断增加的 that.age that.age++; }, 1000); } var p2 = Person()
使用箭头函数:箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this
。因此,在下面的代码中,传递给 setInterval
的函数内的 this
与封闭函数中的 this
值相同
function Person(){ this.age = 0; setInterval(() => { console.log(this.age) this.age++; // |this| 正确地指向 p3 实例 }, 1000); } var p3 = new Person();
简单来说, 箭头函数中的 this 只和定义它时候的作用域的 this 有关,而与在哪里以及如何调用它无关,同时它的 this 指向是不可改变的。
JavaScript 中的 this,使用apply、call、bind时
call、 apply、 bind 等三个方法,可以更改函数中的 'this' 指向:
call:
- 它会立即执行函数,第一个参数是指定执行函数中 'this' 的上下文,后面的参数是执行函数需要传入的参数;
- call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
- call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
语法:func.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
- thisArg:
- 在 func 函数运行时使用的 this 值。this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
- arg1, arg2, ...
- 指定的参数列表。
apply:
- 它会立即执行函数,第一个参数是指定执行函数中 'this' 的上下文,第二个参数是一个数组,是传给执行函数的参数(与 'call' 的区别);
- 使用 apply, 你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。
语法:func.apply(thisArg, [argsArray])
参数:
- thisArg:
- 在 func 函数运行时使用的 this 值。this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
- argsArray(可选):
- 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。
- 如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。
- 从 ECMAScript 5 开始可以使用类数组对象。
bind:
- 它不会执行函数,而是返回一个新的函数(绑定函数),这个新的函数被指定了 'this' 的上下文,后面的参数是执行函数需要传入的参数;
- 绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
语法:var fn = func.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
- thisArg:
- 调用绑定函数时作为 this 参数传递给目标函数的值。
- 如果使用 new 运算符构造绑定函数,则忽略该值。
- 当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。
- 如果 bind 函数的参数列表为空,或者 thisArg 是 nul l或 undefined,执行作用域的 this 将被视为新函数的 thisArg。
- arg1, arg2, ...(可选):
- 当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
call 、bind 、 apply 的异同:
这三个函数的第一个参数都是 this 的指向对象,后面参数略有不同。
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 func.call(obj, arg1, arg2, ..., argn )。
apply 的所有参数都必须放在一个数组里面传进去 func.apply(obj,[arg1, arg2, ..., argn ])。
bind 除了返回是函数以外,它 的参数和 call 一样。
举个小例子:
var name ='全局'; var age = 999; var man = { name: 'magicdark', age: this.age, say: function (job, skll) { console.log('我叫'+this.name+',今年'+this.age+','+skll+',想成为一名优秀的'+job+'') } } var me = { name: 'titan', age: 25 } // 基本使用异同: man.say.call(me, '前端工程师', '热爱编程') // 我叫titan,今年25,热爱编程,想成为一名优秀的前端工程师(后面的参数与say原来的参数顺序一样) man.say.apply(me, ['前端工程师', '热爱编程']) // 我叫titan,今年25,热爱编程,想成为一名优秀的前端工程师(和call基本一样,注意后面的参数放在一个数组) man.say.bind(me, '前端工程师', '热爱编程')() // 我叫titan,今年25,热爱编程,想成为一名优秀的前端工程师(和call基本一样,注意返回的函数要加()执行) // 基本使用传参: man.say.call({name: '小明',age:18}, '中锋', '身高199') // 我叫小明,今年22,身高199,想成为一名优秀的中锋 (this指向{name: '小明',age:18}对象) man.say.apply({age:99}, ['魔导师', '精通黑魔法']) // 我叫undefined,今年99,精通黑魔法,想成为一名优秀的魔导师(this指向{age:99}对象,没name,所以undefined) man.say.bind(null,'参1','参2')() // 我叫全局,今年999,参2,想成为一名优秀的参1 (第一个参数为null,this指向全局对象,有全局变量name和age) man.say.bind()() // 我叫全局,今年999,undefined,想成为一名优秀的undefined (不传任何参数,this指向全局对象,say方法无参数传入,全部undefined)
使用 new 运算符构造
// 用call会报错,原因是我们去 `new` 了 `Person.call` 函数,而非 `Person` ,这里的函数不是一个构造函数 function Person(name) { this.name = name; console.log(this); } var obj = { name: 'gwg2' }; var p = new Person.call(obj, 'gwg'); // Uncaught TypeError: Person.call is not a constructor(…) // 用 new 运算符构造,它会表现为目标函数已经被构建完毕。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数 function Person(name) { this.name = name; console.log(this); } var obj = { name: 'gwg2' }; var Person2 = Person.bind(obj); var p = new Person2('gwg');// Person {name: "gwg"} console.log(obj);// {name: "gwg2"}
为箭头函数指定 this
我们来定义一个全局下的箭头函数,因此这个箭头函数中的 `this` 必然会指向全局对象,如果用 `call` 方法改变 `this` 呢:
var afoo = (a) => { console.log(a); console.log(this); } afoo(1); // 1 // Window var obj = { name: 'gwg' }; afoo.call(obj, 2); // 2 // Window
可以看到,这里的 'call' 指向 'this' 的操作并没有成功,所以可以得出: 箭头函数中的 this 在定义它的时候已经决定了(执行定义它的作用域中的 this),与如何调用以及在哪里调用它无关,包括 (call, apply, bind) 等操作都无法改变它的 this 指向。