Javascirpt学习笔记-This

Javascirpt学习笔记-This_第1张图片
Javascript中this.png

1. 关于This

相比在Java中this只指代当前类的用法,Javascript中this的并不是一个固定的指代,其指代存在绑定规则

1.1 使用原因

其实大部分情况下,使用词法作用域已经可以解决所有需要使用this的场景,代码中完全可以不使用this。但是使用this可以带来函数调用过程中对于函数参数传递的优化。举个例子:

function foo(a, b){
  console.log(a + b);
}
f(1, 2); // 3

在函数调用过程中需要传递两个参数进行调用,但是如果参数是某个对象中的某些属性,那么使用这种调用方式就变成如下情况:

 var obj = {a: 1 ,b: 2};
 foo(obj.a, obj.b); // 3

在传递参数的时候明显比较麻烦,尤其在于如果要利用对象中的复数属性的时候,参数传递就变得特别多,这个时候使用this就可以进行简化:

function foo() {
  console.log(this.a + this.b);
}
var obj = {a: 1, b: 2};
foo.apply(obj); // 3

当然我们完全可以将obj作为参数进行传递,所以说使用this并非开发中必须。

1.2 绑定要点

this到底代表什么关键在于this的绑定规则,但无论什么规则,首先要明确两点:第一,this不代表自身,第二,this和词法作用域没有必然联系例:

function foo() {
  console.log(this.a); // 这里的this并不指代foo函数本身,也和词法作用域无关
}

以上两点从根本上来说指在函数进行声明的时候,并不能确定this代表什么,只有在函数调用的时候,this的绑定关系才可以确定(就像动态作用域一样)
this只有在函数调用的时候确定绑定关系
this只有在函数调用的时候确定绑定关系
this只有在函数调用的时候确定绑定关系

重要的事情说三遍,不过再补充一下:

箭头函数是特别的
箭头函数是特别的
箭头函数是特别的

2. 绑定规则

根据Javascript中的用法,this有以下的几种绑定规则:

2.1 全局绑定

直接在Javascript全局中使用this,这个时候this指向一个全局对象,在浏览器中代表window对象,node中代表global对象:

 console.log(this);

2.2 Function Invoke(默认绑定)

直接调用方法的时候,如果在严格模式下,this的绑定为undefined(之所以返回undefined,是因为函数调用的时候执行的上下文并不确定,严格模式于是将执行中的this绑定为undefined),非严格模式下,this绑定为全局对象,同 2.1 全局绑定

// 1. 非严格模式下
function foo(){
  console.log(this);
}
foo(); // window
// 2. 严格模式下
(function(){
  'use strict'
  function foo() {
    console.log(this);
  }
  foo(); // undefined
})()
// 3. 严格模式下调用
(function(){
  'use strict'
  foo(); // window
})()

请注意例子中的第三个调用,在调用的时候使用了严格模式,但是打印结果并不是undefined,因此说明严格模式是在函数声明的时候对this的绑定产生影响,在调用的时候对此并没有影响。
如果之后所有的规则都不符合的时候,将使用这一条规则,来进行this的绑定。

2.3 Method Invoke(隐式绑定)

当使用对象调用的时候,this将绑定为该对象:

var obj = {
  a: 1,
  foo: function() {
    console.log(this.a);
  }
}
obj.foo(); // 1

但是隐式绑定过程存在两种隐式丢失的情况(可以使用箭头函数解决,详见 2.6 箭头函数),使用函数别名和回调函数:

var obj = {
  a: 1,
  foo: function() {
    console.log(this.a);
  }
}
// 1. 使用函数别名
var baz = obj.foo;
baz(); // undefined
// 2. 使用回调函数
setTimeout(obj.foo , 0); // undefined

原因在于在使用函数别名和回调函数的时候,可以理解为进行了以下的操作

var baz = obj.foo = function() {
  console.log(this.a);
}      

于是,在调用baz的时候,将根据 2.2 默认绑定this进行处理

2.4 apply, call, bind (显示绑定)

使用显示绑定的时候,this绑定为传入的对象:

function foo(a, b, c){
  console.log(this.a + a + b + c);
}
foo.apply({a: 1}, [1, 2, 3]) // 1 + 1 + 2 + 3 = 7
foo.call({a: 1}, 1, 2, 3) // 7
foo.bind({a: 1})(1, 2, 3) // 7

applycall的使用本质上没有区别,只是传递参数不同
bind是ES5之后Function原型链上增加的方法,先将this绑定到传递的对象,但是使用bind进行绑定的时候是硬绑定,硬绑定是指这个绑定只能绑定一次,只有第一次绑定可以生效:

function foo() {
  console.log(this.a);
}
foo.bind({a: 1}).bind({a: 2})(); // 1

因为现在很多内置对象提供了原型链上的方法,于是通过这种原型链上API的调用是显示调用的过程

function foo(i) {
  console.log(this.a + i);
}
[1,2,3].forEach(foo, {a: 1}); // 2 3 4 

2.5 new(构造调用)

使用new进行绑定的时候,this绑定为构建函数构造的新对象,例:

function F() {
  this.a = 1;
}
var f = new F(); // 等同于 var f = new F;
console.log(f.a); // 1

