Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript之this全面解析 #16

Open
heyushuo opened this issue Dec 27, 2019 · 0 comments
Open

JavaScript之this全面解析 #16

heyushuo opened this issue Dec 27, 2019 · 0 comments

Comments

@heyushuo
Copy link
Owner

前言

this 关键字在 Javascript 中非常常见,但是很多开发者很难说清它到底指向什么。大部分人会从字面意思上去理解 this,认为 this 指向函数自身,实际上this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调
用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

总结: 函数被调用时发生 this 绑定,this 指向什么完全取决于函数在哪里被调用

一、this 的绑定规则

this 一共有 4 中绑定规则,接下来一一介绍每种规则的解释和规则直接的优先级

  • 默认绑定(严格/非严格模式)
  • 隐式绑定
  • 显式绑定
  • new 绑定

1.1 默认绑定(严格/非严格模式)

  • 独立函数调用: 独立函数调用时 this 使用默认绑定规则,默认绑定规则下 this 指向 window(全局对象)
  • 严格模式下: this 无法使用默认绑定,this 会绑定到 undefined。

独立函数调用

function foo() {
  console.log(this.a);
}
var a = 2;
foo(); // 2

严格模式下:

function foo() {
  "use strict";
  console.log(this); //undefined
  console.log(this.a); //Uncaught TypeError: Cannot read property 'a' of undefined
}
var a = 2;
foo();

注意下边两种情况

var age = "18";
var obj = {
  name: "heyushuo",
  age: 25,
  fn: function() {
    function sayName() {
      console.log(this); //window
      console.log(this.age); //undefined
    }
    sayName();
  }
};
obj.fn();

函数 sayName 虽然是在 obj.fn 内部定义的,但是它仍然是一个独立函数调用,this 仍然指向 window。

var a = "global";
var obj = {
  a: 2,
  foo: function() {
    console.log(this.a); //global
  }
};
var bar = obj.foo; // 函数别名!
bar();

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是函数本身,因此此时的
bar() 其实是一个不带任何修饰的独立函数调用,因此应用了默认绑定。

1.2 隐式绑定

当函数引用有上下文对象时(例如:obj.foo 这个时候使用 obj 上下文来引用函数 foo),隐式绑定规则会把函数中的 this 绑定到这个上下文对象。

var obj = {
  name: "heyushuo,
  foo: function() {
    console.log(this.name); //heyushuo
  }
};

obj.foo();

对象属性引用链中只有上一层或者说最后一层在调用中起作用。

var obj = {
  name: "heyushuo",
  obj1: {
    name: "kebi",
    foo: function() {
      console.log(this.name); // kebi
    }
  }
};

obj.obj1.foo();

隐式丢失
被隐式绑定的函数会丢失绑定对象,而应用默认绑定,把 this 绑定到全局对象或者 undefined(严格模式) 上。
第一种

var a = "global";
var obj = {
  a: 2,
  foo: function() {
    console.log(this.a); //global
  }
};
var bar = obj.foo; // 函数别名!
bar();

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是函数本身,因此此时的
bar() 其实是一个不带任何修饰的独立函数调用,因此应用了默认绑定。

第二种传入回调函数时:

var a = "global";
var obj = {
  a: 2,
  foo: function() {
    console.log(this.a); //global
  }
};
var bar = obj.foo; // 函数别名!
function doFoo(fn) {
  fn(); // <-- 调用位置!
}
doFoo(bar); //global

//和下边这种一样
setTimeout(obj.foo, 300);

1.3 显示绑定

通过 call() 或者 apply()方法。第一个参数是一个对象,在调用函数时将这个对象绑定到 this 上,称之为显示绑定。

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

