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 指向。

 

posted @ 2020-05-11 10:11  elfpower  阅读(224)  评论(0编辑  收藏  举报