面试官:灵魂30+问,你能答对几道?

300 阅读12分钟

前言

你有没有这样的感受,虽然刷了很多题,但是到了真正面试的过程中,面对面试官的步步紧逼的提问,仍然感到力不从心。这是因为,很多人刚开始刷面试题,可能会去收集一堆面试题,然后就开始按照顺序一道一道这么刷过去,这样刷题的问题在于,虽然刷了很多题,但是每道题都是零散的点,对于这道题的相关知识点没有系统的整理和认识。所以,在真正的面试中,面对面试官对某一个点步步紧逼的提问,内心逐渐崩溃 😭

所以,本系列文章,将从 JS 相关知识点开始,展开面试

正文

1.面试官请说一说 javascript 创建对象的几种方式?

(心想,简单啊):第一,我们可以直接通过字面量的方式创建对象。第二,我们可以通过 new 一个构造函数的方式来创建对象。第三,我们可以通过 Object.create 方式创建对象

2.面试官嗯,不错。那你可以说一说 new 的过程都发生了一些什么吗?

(这,也难不到我啊):第一,构造函数会创建一个对象。第二,构造函数中的 this 会指向这个新创建的对象。第三,这个对象的原型会指向这个构造函数的原型对象。第四,最后返回这个新对象

3.面试官嗯嗯,那你能解释下什么是对象的原型,什么又是原型对象吗?

:首先,每个对象都有一个proto属性,通过这个访问这个属性,就可以获取到该对象的原型。其次,每个构造函数都有一个原型对象,通过 prototype 属性就可以访问到它的原型对象。然后,对于通过 new 创建的对象,通过proto属性就可以访问到他的构造函数的原型对象。

4.面试官:那如何打印出属于对象自身的所有属性?

:有三种方式。一是,for...in + hasOwnProperty,不过这种方法只能获取到对象的所有可枚举属性。二是,通过Object.keys方法,同样的,它也只能获取到对象的所有可枚举属性。三是通过Object.getOwnPropertyNames方法,这个方法可以获取到对象自身上的所有可枚举和不可枚举属性。

5.面试官:嗯,那所有对象都有自己的原型对象吗?

:也不是,通过 Object.create(null)这种方式创建的对象没有自己的原型对象。

6.面试官:那你能实现一下 Object.create 方法吗?

:行

Object.myCreate = function (proto, props) {
  function F() {}

  F.prototype = proto;

  if (props && typeof props == 'object') {
    Object.defineProperties(obj, props);
  }

  return new F();
};

7.面试官:我看你上面用了继承的写法,你能说一下为什么会有继承吗?

:我的理解,继承主要是为了方便的实现代码逻辑的复用

8.面试官(满意的点了点头): 现在有一个构造函数 Animal 和构造函数 Cat,Animal 构造函数的原型对象上有一个 say 方法,如下所示:

function Animal() {
  this.voice = 'aaa';
}

Animal.prototype.say = function () {
  return this.voice;
};

function Cat() {}

请你实现 Cat 继承 Animal,使得

const cat = new Cat();

cat.say(); // 'aaa'

我(信心满满,三下五除二就写好了代码):好的

function Animal() {
  this.voice = 'aaa';
}

Animal.prototype.say = function () {
  return this.voice;
};

function Cat() {}

Cat.prototype = new Animal();

// 测试一下
const cat = new Cat();
cat.say(); // 'aaa'

9.面试官:现在我们希望 cat 可以调用 say 方法时,可以发出自己的叫声。你觉得应该怎么做呢?

const cat = new Cat();
cat.say(); // 'miao miao'

我(这还不简单):我们可以通过直接在 Cat 方法类里添加一句this.vioce = miao miao

function Cat() {
  this.vioce = 'miao miao';
}

10.面试官:那如果现在又有一个类 Dog,它和 Cat 一样需要继承 Animal,不过它需要发出'wang wang'的叫声,有没有更简单的方法,让我们可以不用在每个类里都重复添加this.voice=xxx呢?

我(思考了片刻):额...我们可以这样改

function Animal(voice) {
  this.voice = voice;
}

Animal.prototype.say = function () {
  return this.voice;
};

function Cat() {
  Animal.call(this, 'miao miao');
}

Cat.prototype = new Animal();

const cat = new Cat();

cat.say(); // miao miao

11.面试官:你觉得这段代码还有什么地方可以优化一下吗?

我(渐入佳境):这里 Animal 类调用了两次,改写 Cat.prototype 原型对象这里可以使用 Object.create 稍微修改一下

Cat.prototype = Object.create(Animal.prototype);

12.面试官:除了这些,你觉得还有什么地方可以优化一下?

我:额...不知道

13.面试官:嗯,没关系。你上面的写法改写了 Cat 的类的原型对象,这样使得 Cat 的原型对象的本身的 constructor 属性变成了 Animal,所以,我们可以像下面这样修改一下代码

Cat.prototype.constructor = Cat;

14.面试官:那你能画一下他们 cat 对象的原型链关系吗?

我: 好的

cat -> Cat.prototype -> Animal.prototype

15.面试官:嗯?你上面说过每个构造函数都有一个原型对象,可以通过构造函数的 prototype 属性去获取。那么普通函数有原型对象吗,可以通过 prototype 访问吗?

:额...这个,应该有吧

16.面试官:看来你不是很肯定啊,其实普通函数也是有的。那箭头函数有原型对象吗?箭头函数可以通过 new 来调用吗?

(我抹了抹额头的汗,内心逐渐慌乱):额...不知道,应该不行吧

17.面试官:那我来说一下。首先,箭头函数是没有原型对象的。其次,箭头函数也不能通过 new 调用,通过 new 调用会报错。

我:哦哦,原来如此

18.面试官:下面,你来手写实现一下 new 吧?

我(信心满满):好的。

function New(Constructor, ...args) {
  // 创建一个对象实例
  const instance = {};

  // 改写构造函数的this指向
  Constructor.call(instance, ...args);

  // 设置对象实例的原型为构造函数的原型对象
  Object.setPrototypeOf(instance, Constructor.prototype);

  // 返回一个对象
  return instance;
}

19.面试官:写的不错,那我们现在如果在 new 的过程中,构造函数本身 return 了一个值,那是返回该值还是返回你上述创建的这个对象?如下所示:

function Cat(sound) {
  this.sound = sound;
  return 123;
}

const cat = new Cat('miao');
// ❓cat是123还是{ sound: 'miao' }

我(发现事情并不简单):如果我没记错的话,应该是{ sound: 'miao' }

20.面试官:那下面这样呢?

function Cat(sound) {
  this.sound = sound;
  return { color: 'white' };
}

const cat = new Cat('miao');
// ❓cat是{ cat: 'white' }还是{ sound: 'miao' }

我(内心,强装镇定):应该还是返回{ sound: 'miao' }吧...

21.面试官:不对,其实是返回{ color: 'white' }。这里如果 new 的过程中,调用构造函数有返回值,则分两种情况。如果构造函数的返回值是引用值则直接返回该引用值,如果构造函数返回的是原始值,则还是返回我们创建的实例对象。明白了吗?

我(恍然大悟,频频点头):明白明白。

22.面试官:那你继续完善一下刚刚实现的 New 函数

我:好的

function New(Constructor, ...args) {
  const instance = {};

  const value = Constructor.call(instance, ...args);

  // 如果函数有返回值且返回值为引用值,则直接返回该值
  if (value && typeof value == 'object') return value;

  Object.setPrototypeOf(instance, Constructor.prototype);

  return instance;
}

23.面试官(孺子可教也):我看你这里用了 call 函数来改变 this 指向,那你能说一说,call、apply、bind 的作用和区别吗?

我:嗯。call、apply、bind 都是用来改变函数内部 this 指向的。但是 call 和 apply 是调用函数时,改变函数内部 this 指向的。call 和 apply 的区别在于 apply 的第二个参数为数组,而 call 的第一个参数后面的参数,都是是以参数列表的形式传入,参数与参数之间用逗号分隔。而 bind 是通过重新创建一个函数,从而指定这个函数的内部 this 指向,然后再返回这个函数供用户使用。

24.面试官:那请你说说 this 几种不同的使用场景或者说说如何正确的判断 this?

我:第一,通过 new 调用的函数,this 指向它的实例。第二:通过 call,apply,bind 调用的函数,this 指向传入这些函数的对象。第三:作为对象的方法调用的函数,this 指向该对象。第四:箭头函数的 this 指向,继承于它的上一层作用域中的 this

25.面试官:很好,那以 call 为例吧,你能实现一下吗?

我(又有信心了):没问题~

Function.prototype.myCall = function (context, ...args) {
  const fn = this;

  context.fn = fn;

  const val = context.fn(...args);

  delete context.fn;

  return val;
};

26.面试官:我像下面这样调用 myCall,你觉得会有什么问题吗?

function fn() {
  return this;
}

fn.myCall(1);
fn.myCall(true);
fn.myCall('hello');

我:额,会直接报错

27.面试官:对的,但是实际上,我们以浏览器环境为例,像上面这样调用 call,其实是不会报错的。你看: image.png

我(哇,不愧是大佬):哦,平时写代码时,都没注意到还有这些细节。

28.面试官:那你再完善一下代码

我:嗯嗯

Function.prototype.myCall = function (context, ...args) {
  if (typeof context == 'number') {
    context = new Number(context);
  } else if (typeof context == 'string') {
    context = new String(context);
  } else if (typeof context == 'boolean') {
    context = new Boolean(context);
  } else if (context == null || context == undefined) {
    // 兼容node环境
    context = window;
  }

  const fn = this;

  context.fn = fn;

  const val = context.fn(...args);

  delete context.fn;

  return val;
};

29.面试官:你这个代码太多 if,else 了,你看,这样优化一下是不是会更好?

Function.prototype.myCall = function (context, ...args) {
  const map = {
    number: function (context) {
      return new Number(context);
    },
    string: function (context) {
      return new String(context);
    },
    boolean: function (context) {
      return new Boolean(context);
    },
    undefined: window,
    null: window,
  };

  context =
    map[context] ||
    (map[typeof context] && map[typeof context](context)) ||
    context;

  const fn = this;

  context.fn = fn;

  const val = context.fn(...args);

  delete context.fn;

  return val;
};

我:恩恩,是的。这样代码确实更简化

30.面试官:你看,这里其实还有一种情况没有考虑到,就是 es6 的 symbol 数据类型,这里你觉得如果 context 是 symbol 数据,你觉得 this 会变成什么样?

我:不知道...

31.面试官:好,那我们来看看,其实是像下面这样的,它的 this 会变成一个对象 image.png

我:哦,原来如此。

32.面试官:在我们上面的代码中,字符串、数字、布尔值他们都可以通过new String('1')new Number(1)new Boolean(true)这些方式创建包装对象。symbol 类型可以通过new Symbol(Symbol())这样的方式创建它的包装对象吗?

我(猜一把):不可以。

33:面试官:嗯,那如果你想要创建一个 symbol 类型的包装对象,你应该怎么做呢?

我(额...猜不下去了):额...不知道

34: 面试官:行吧。其实你可以像下面这样创建 symbo 类型的包装对象。

var sym = Symbol('foo');
typeof sym;
// 创建symbol类型的包装对象
var symObj = Object(sym);
typeof symObj;

我:嗯嗯。

35.面试官:那现在你再来完善一下代码吧?

我:好的

