原生js中的三座大山(上)-this

1,409 阅读5分钟

前言:

想要学好前端,基本功必须得要扎实,原型和原型链、闭包、this,那这三座大山就一定要跨过去,今天先说this,话不多说,go~

this:

在开发时,我们要搞清楚this的指向,是至关重要的~

为了能够一眼看出this指向的是什么,我们需要确定它的绑定规则是哪个?this有四种绑定规则:

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定
默认绑定

无法应用其他规则时就应用默认绑定,经常是独立函数调用

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

上面函数就应用了this的默认绑定,foo()前面没有调用它的对象,其实也可以这么想,window.foo(),window可以省略,所以foo()里面的this指向全局对象window(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。

隐式绑定

考虑调用位置是否有上下文对象

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

因为调用foo的时候前面加上了obj,所以隐式绑定会把函数调用中的this绑定到这个上下文对象,也就是obj,所以this.a 和obj.a是一样的 但是,你需要记住一句话:this永远指向最后调用它的那个对象

function foo(){
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    obj1:obj1
}
obj2.obj1.foo();//2

可见,结果是2,因为this指向最后调用它的那个对象,即obj1

在使用隐式绑定的时候,有一个常见的问题,就是会出现隐式丢失

隐式丢失

也就是说出现了隐式丢失,会应用默认绑定的规则

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
var baz = obj.foo;
var a = '我是全局对象的a';
baz();//我是全局对象的a

因为baz()其实是一个独立函数调用,所以应用了默认绑定

还有一种更常见的情况会发生隐式丢失,那就是在传入回调函数时:

function foo(){
    console.log(this.a);
}
function baz(fn){
    fn()
}
var obj = {
    a:2,
    foo:foo
}
var a = '我是全局对象的a';
baz(obj.foo);//我是全局对象的a

参数传递其实就是一种隐式赋值

显示绑定

所谓显示,是因为你可以直接指定this的绑定对象,我们可以借助apply,call,bind等方法

apply和call作用一样,只是传参的方式不同,都会执行对应的函数,但是bind不一样,它不会执行,需要手动去调用

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

foo.call(obj);//2
foo.apply(obj);//2

但是,依然无法解决丢失绑定的问题,如下:

function sayHi(){
     console.log('Hello,', this.name);
 }
 var per = {
     name: 'mengyun',
     sayHi: sayHi
 }
 var name = 'anna';
 var Hi = function(fn) {
     fn();
 }
 Hi.call(per, per.sayHi); //Hello, anna

但是我们可以给fn也硬绑定this,就可以解决这个问题

function sayHi(){
    console.log('Hello,', this.name);
}
var per = {
    name: 'mengyun',
    sayHi: sayHi
}
var name = 'anna';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(per, per.sayHi); //Hello, mengyun

原因:因为per被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是per对象。

上面代码用bind可以改写成:(bind在你不知道的js(上)中就是被归为硬绑定)

function sayHi(){
    console.log('Hello,', this.name);
}
var per = {
    name: 'mengyun',
    sayHi: sayHi
}
var name = 'anna';
var Hi = sayHi.bind(per);
Hi.call(per, per.sayHi); //Hello, mengyun
new绑定

js跟其他语言不一样,没有类,所以我们可以用构造函数来模拟类

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 这个对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么返回这个新对象,否则返回构造函数返回的对象
手写一个new
function _new(fn,...args){
  //1、创建一个空对象
  //2、这个对象的 __proto__ 指向 fn 这个构造函数的原型对象
  var obj = Object.create(fn.prototype);
  //3、改变this指向
  var res = fn.apply(obj,args);
  // 4. 如果构造函数返回的结果是引用数据类型,则返回运行后的结果,否则返回新创建的 obj
  if ((res!==null && typeof res == "object") || typeof res == "function") {
      return res;
  }
  return obj;
}

下面我们用new来绑定一下this,

function Person(name){
	 this.name = name;
}
var per = new Person('mengyun');
console.log(per.name);//mengyun

此时per已经被绑定到Person调用中的this上

优先级

在知道了四种绑定规则之后,我们需要的就是准确判断是哪种绑定规则,但是如果某个调用位置可以应用多条规则,那么就需要有个优先级了。 毫无疑问,默认绑定是最低的,可以先不考虑他,下面来看个例子:

function f1(){
   console.log(this.a);
}
var obj1 = {
   a:2,
   f1:f1
};
var obj2 = {
   a:3,
   f1:f1
};

obj1.f1();//2
obj2.f1();//3

obj1.f1.apply(obj2);//3

可以看出来,显示绑定优先级大于隐式绑定 其他的demo大家可以自行去写,最后的结论就是 new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

绑定例外

如果把null或者undefined作为绑定对象传入call,apply,bind,这些值往往会被忽略,实际应用的是默认绑定规则:

var obj= {
    name: 'mengyun'
}
var name = 'Anna';
function bar() {
    console.log(this.name);
}
bar.call(null); //Anna

注意: 但是以上规则只能应用在普通函数上,箭头函数例外,因为它没有自己的this,而是根据外层作用域来决定的,而且箭头函数的绑定无法被修改

function foo(){
    return (a)=>{
        console.log(this.a);
    }
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var baz = foo.call(obj1);
baz.call(obj2);//2

由于foo()的this被绑定到obj1,所以baz()也被绑定到obj1,并且无法被修改