显示绑定引申出来一个硬绑定,代码如下

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments); //内部已经强制绑定了传入函数this的指向
  };
}
var obj = {
  a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5

bar 函数无论如何调用,它总会手动在 obj 上调用 fn,强制把 fn 的 this 绑定到了 obj。这样也解决前面提到的丢失绑定问题

由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}
var obj = {
  a: 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

1.4 new 绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  • 创建(或者说构造)一个全新的对象。
  • 这个新对象会被执行 [[ 原型 ]] 连接。
  • 这个新对象会绑定到函数调用的 this。
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

例如:

function foo() {
  this.name = "heyushuo";
  this.age = 25;
}
foo.prototype.sayName = function() {
  console.log(this.name + this.age);
};
var bar = new foo();

console.log(bar); //{name: "heyushuo", age: 25}
//这个新对象会绑定到函数调用的 this。所以此时的this就是bar对象
console.log(bar.age); // 25

如下图是 new foo() 这个对象

二、四种绑定关系的优先级

判断 this,可以按照下面的顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo();
  1. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是
    指定的对象。
var bar = foo.call(obj2);
  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上
    下文对象。
var bar = obj1.foo();
  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到
    全局对象。
var bar = foo();

三、被忽略的 this

把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认规则。(this 指向 window
例如:

function foo() {
  console.log(this.a);
}
var a = 2;
foo.call(null); // 2

下面两种情况下会传入 null
使用 apply(..)来“展开”一个数组,并当作参数传入一个函数
bind(..)可以对参数进行柯里化(预先设置一些参数)

function foo(a, b) {
  console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3

// 使用 bind(..) 进行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

这两种方法都需要传入一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你仍然需要传入一个占位值,这时 null 可能是一个不错的选择(更安全的做法就是传入一个特殊的对象(空对象),把 this 绑定到这个对象不会对你的程序产生任何副作用。JS 中创建一个空对象最简单的方法是**Object.create(null)**,这个和{}很像,但是并不会创建 Object.prototype 这个委托,所以比{}更空。)

四、箭头函数

以上四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数

箭头函数不使用this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

看一下下边这个例子:

function foo() {
  // 返回一个箭头函数
  return a => {
    //this 继承自 foo()
    console.log(this.a);
  };
}
var obj1 = {
  a: 2
};
var obj2 = {
  a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() {
  setTimeout(() => {
    // 这里的 this 在此法上继承自 foo()
    console.log(this.a);
  }, 100);
}
var obj = {
  a: 2
};
foo.call(obj); // 2

在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo() {
  var self = this; // lexical capture of this
  setTimeout(function() {
    console.log(self.a);
  }, 100);
}
var obj = {
  a: 2
};
foo.call(obj); // 2

五、接下里思考下边几道题加深对 this 的认识(答案在最下边)

var name = "heyushuo";
function foo() {
  var name = "kebi";
  this.name = "yaoming";
  console.log(name);
}

foo();
var name = "heyushuo";
function foo() {
  this.name = "kebi";
  console.log(name);
}

foo();
var name = "heyushuo";
function foo() {
  this.name = "kebi";
}
foo.prototype.sayName = function() {
  console.log(name);
};
var bar = new foo();
bar.sayName();
var name = "heyushuo";
setInterval(function() {
  var name = "kebi";
  console.log(this.name);
}, 100);

答案:

  • 1.kebi
  • 2.kebi
  • 3.heyushuo
  • 4.heyushuo
var name = "heyushuo";
var obj = {
  name: "kebi",
  sayName: () => {
    console.log(this.name);
  }
};
obj.sayName();
var name = "heyushuo";
var obj = {
  name: "kebi",
  sayName: function {
    setTimeout(function(){
      console.log(this.name);
    }, 100);
  }
};
obj.sayName();
var name = "heyushuo";
var obj = {
  name: "kebi",
  sayName: function {
    setTimeout(()=>{
      console.log(this.name);
    }, 100);
  }
};
obj.sayName();
var name = "heyushuo";
var obj = {
  name: "kebi",
  sayName: function {
    return function(){
     console.log(this.name);
    }
  }
};
obj.sayName()();
var name = "heyushuo";
var obj = {
  name: "kebi",
  sayName: function {
    return ()=>{
     console.log(this.name);
    }
  }
};
obj.sayName()();

答案:

  • 1.heyushuo
  • 2.heyushuo
  • 3.kebi
  • 4.heyushuo
  • 5.kebi
var name = "kebi";
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function() {
  this.sayAge();
  console.log(this.name);
};

Person.prototype.sayAge = function() {
  console.log(this.age);
};

var person = new Person("heyushuo", 25);
person.sayName();
var name = "kebi";
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function() {
  setTimeout(function() {
    console.log(this.name);
  }, 100);
};

var person = new Person("heyushuo", 25);
person.sayName();
var name = "kebi";
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function() {
  setTimeout(() => {
    console.log(this.name);
  }, 100);
};

var person = new Person("heyushuo", 25);
person.sayName();

答案:

  • 1.25 heyushuo
  • 2.kebi
  • 3.heyushuo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant