01 开篇:为什么要学习本专栏
更新时间:2020-03-08 18:49:49
知识犹如人体的血液一样宝贵。
——高士其
大家好,我是 Nolan,前美团高级前端工程师,长期担任面试官,曾主持过美团的多个重要产品的
开发工作。现在旅居澳大利亚,担任一家支付公司的前端业务负责人。
多年的面试官经历,使我得出一个结论:面试表现和工作能力,并没有大多数人以为的那么相关。
一个技术能力优秀的程序员,很可能因为忘记 Event Loop 的内容而被认为基础知识不扎实;一个
原来在团队中没有存在感每天划水的人,却有可能因为碰巧面试前一天晚上刷到了浏览器缓存机制
的面试题,而被认为是基础扎实的大牛。实际上,如果面试总是能准确地反映出一个人真正的技术
能力,大公司里就不会有这么多划水的人了。
工作能力强,并不代表面试表现就一定好;工作有工作的技巧,面试有面试的技巧,它们有一些联
系,但是它们是两个不同的学科。如果你下个月要参加长跑比赛,从现在开始你会每周练习三次长
跑;但是如果你下个月要参加面试,却不从现在开始花一个月的时间刷一遍面试题,岂不是很奇怪
吗?
市面上有不少介绍前端面试的资料,但是都包含各式各样的问题:有的年代久远题目过时已久;有
的主题不全,某些资料竟然以作者不熟悉 CSS 为由将 CSS 的题目一笔带过,而关于 TypeScript,
React Native,层叠上下文,移动端开发的题目更是少之又少;有的浅尝辄止,只是提供一个答案
不做深入研究,让读者一知半解;有的东拼西凑,完全形不成系统。
所以我决定自己来写一个面试专栏,这个专栏花费了我很多精力,但是我相信是值得的。只要你从
头到尾练习一遍,一定可以解答你的很多疑惑,为你的面试助力。
我选择题目的标准是:
1. 实用性。即,题目背后的知识点一定是面试中大概率会被问到的知识点
2. 代表性。即,每道题目可以都准确地代表一类知识点,这样有利于读者举一反三,遇到这一类
的题目都能有思路
3. 全面性。即,覆盖到所有常见的前端面试知识点,这样读者只需要通读本专栏,而不需要四处
搜寻其他的材料作为补充
4. 科学性。即,越是常考的题目,越放在前面。这样可以保证读者熟知最常考到的题目。比如大
多数面试书会遵循 JavaScript 技术书籍的编写原则:把 JavaScript 的变量与变量类型 这个题
目放在第一节。实际上这是不科学的,因为 JavaScript技术书籍要兼顾到 JavaScript 新手,
所以第一节一定要讲最基本的知识;而大部分面试书籍的读者都有一定的前端知识,就不如把
函数调用、执行上下文、异步函数等知识放在第一章。
本专栏共分为 7 章,分别为:
1. 开篇 本章介绍了整个专栏的基本内容以及设计原则
2. JavaScript
本章包含了 JavaScript 相关知识的常见考点,包含 函数定义、调用、闭包、原型与继承、异
步函数等等内容,是整个专栏中的重中之重,同学们务必要掌握每一道题目。
3. CSS
本章包含了 CSS 单位、布局、层叠上下文、自适应等内容,这一章的内容是很多面试书籍会
忽略的内容,因为 CSS 是很多前端程序员的薄弱项,包括这些书籍的作者。这就造成了很多
对 JavaScript 能侃侃而谈却被一道简单的 CSS 题目难倒的尴尬场景。而本章致力于覆盖 CSS
面试中常见的面试题目,希望能为 CSS 基础薄弱的同学助力。
4. 客户端
本章的内容是浏览器相关,包括 HTML、浏览器、性能优化、HTTP 等知识点,这部分内容考
察面试者在代码能力之外的知识能力。
5. 框架与库
本章的内容包含了 React、Vue、TypeScript、React Native 等常考知识点。React 小节包含
了 React Hook 的内容,而 Vue 则包含了 Vue3 的相关知识。网上关于 TypeScript 的面试题
很少,React Native 的题目则更是难得,笔者总结了一些经典题目。
6. 计算机基础知识
很多公司喜欢考察一些前端以外的知识,以证明你有主动学习的热情。本章总结了一些常见的
计算机相关的面试知识。
7. 简历与面试
本章笔者根据自己多年的担当面试者与面试官的经验,聊一聊关于简历和面试的心得。帮助读
者树立对于面试的正确态度。
希望每位同学都能在这本书中有所收获。
02 函数定义与调用
更新时间:2020-02-23 16:02:13
不要问你的国家能够为你做些什么,而要问你可以为国家做些什么。
——林肯
函数是 JavaScript 中最重要的概念,其定义和调用都包含了很多知识点。特别是其中的函数上下
文,也就是函数中的 this 关键字,是前端面试中的必考知识点。本节的题目会涉及到函数上下文、
箭头函数等内容。
1. 一个页面的脚本如下,请问会打印出什么内容?
function printThis() {
return this;
}
console.log(printThis() === window)
答案:
严格模式下,打印 false ;非严格模式下,打印 true 。
因为当函数被当作独立函数调用时,严格模式下 this 指向 undefined ;非严格模式下 this 指向 wi
ndow 。
解读:
同样一个函数,其被调用的方式有很多,在不同的情况下函数上下文的指向也不一样。这道题就展
示了当函数被当作独立函数调用时的场景。这道题目的另一个价值是与后面将要提到的函数被当作
构造函数(下一道题)的场景作对比。
注意,要尽量回答出严格模式和非严格模式的区别,这个区别是很经典的。
2. 一个页面的脚本如下,请问会打印出什么内容?
function printThis() {
return this;
}
const obj = {printThis}
console.log(obj.printThis() === window)
答案:
打印出 false 。
因为当函数被当作一个对象的方法调用时,其中的 this 指向此对象。由于 printThis 函数变成了 o
bj 对象的一个方法,所以 printThis 中的 this 指向 obj ,而不是 window 。
解读:
这道题考察了当函数被当作对象的方法调用时 this 的指向问题。当函数被当做对象的方法调用时,
this指向此对象,这是在无数面试中被考察过无数遍的内容。
在本题中,虽然函数在被定义时是个独立函数,但是其在被调用时是 obj 对象的一个方法,所以适
用于函数被当作对象方法调用的规则。
3. 一个页面的脚本如下,请问会打印出什么内容?
function Dog() {
this.name = 'Puppy'
}
const dog = Dog()
console.log(dog.name)
答案:
会报错: Uncaught TypeError: Cannot read property 'name' of undefined
因为在这段代码中 Dog 函数被当作普通函数调用,而 Dog 函数没有返回值,也就是说返回值是 undef
ined ,所以当代码尝试去读取 undefined 的 name 属性时会报错。
解读:
这道题的陷阱就在于, Dog 函数的函数名首字母是大写的。我们都知道在 JavaScript 中默认的规则
是:**构造函数的函数名以大写字母开头,其他函数名以小写字母开头。**本题的函数名首字母虽然
是大写,但是它是被当作独立函数调用的,所以应适用于独立函数的规则。
当 Dog 函数被当作构造函数调用时:
function Dog() {
this.name = 'Puppy'
}
const dog = new Dog()
console.log(dog.name)
此时就会如预期一样打印出 Puppy 。
顺便一提,大家在写 React 的时候可能注意到过,其中的组件名都是大写字母开头,如 ant.design
中的 Modal , Menu 等组件,这是因为组件相对于组件实例,正是构造函数相对于构造函数实例的关
系。
4. 一个页面的脚本如下,请问会打印出什么内容?
const puppet = {
rules: false
};
function Emperor() {
this.rules = true;
return puppet;
}
const emperor = new Emperor();
console.log(emperor.rules)
答案:
false
虽然当函数被当作构造函数调用时, this 关键字指向使用构造函数构建的实例。但是 Emperor 构造
函数最后返回了一个 puppet 对象,如果构造函数有返回值且返回值为一个对象,此构造函数构造的
实例就是这个返回值。
所以构造函数返回的实际上是在第一行声明的 puppet 对象,所以 emperor.rules 的值为 false
解读:
这道题函数的使用场景类似于
字,又有构造函数的实例创建过程。
,既有具有独立函数特色的 return 关键
5. Function.prototype.apply 方法和 Function.prototype.call 方
法有什么区别?
答案:
参数传入格式不同。
Function.prototype.apply 接受数组字面量(array literal),如 :
fun.apply(this, ['eat', 'bananas'])
或数组对象, 如:
fun.apply(this, new Array('eat', 'bananas'))
实际上, Function.prototype.apply 可以接受任何形式的类数组对象,如 arguments , NodeList
等,也就是说,只要是有 length 属性和 (0..length-1) 范围的整数属性就可以。如:
{'length': 2, '0': 'eat', '1': 'bananas'}
构
造
函
数
与
独
立
函
数
的
结
合
而 Function.prototype.call 是从第二个参数开始,每个参数依次传入,如:
callback.call(list[n], n1, n2);
解读:
一个很有用的记忆方法:call 这个单词的意思是拨打电话,打电话的时候数字要一个一个地按,而
不是把数字放在数组里一起按,所以 call 方法的参数是要一个一个传入的。而剩下的 apply 方法就
要传入类数组参数了。
6.在浏览器中运行以下脚本,点击页面后,会打印出 false。请问为什
么会打印出 false?
function Button() {
this.clicked = false;
this.click = function () {
this.clicked = true;
console.log(button.clicked, 'clicked');
};
}
const button = new Button();
document.addEventListener("click", button.click)
答案:
因为 addEventListener 的回调函数的函数上下文为触发事件的元素的引用。所以 click 事件发生
时, button.click 回调函数被调用时里面的 this 指向了 document 元素。
所以 document.clicked 被设置为 true ,而 button.clicked 则没有变化,依然为 false 。
解读:
注意打印时打印的是 button.clicked 而不是 this.clicked ,这已经在明显暗示:button 和 this
并不指向同一个对象。
addEventListener 的回调函数的函数上下文为触发事件的元素的引用,为什么要这么设计呢?想象
这样一个场景: