通过面试题深入理解javaScript中的this

158 阅读10分钟

了解this

1. this 是什么

thisJavaScript中一个很特别的关键字,被自动定义在所有函数的作用域中。

this会在执行上下文中绑定一个对象,但是是根据什么条件绑定的?在不同的执行条件下会绑定不同的对象,这也是让人捉摸不定的地方。

下来我们一起来彻底搞定this

2. 为什么要使用this

this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。

在没有this的情况下,我们定义一个对象,我们要拿到obj中的属性,就必须通过obj去获取,如果我们将obj改成obj1,那么我们就需要将所有的obj改成obj1,

var obj = {
  name: "monkey",
  job: "web开发",
  foo: function () {
    console.log(`${obj.name}的职业是${obj.job}`); //monkey的职业是web开发
  },
};
obj.foo();

我们使用在this调用的时候,就会方便很多,此时我们在修改obj为obj1的时候,我们就不需要去修改foo方法中的的引用,这里只有两个属性,如果属性再更多,使用this是不是更方便。

var obj = {
  name: "monkey",
  job: "web开发",
  foo: function () {
    console.log(`${this.name}的职业是${this.job}`); //monkey的职业是web开发
  },
};
obj.foo();

通过上面这个场景,我们了解到使用this的方便性。下面我们继续了解this

3. this的指向

this的指向和函数在哪里定义无关,和调用方式有关。

在大多数情况下,this都是出现在函数中,很少在全局中去使用。

  • 在浏览器中,this 指向的是window
  • 在node中,this指向的是一个空对象{}

下面我们看下在浏览器中使用this

function foo() {
  console.log(this);
}

// 1. 直接调用这个函数
foo(); // Window

// 2. 创建一个对象,对象中的函数指向foo
var obj = {
  name: "monkey",
  foo: foo,
};
obj.foo(); //{name: 'monkey', foo: ƒ}

// 3. apply调用
foo.apply("abc"); //String {'abc'}

同一个函数this的不同

通过上面这个示例可以看到,同一个函数,this的结果完全不同。下面我们就了解下,this的绑定规则是什么。

this的绑定规则

我们来看看在函数的执行过程中调用位置如何决定this的绑定对象。

1. 默认绑定

当独立函数调用,也就是说,我们直接调用函数,而这个函数没有绑定到任何对象上面。

/* 案例一  直接调用*/
function foo() {
  console.log("foo", this);
}
foo(); // Window

/* 案例二  函数嵌套调用*/
function foo1() {
  console.log("foo1", this); // Window
}
function foo2() {
  console.log("foo2", this); // Window
  foo1();
}
function foo3() {
  console.log("foo3", this); // Window
  foo2();
}
foo3();

/* 案例三 对象中得方法*/
var obj = {
  name: "monkey",
  foo: function () {
    console.log("obj", this); // Window
  },
};
var bar = obj.foo;
bar();

上面这三种都是通过独立函数调用的,所有的this都指向的是window。执行结果如下:

默认绑定

2. 隐式绑定

调用位置是否有上下文对象,也就是说通过某个对象发起的函数调用,该对象中必须有对函数的引用。

function foo() {
  console.log(this);
}

/*案例一 */
var obj1 = {
  name: "obj1",
  foo: foo,
};
obj1.foo();

/* 案例二 */
var obj2 = {
  name: "obj2",
  bar: obj1.foo,
};
obj2.bar();

以上都属于隐式绑定,他们都是通过对象调用,this就指向了该对象。执行结果如下

隐式绑定

注意:特殊情况 隐式丢失

function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo,
};

// 将obj1的foo赋值给bar
var bar = obj1.foo;
bar();

隐式丢失

这种情况下,因为foo最终被调用的位置是bar,而bar在进行调用时没有绑定任何的对象,也就没有形成隐式绑定。

这种情况下,相当于一种默认绑定。

3. 显示绑定

在上面的隐式绑定中,我们必须在一个对象内部中引用函数,从而把this隐式绑定到这个对象上。如果我们不想通过对象内部属性对函数引用,那么我们就需要使用call/apply/bind进行显式绑定

callapply 绑定

function foo() {
  console.log(this);
}
var obj = {
  name: "monkey",
};
// call 和 apply 是可以指定 this 的绑定对象
foo.call(obj);
foo.apply(obj);

console.log("--------特殊情况-----------")
foo.call(null);
foo.apply(undefined);

执行结果如下: call和apply的绑定 使用显示绑定时需要注意,如果使用nullundefined,那么这个显示绑定会被忽略,从而使用默认绑定。

bind 绑定

function foo() {
  console.log(this);
}

var obj = {
  name: "monkey",
};

var newFoo = foo.bind(obj);
newFoo();
newFoo();

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。 执行结果如下:

bind绑定

4. new绑定

通过new关键字来创建构造函数的实例,绑定this

使用new关键字来调用函数时,会执行如下的操作:

  • (1)创建一个全新的对象。
  • (2) 这个新对象会被执行Prototype链接。
  • (3)这个新对象会绑定到函数调用的this。
  • (4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function Person(name) {
  console.log(this); //指向的就是Person对象
  this.name = name;
}

var p1 = new Person("moneky");
console.log(p1);

执行结果如下:

new绑定

this的绑定例外

在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

1. 忽略显示绑定

这种情况,我们在上面 “call 和 apply 绑定”中已经介绍了,把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

2. 间接函数引用

var obj1 = {
  name: "obj1",
  foo: function () {
    console.log(this);
  },
};
var obj2 = {
  name: "obj2",
};
obj2.baz = obj1.foo;
obj2.baz();

(obj2.bar = obj1.foo)();

执行结果如下: 间接函数引用

两种方式所绑定的this不同,第二种方式进行了赋值调用,实际上是间接函数引用 赋值表达式(obj2.bar = obj1.foo)的返回值是目标函数的引用,再加上一个小括号,表示立即执行,相当于是直接调用了foo()函数。

3.ES6箭头函数

箭头函数本身不绑定this,this来源于它的上级作用域

var obj = {
  name: "monkey",
  foo: () => {
    console.log(this);
  },
};
obj.foo();

执行结果如下: ES6箭头函数中的this

规则优先级

了解了规则,如果同时设置了两种规则,那么谁的优先级高呢。

1.默认规则的优先级最低

2.显示绑定优先级高于隐式绑定

function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo,
};

var obj2 = {
  name: "obj2",
  foo: foo,
};

obj1.foo(); //{name: 'obj1', foo: ƒ}
// 隐式绑定和显示绑定同时存在
obj1.foo.call(obj2); //{name: 'obj2', foo: ƒ}

其中obj1.foo为隐式绑定call(obj2)为显示绑定 执行结果如下: 显示绑定优先级高于隐式绑定

3.new绑定优先级高于隐式绑定

function foo() {
  console.log(this);
}

var obj = {
  name: "obj",
  foo: foo,
};

new obj.foo(); //执行结果: foo {}

4.new绑定高于显示绑定(bind)

new绑定和callapply是不允许同时使用的。

function foo() {
  console.log(this);
}

var obj = {
  name: "obj",
};

var bar = foo.bind(obj);
new bar(); //执行结果:foo {}

如此可以得出以下结论:

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

this面试题

下面我们就通过面试题,再来理解this

1.面试题一-间接函数引用

var name = "window"
var person = {
    name: "person",
    sayName: function () {
        console.log(this.name)
    },
}
function foo() {
    var sss = person.sayName;
    sss();  // 1
    person.sayName(); // 2
    (person.sayName)();  //3
    (b = person.sayName)(); //4
}
foo()

分析:

  1. person.sayName 直接赋值给了sss,直接调用sss函数,即为独立函数调用,默认绑定,所以应该是window
  2. 通过person对象调用,隐式绑定,所以应该是person
  3. 和2其实是一样的,都是对象调用,隐式绑定,所以应该是person。。
  4. 赋值表达式调用,上面介绍this的绑定例外中的间接函数引用,默认绑定,所以应该是window

执行结果

面试题一结果

2.面试题二-对象中定义函数

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person2 = { name: 'person2' }
person1.foo1(); //1
person1.foo1.call(person2); //2

person1.foo2(); //3
person1.foo2.call(person2); //4

person1.foo3()(); //5
person1.foo3.call(person2)(); //6
person1.foo3().call(person2); //7

person1.foo4()(); //8
person1.foo4.call(person2)(); //9
person1.foo4().call(person2); //10

分析:

  1. 通过persion1对象直接调用foo1(),隐式绑定,所以结果是person1
  2. 使用了隐式绑定和call显示绑定,显示绑定优先级高于隐式绑定,所以结果是person2
  3. 由于foo2是箭头函数,其本身是不绑定this的;向上级作用域查找,由于对象不存在作用域,所以它的上级就是全局作用域,所以结果是window
  4. 由于foo2是箭头函数,不适用于绑定规则,同3,所以结果是window
  5. 由于foo3函数返回值是一个函数,因此“person1.foo3()()”相当于是立即执行了返回函数,即独立函数调用,默认规则,所以结果是window
  6. 先是通过call显示绑定到了person2上,由于foo3函数返回值是一个函数,再加上最有一个小括号,相当于是立即执行了返回函数。即独立函数调用,所以结果是window
  7. 拿到foo3的返回函数,通过call显示绑定到了person2中,所以结果是person2
  8. foo4()的函数返回的是一个箭头函数,箭头函数只看上层作用域,其上级作用域是foo4函数,而foo4中的this指向的是person1,所以结果是person1
  9. foo4()显示绑定到person2中,并且返回一个箭头函数,所以结果是person2
  10. foo4返回的是箭头函数,箭头函数只看上层作用域,其上级作用域是foo4函数,而foo4中的this指向的是person1,所以结果是person1

注意:

  • person1.foo4 :表示的是foo4这个函数。
  • person1.foo4() :表示的是foo4的返回值,也就是箭头函数。

执行结果:

面试题二结果

3.面试题三-构造函数中定义函数

var name = "window";
function Person(name) {
  this.name = name;
  this.foo1 = function () {
    console.log(this.name);
  };
  this.foo2 = () => console.log(this.name);
  this.foo3 = function () {
    return function () {
      console.log(this.name);
    };
  };
  this.foo4 = function () {
    return () => {
      console.log(this.name);
    };
  };
}
var person1 = new Person("person1");
var person2 = new Person("person2");

person1.foo1(); //1
person1.foo1.call(person2); //2

person1.foo2(); //3
person1.foo2.call(person2); //4

person1.foo3()(); //5
person1.foo3.call(person2)(); //6
person1.foo3().call(person2); //7

person1.foo4()(); //8
person1.foo4.call(person2)(); //9
person1.foo4().call(person2); //10

分析:

  1. 通过persion1对象直接调用foo1(),隐式绑定,所以结果是person1
  2. 隐式绑定+显示绑定,显示绑定优先级高,所以结果是person2
  3. foo2是箭头函数,其本身是不绑定this的,向上级查找,上级作用域是Person构造函数,所以结果是person1
  4. foo2是箭头函数,不适用于绑定规则,显示绑定无效,同3,所以结果是person1
  5. 由于foo3()返回的是一个函数,再加上小括号,相当于是全局调用,独立函数调用,所以结果是window
  6. 显示绑定后,又在全局调用,还是独立函数调用,所以结果是window
  7. foo3()先是隐式绑定,返回函数后,再显示绑定到person2中,所以结果是person2
  8. foo4()隐式绑定后,返回一个箭头函数,箭头函数本身是不绑定this的,向上级查找,上级作用域是Person构造函数,所以结果是person1
  9. foo4显示绑定到person2中,返回一个箭头函数,箭头函数本身是不绑定this的,向上级查找,上级作用域是Person构造函数,已经显示绑定到了person2中, 所以结果是person2
  10. 先是隐式绑定,返回箭头函数,其本身是不绑定this的,向上级查找,上级作用域是Person构造函数,所以结果是person1

执行结果:

面试题三结果

4.面试题四-构造函数中定义对象再嵌套函数

var name = "window";
function Person(name) {
  this.name = name;
  this.obj = {
    name: "obj",
    foo1: function () {
      return function () {
        console.log(this.name);
      };
    },
    foo2: function () {
      return () => {
        console.log(this.name);
      };
    },
  };
}
var person1 = new Person("person1");
var person2 = new Person("person2");

person1.obj.foo1()(); //1
person1.obj.foo1.call(person2)(); //2
person1.obj.foo1().call(person2); //3

person1.obj.foo2()(); //4
person1.obj.foo2.call(person2)(); //5
person1.obj.foo2().call(person2); //6

分析:

  1. foo1()隐式绑定后,返回一个函数,后面再加一个小括号,相当于再全局调用,独立函数调用,所以结果是window
  2. person1.obj.foo1只是一个赋值表达式,foo1.call()显示绑定到person2中,返回一个函数,后面再加一个小括号,相当于再全局调用,独立函数调用,所以结果是window
  3. foo1()隐式绑定后,返回一个函数,又显示绑定到person2中,所以结果是person2
  4. foo2()隐式绑定到obj中,返回一个箭头函数,然后向上级查找,上级是foo2所在的函数,foo2this指向的是obj, 所以结果是obj
  5. foo2显示绑定到person2中,返回一个箭头函数,没有作用域,向上级foo2中查找,而foo2已经被显示绑定到了person2中,所以结果是person2
  6. foo2()隐式绑定到obj中,返回的是箭头函数,箭头函数没有作用,向上级查找,所以结果是obj

执行结果:

面试题四结果