前言
你有没有这样的感受,虽然刷了很多题,但是到了真正面试的过程中,面对面试官的步步紧逼的提问,仍然感到力不从心。这是因为,很多人刚开始刷面试题,可能会去收集一堆面试题,然后就开始按照顺序一道一道这么刷过去,这样刷题的问题在于,虽然刷了很多题,但是每道题都是零散的点,对于这道题的相关知识点没有系统的整理和认识。所以,在真正的面试中,面对面试官对某一个点步步紧逼的提问,内心逐渐崩溃 😭
所以,本系列文章,将从 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,其实是不会报错的。你看:
我(哇,不愧是大佬):哦,平时写代码时,都没注意到还有这些细节。
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 会变成一个对象
我:哦,原来如此。
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.面试官(满意地点了点头):不错不错。