Function.prototype.myCall = function (context, ...args) {
  const map = {
    number: function (context) {
      return new Number(context);
    },
    string: function (context) {
      return new String(context);
    },
    boolean: function (context) {
      return new Boolean(context);
    },
    undefined: window,
    null: window,
    // 增加一种类型,symbol类型
    symbol: function (context) {
      return Object(context);
    },
  };

  context =
    map[context] ||
    (map[typeof context] && map[typeof context](context)) ||
    context;

  const fn = this;

  context.fn = fn;

  const val = context.fn(...args);

  delete context.fn;

  return val;
};

36.面试官:可以,那你可以说一说你对 symbol 的认识吗?

我:symbol 翻译成中文的意思是象征、标记、记号的意思。在 js 中代表独一无二的值

37.面试官:不错。那你能说一说,symbol 都有哪些使用场景吗?

我(此刻内心已波澜不惊):额...不太知道

38.面试官:行吧。那我来说一说,

第一:symbol 可以作为对象的属性使用。你可以想象一下如果一个复杂对象中含有很多属性,而我们又想要为这个对象添加属性的时候,其实是很容易将这个对象中的某个属性名给覆盖掉,利用 Symbol 值作为属性名就可以很好的避免这一现象。

// 假设你的同事开发了一个模块,这个模块叫a.js,它暴露了一个对象
// a.js
export default {
  type: 'animal',
  name: 'cheese',
  getName: function() {
    ...
  }
  ...等等等等
}

// 现在你要需要往这个对象里添加一个方法
// b.js
import obj from 'a.js'

obj.getName = function() {
}
// 如果你这样写,就会覆盖掉a.js中的getName方法,如果同事c开发的某个模块有用这个模块中的getName方法,那么就会报错。
// 所以,这里,你可以创建一个symbol属性名来添加该方法就可以避免这个问题
const GET_NAME = Symbol('GET_NAME');
obj[GET_NAME] = function() {
  //...
}

第二:同时也可以用来模拟对象的私有属性。

// 现在,如果有同事d也要用到这个a模块
import obj form 'a.js'

// 第一:同事d无法访问到我定义的这个symbol属性相关的值
obj[Symbol('GET_NAME')] // ❌
Symbol(1) == Symbol(1) // false

// 第二:如果同事d要通过JSON.stringify来操作这个obj,也碰不到这个Symbol属性以及相关的值。
//      因为Symbol对应的属性是不可遍历的
console.log(Object.keys(obj)); // [type, name, getName...]
console.log(Object.getOwnPropertyNames(obj)); // [type, name, getName...]
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol('GET_NAME')]

第三:我们也可以用 Symbol 来定义常量。

const APPLE = 'APPLE';
const PEAR = 'PEAR';
const BANANA = 'BANANA';

function selectFruit(fruit) {
  switch(fruit)
    case APPLE:
      chopped(fruit);
    case PEAR:
      boiled(fruit);
    case BANANA:
      eat(fruit);
  }
}

像上面的代码一样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,一般来说,我们业务上通常希望这些常量的值是唯一的,常量少的时候还少,但是常量一多,你可能还要绞尽脑汁给他们取名字。但是有了 Symbol,就简单多了。

const APPLE = Symbol();
const PEAR = Symbol();
const BANANA = Symbol();

我(再次感叹,不愧是大佬):嗯嗯

39.面试官:那刚刚我们实现的这段 myCall 代码,你觉得还有什么地方可以优化?

Function.prototype.myCall = function (context, ...args) {
  const map = {
    number: function (context) {
      return new Number(context);
    },
    string: function (context) {
      return new String(context);
    },
    boolean: function (context) {
      return new Boolean(context);
    },
    undefined: window,
    null: window,
    // 增加一种类型,symbol类型
    symbol: function (context) {
      return Object(context);
    },
  };

  context =
    map[context] ||
    (map[typeof context] && map[typeof context](context)) ||
    context;

  const fn = this;

  context.fn = fn;

  const val = context.fn(...args);

  delete context.fn;

  return val;
};

我:嗯。向 context 对象添加属性那个地方,属性名可以使用 symbol 值来代替。像下面这样

context[Symbol()] = fn

40.面试官(满意地点了点头):不错不错。