这里使用new进行构造的时候,Javascript会按照以下的过程执行
创建一个新对象 --> 将对象和构造方法的prototype做关联 --> 将this绑定到该对象 --> 如果构造函数无返回值则返回该对象,否则返回新对象
所以,转换为代码理解也就是:

// 1. 无返回的构造函数
function F() {
  this.a = 1;
}
/* 使用new以后,函数内部变化为
 function F() {
   var _o_ = {} ; // 创建一个新对象
   _o_.prototpye = F.prototype; // 原型链关联
   _o_.a = 1; // this绑定到对象
   return _o_; // 返回该对象 
 }
*/
var f = new F();
console.log(f.a); // 1
// 2. 有返回的构造函数
function F() {
  this.a = 1;
  return {b: 2};
}
/* 使用new以后,函数内部变化为
 function F() {
   var _o_ = {} ; // 创建一个新对象
   _o_.prototpye = F.prototype; // 原型链关联
   _o_.a = 1; // this绑定到对象
   return {b: 2}; // 返回原返回值 
 }
*/
var f = new F();
console.log(f.a) // undefined
console.log(f.b) // 2

于是,如果原构造函数存在返回值的情况下,this绑定为了函数内部创建的新对象,但是这个新创建的对象并没有返回,所以没办法再对这个绑定的内容进行引用了。

2.6 箭头函数

使用箭头函数进行绑定的时候,this绑定外层(函数或者全局)作用域环境:

var obj = {
  a: 1,
  foo: () => {
    console.log(this);
  }
}
obj.foo(); // window

这个例子看上去和 2.3 隐式绑定 是差不多的,但是区别这里的this指向发生变化,this不再指向调用对象本身,而是直接使用外层函数全局对象。
但是下面的例子是需要注意的:

function f(){
    return function() {
        console.log(this.a);
    }
}
f.apply({a:1}).apply({a:2}) // 2;
// 箭头函数
function f(){
    return() => {
        console.log(this.a);
    }
}
f.apply({a:1}).apply({a:2}) // 1 

在第一次调用 apply的时候,生成了一个外层的作用域,此时this指向了该外层全局对象的{a: 1},之后不会再发生变化

2.7 事件绑定

使用事件函数进行绑定的时候,根据使用情况不同,this通常指向当前事件节点,例:

// 1. 使用addEventListener等事件绑定,this指向绑定的节点

  
    
    
  

// 2. 使用html事件绑定,this指向当前dom节点

  
    
  

// 3. 使用html事件绑定,增加IIFE,this指向window

  
    
  

3. 优先级

当我们如果使用的情况比较复杂,同时存在以上几种绑定关系的时候,我们该如何处理?实际上绑定的关系存在一个优先级,按照优先级来进行处理(这里除开 2.7 事件绑定,因为通常事件绑定下不会存在那么复杂的关系)
2.5 构造绑定 > 2.4 显示绑定 > 2.3 隐式绑定 > 2.2 默认绑定
2.6 箭头函数 有特殊表现
例:

// 1. 隐式绑定 > 默认绑定
function foo() {
  console.log(this.a);
}
var obj = {a: 1, foo: foo};
obj.foo(); // 1
// 2. 显示绑定 > 隐式绑定
obj.foo.apply({a: 2}) // 2
// 3. 构造绑定 > 显示绑定
function F(a) {
  this.a = a;
}
var obj = {};
var bar = F.bind(obj);
bar(1);
console.log(obj.a); // 1
var obj2 = new bar(2);// 使用new修改了this的绑定
console.log(obj.a); // 1 原来的值没有发生变化
console.log(obj2.a); // 2 新的值发生了变化
// 4. 箭头函数的特殊表现
var a = 2;
var f = () => {
  console.log(this.a);
}
f.apply({a: 3}); // 2

4. 其他

4.1 显示绑定中null的使用

在进行显示绑定的时候,如果传递绑定对象为null时,因为不符合我们说的其他规则,所以将使用默认规则:

function foo() {
  console.log(this);
}
foo.apply(null) // window

但是如果直接传递null进行方法调用存在变量泄漏的风险:

function foo() {
  this.a = 1;
}
foo.apply(null);
console.log(a); // 1

4.2 Object.create(null)

为了解决 4.1 显示绑定中null的使用 存在的变量泄漏风险,于是可以使用Object.create(null)创建一个空对象,使用这种方式创建的空对象,比{}更纯粹,因为他不继承Object.prototype

function foo() {
  this.a = 1;
}
foo.apply(Object.create(null));
console.log(a); // ReferenceError

5. 参考

《你不知道的Javascript(上卷)》
MDN this
Gentle explanation of 'this' keyword in JavaScript
Function.prototype.call
Function.prototype.apply
Function.prototype.bind

6. 练习

惯例最后来个练习吧

function foo() {
  console.log(this.a);
}
var obj = {
  a: 1,
  foo: foo
}
var obj2 = {
  a: 1,
  foo: () => {
    console.log(this.a);
  }
}
function F() {
  this.a = 2;
}
var f = new F();
foo(); // undefined
obj.foo(); // 1
obj.foo.bind({a: 2})(); // 2
obj2.foo.bind({a: 2})(); // undefined
console.log(f.a); // 2

你可能感兴趣的:(Javascirpt学习笔记-This)