前端 2019-09-11 10:36:17

调用

JS(ES5)里面有四种函数调用形式: 方法调用模式(当一个函数被保存为对象的一个属性时,称它为方法) 函数调用模式(当一个函数并非一个对象的属性时)、 构造器调用模式、 call或者apply调用模式

// 函数调用模式
func(p1, p2) 
// 方法调用模式
obj.child.method(p1, p2) 
// call或者apply调用模式
func.call(context, p1, p2)

func.call(context, p1, p2)才是正常调用形式, 其他两种都是语法糖,可以等价地变为 call 形式:

func(p1, p2)等价于 func.call(undefined, p1, p2);

obj.child.method(p1, p2) 等价于 obj.child.method.call(obj.child, p1, p2);

所以实际上函数的调用方式就一种: func.call(context, p1, p2) 我们所说的this就是context上下文

this指向

1.全局环境

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。

// 浏览器中
this === window // true
'use strict'
this === window // true

2.函数调用模式

function func() {
    console.log(this);
}
func(); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
// 等价于
func.call(undefined) // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

那为什么这段代码运行在浏览器中打印出的是window对象,而不是undefined呢?

因为浏览器里有一条规则:

如果你传的 context 就 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)

3.方法调用模式

var person = {
  name: 'unknown',
  doSomething: function () {
    console.log(this.name); 
  },
  alfred: {
    name: 'alfred',
    doSomething: function () {
      console.log(this.name); 
    }
  }
};
person.doSomething(); // unknown
person.alfred.doSomething(); // alfred

person.doSomething() 等价于 person.doSomething.call(person)

person.alfred.doSomething() 等价于 person.alfred.doSomething.call(person.alfred)

[]中函数的this

function fn (){ console.log(this) }
function fn2(){ console.log('fn2')}
var arr = [fn, fn2]
arr[0]() // [ƒ, ƒ]

arr[0] = arr.0
通过call形式转换成arr.0.call(arr)
所以this指向当前数组

原型链中的this

一个方法存在于一个对象的原型链上,那么this指向的是调用这个方法的对象,就像该方法在对象上一样

var o = {
  a: 2,
  b: 7,
  f: function () { 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

getter 与 setter 中的 this

用作 getter 或 setter 的函数都会把 this 绑定到设置或获取属性的对象。

function sum() {
  return this.a + this.b + this.c;
}

var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};

Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});

console.log(o.average, o.sum); // logs 2, 6

4.构造器调用模式

一个函数如果创建的目的就是希望结合new操作符来调用,那它就是构造器函数。new操作符会创建一个连接到该函数的prototype成员的新对象,同时this会被绑定到这个新对象上。

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

function Person (name) {
  this.name = name;
}
Person.prototype.getName = function () {
  return this.name;
};

const person = new Person('alfred');
console.log('the person is called ' + person.getName()); // the person is called alfred

5.call、apply调用模式

call、apply、bind方法可以重新指定this指向

var person = {
  name: 'unknown',
  doSomething: function () {
    console.log(this.name); 
  },
  alfred: {
    name: 'alfred',
    doSomething: function () {
      console.log(this.name); 
    }
  }
};
person.doSomething(); // unknown
person.alfred.doSomething(); // alfred
// 通过call方法重新指定this,指向到person对象,apply、bind和call同理
person.alfred.doSomething.call(person); // unknown
person.alfred.doSomething.apply(person); // unknown
person.alfred.doSomething.bind(person)(); // unknown
// 需要注意!!bind只生效一次!
person.alfred.doSomething.bind(person).bind(person.alfred)(); // unknown

6.箭头函数调用模式

箭头函数没有自己的this,arguments,super或 new.target。

在箭头函数中,this与封闭词法环境的this保持一致,this 被设置为他被创建时的环境。这同样适用于在其他函数内创建的箭头函数:这些箭头函数的this被设置为封闭的词法环境的。

在全局代码中,它将被设置为全局对象:

var func = ()=>{console.log(this)};
func(); // window

var person = {
  name: 'unknown',
  foo: function () {
    var x = () => { console.log(this); };
    return x;
  },
  alfred: {
    name: 'alfred',
    doSomething: () => {
      console.log(this); 
    }
  },
  jyh: {
    name: 'jyh',
    doSomething: function () {
      console.log(this); 
    }
  }
};
// alfred.doSomething没有被包含在某个函数内,未形成封闭词法环境,所有this指向window对象
person.alfred.doSomething();  // window
person.jyh.doSomething(); // jyh

// foo返回的函数this在foo被调用时绑定
var func1 = person.foo(); 
func1(); // person
var func2 = person.foo;
// func2()调用时foo的this指向window
func2()(); // window

通过 call 或 apply 调用
由于 箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this),他们的第一个参数会被忽略。

var func = () => {
  console.log(this);
};

func(); // window
var person = {
  name: 'unknown'
};
// call、bind同样无法改变箭头函数this指向
func.apply(person);  // window

7.DOM事件处理函数调用

当函数被用作事件处理函数时,它的this指向触发事件的元素(一些浏览器在使用非addEventListener的函数动态添加监听函数时不遵守这个约定)

// 被调用时,将关联的元素变成蓝色
function bluify(e){
  console.log(this === e.currentTarget); // 总是 true

  // 当 currentTarget 和 target 是同一个对象时为 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}

// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');

// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

e.currentTarget为绑定事件的元素,e.target为触发事件的元素,所以this一般就指向e.target

8.内联事件处理函数调用

内联事件处理函数被调用时,会指向监听器所在的DOM元素, 但只有外层代码中的this是这样设置的

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

alert 'button'

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

没有设置内部函数的this, 所以alert 'window'

参考资料: