| JavaScript对话面试官系列 | - this

100 阅读6分钟

起因

当我输入了大量来自优质个人博客文章,经典书籍,名牌讲师的 JavaScript 系统知识之后,我反而变得有些困惑。我明明可以流利的书写八大继承,流利的书写 Underscore 库中的防抖节流,深拷贝……,我也知晓什么是闭包,什么是迭代器,生成器……。但是问题来了,当我试图将一个 JavaScript 核心概念,比如闭包……,介绍给同学,讲解给自己,对话面试官的时候。原本自认为可以脱口而出的语言,到嘴边却显得吞吞吐吐。只能用只言片语,或者东拼西凑的知识点来表达给对方。这无疑让我有了一种,虽然花了大量时间但是却从未拥有过 JavaScript 的感觉。

目的

所以这个系列要尝试解决的问题就是当别人询问或者考察我JavaScript 核心概念的时候,我可以尽可能流畅的,清晰的表达给他人。

期望

希望掘金优秀的前端技术人员和前辈们可以在百忙之中多多补充这篇文章,多多审查这篇文章,多多提问我。我期望可以通过这个系列来解决我当前的问题。

this

  • 《你不知道的 JavaScript 》这本书当中关于 this 内容我觉得很好的阐述了 this 的绑定规则。这篇文章 可以说是 做笔记了。

    • 默认绑定: 独立函数调用, 可以将这条规则看作是无法引用与其他规则的默认规则。(全局函数当中的 this 在非严格模式下等于 window, 在严格模式下等于 undefined)。

    • 显示绑定: 通过 applycall 来显示绑定this, 还有一种显示绑定的变种是硬绑定 bind()

    • 隐式绑定:考虑调用位置是否有上下文对象,比如调用对象的方法那么方法当中的 this 会指向该对象。 隐式绑定有 this 丢失问题, 函数别名var bar = obj.foo; bar(); 传入函数参数, 传入语言内置的函数(setTimeout 函数,点击事件)都会导致 this 丢失。

    • new 绑定: 绑定 this到构造函数的实例对象上。

      • new 一个构造函数发生了什么:

        1.创建一个空对象(实例对象)。

        2.将实例对象的__proto__指定到构造函数的prototype上。

        3.绑定构造函数当中的 this 到实例对象, 执行构造函数当中的代码,给实例对象赋值属性和方法。

        4.构造函数当中是否有返回值 ,没有就返回这个实例对象, 有就看返回的是否是对象类型, 是就返回这个对象, 不是就返回实例对象。

this 的四项绑定规则案例

默认绑定

// 默认绑定: 独立函数调用
// 1.案例一:
function foo() {
  console.log(this);
}
foo();
​
// 2.案例二:
function foo1() {
  console.log(this);
}
​
function foo2() {
  console.log(this);
  foo1();
}
​
function foo3() {
  console.log(this);
  foo2();
}
​
foo3();
​
// 3.案例三:
function foo() {
  function bar() {
    console.log(this);
  }
  return bar;
}
​
var fn = foo();
fn(); // window

隐式绑定

// 案例一
var obj = {
  name: 'why',
  foo: function () {
    console.log(this);
  },
};
​
var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例二
function foo() {
  console.log(this);
}
var obj = {
  name: 'why',
  foo: foo,
};
​
var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例三
var a = 'global';
function foo(fn) {
  fn();
}
var obj = {
  a: 'local',
  eating() {
    console.log(this.a, '正在吃饭'); // 函数参数 this丢失
  },
};
foo(obj.eating); // global 正在吃饭
// 注意在node环境下调试的时候,全局的this指向空对象
// 案例四
setTimeout(() => {
  console.log(this); // window 传入系统内置的函数
}, 1000);
// 案例五
div.addEventListener('click', function () {
  console.log(this); // Dom div 传入系统内置的函数
});
// 案例6 map filter
arr.map((item) => {
  console.log(this); //默认是window第二个参数可以指定绑定 this 传入系统内置的函数
}, obj);

显示绑定

function foo() {
  console.log('函数被调用了', this);
}
var obj = {
  name: 'obj',
};
​
// call / apply是可以指定this的绑定对象;
foo.call(obj, 参数);
foo.apply(obj, 参数列表);
foo.apply('aaaa');

new 绑定

function Person(name, age) {
  this.name = name;
  this.age = age;
}
​
var p1 = new Person('why', 18);
console.log(p1.name, p1.age);

this 绑定的优先级

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

显示绑定优先级高于隐式代码

const obj = {
  name: 'ryan',
  getName() {
    return this.name;
  },
};
console.log(obj.getName.call('string')); // this -> string;
//更明显的比较
function foo() {
  console.log(this);
}
​
var obj = {
  foo: foo.bind('aaa'),
};
obj.foo(); // this -> 'aaa';

new 绑定大于隐式绑定代码

var obj = {
  name: 'ryan',
  getName() {
    return this.name;
  },
};
let fn = new obj.getName(); this -> 实例对象fn

new 绑定大于显示绑定代码

// 由于new 和 call以及apply都是调用函数,自然就不可以一起使用。
function foo() {
  console.log(this);
}
const fn = foo.bind('a');
const nFn = new fn();
console.log(nFn); this -> 实例对象nFn

特殊绑定

忽略显示绑定

// apply/call/bind: 当传入 null/undefined 时, 自动将 this 绑定成全局对象
foo.apply(null);
foo.apply(undefined);

规范绑定

var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this);
  },
};
​
var obj2 = {
  name: 'obj2',
};
(obj2.bar = obj1.foo)();

箭头函数当中的 this

  • 箭头函数不绑定 this,没有 arguments,也不可以作为构造函数 使用 new 来调用。
  • 箭头函数当中的 this 指向外层全局或者函数作用域当中的 this。
var obj = {
  data: [],
  getData: function () {
    setTimeout(() => {
      var result = ['abc', 'cba', 'nba'];
      this.data = result;
    }, 2000);
  },
};

this 面试题

面试题误区

var obj = {
  name: 'ryan',
  getName: () => {
    return this.name;
  },
};
​
// ""
  • 如果真的弄懂了以上规则,刷题如切菜般容易。
  • getName 的外层作用域是全局作用域,而不是 obj 对象大括号作用域。因为在 js 当中只有全局作用域和函数作用域。
  • 给箭头函数绑定 this 是徒劳的((() => {}).call('string'))

面试题 1

var name = 'window';
​
var person = {
  name: 'person',
  sayName: function () {
    console.log(this.name);
  },
};
​
function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  `(person.sayName())`; // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)
​
sayName();

面试题 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(); // person1(隐式绑定)
person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)
​
person1.foo2(); // window(不绑定作用域,上层作用域是全局)
person1.foo2.call(person2); // window
​
person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window(独立函数调用)
person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)
​
person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
person1.foo4().call(person2); // person1(上层找到person1)

面试题三

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(); // person1
person1.foo1.call(person2); // person2(显示高于隐式绑定)
​
person1.foo2(); // person1 (上层作用域中的 this 是 person1)
person1.foo2.call(person2); // person1 (上层作用域中的 this 是 person1)
​
person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2
​
person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1var obj = {
  name: 'obj',
  foo: function () {},
};
​

面试题四

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()(); // window 独立函数调用
person1.obj.foo1.call(person2)(); // window
person1.obj.foo1().call(person2); // person2
​
person1.obj.foo2()(); // obj
person1.obj.foo2.call(person2)(); // person2
person1.obj.foo2().call(person2); // obj

code.png

code.png