常考的前端基础面试题(笔记)

常考的基础面试题

Html/css基础知识

1.说一下选择器的权重和优先级

CSS 选择器有很多,不同的选择器的权重和优先级不一样,对于一个元素,如果存在多个选择器,那么就需要根据权重来计算其优先级。

权重分为四级,分别是:

  1. 代表内联样式,如style="xxx",权值为 1000;
  2. 代表 ID 选择器,如#content,权值为 100;
  3. 代表类、伪类和属性选择器,如.content:hover[attribute],权值为 10;
  4. 代表元素选择器和伪元素选择器,如divp,权值为 1。

需要注意的是:通用选择器(*)、子选择器(>)和相邻同胞选择器(+)并不在这四个等级中,所以他们的权值都为 0。 权重值大的选择器其优先级也高,相同权重的优先级又遵循后定义覆盖前面定义的情况。

2.flex布局

布局的传统解决方案基于盒子模型,依赖 display 属性 + position 属性 + float 属性来实现布局。但是对于那些特殊布局非常不方便,比如,垂直居中就不太容易实现。在目前主流的移动端页面中,使用 flex 布局可以更好地完成需求,因此 flex 布局的知识是必须要掌握的。

基本使用

任何一个容器都可以使用 flex 布局,代码也很简单。

// 按比例分配容器空间
<style type="text/css">
    .container {
      display: flex;
    }
    .item {
        border: 1px solid #000;
        flex: 1;
    }
</style>

<div class="container">
    <div class="item">aaa</div>
    <div class="item" style="flex: 2">bbb</div>
    <div class="item">ccc</div>
    <div class="item">ddd</div>
</div>

可以看到第三个<div>flex: 2,其他的<div>flex: 1,这样第二个<div>的宽度就是其他的<div>的两倍。

3.说一下实现居中对齐的方式有哪些

水平居中

inline 元素用text-align: center;即可,如下:

像img元素的水平居中也是可以使用的,text-align并不只是对文本元素起作用

.container {
   text-align: center;
}

block 元素可使用margin: auto;,PC端很多都是通过这种方式实现的


.item {
    width:400px;
    margin: auto; 
}

绝对定位元素可结合leftmargin实现,但是必须知道宽度。

结合margin需要提前知道宽度

.item {
    width: 300px;
    height: 100px;
    position: absolute;
    left: 50%;
    margin-left: -150px;
}

也可以使用transfrom,并且不需要知道宽度

.item {
    /* width: 300px; */
    height: 100px;
    position: absolute;
    left: 50%;
    margin-left: -50%;
    transform: translateX(-50%);
    background-color: red;
}
垂直居中

inline 元素可设置line-height的值等于height值,如单行文字垂直居中:

.container {
   height: 50px;
   line-height: 50px;
}

绝对定位元素,可结合leftmargin实现,但是必须知道尺寸。

  • 优点:兼容性好
  • 缺点:需要提前知道尺寸
.container {
    position: relative;
    height: 200px;
}
.item {
    width: 80px;
    height: 40px;
    position: absolute;
    left: 50%;
    top: 50%;
    margin-top: -20px;
    margin-left: -40px;
}

绝对定位可结合transform实现居中。

  • 优点:不需要提前知道尺寸
  • 缺点:兼容性不好 transfrom ,使用在浏览器中使用需要添加前缀进行处理
.container {
    position: relative;
    height: 200px;
}
.item {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: blue;
}

绝对定位结合margin: auto,不需要提前知道尺寸,兼容性好。这个也是比较推荐的一种方式

.container {
    position: relative;
    height: 300px;
}
.item {
    width: 100px;
    height: 50px;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

其他的解决方案还有,不过没必要掌握太多,能说出上文的这几个解决方案即可。

比如还有像flex布局,几行代码就可以实现,也是现在在移动端使用非常多的

如果面试中再遇到这个问题就不用慌了。

4.说一下对Html语义化的理解

所谓“语义”就是为了更易读懂,这要分两部分:

  • 让人(写程序、读程序)更易读懂
  • 让机器(浏览器、搜索引擎)更易读懂

让人更易读懂

对于人来说,代码可读性、语义化就是一个非常广泛的概念了,例如定义 JS 变量的时候使用更易读懂的名称,定义 CSS class 的时候也一样,例如length list等,而不是使用a b这种谁都看不懂的名称。

不过平常考查的“语义化”并不会考查这么广义、这么泛的问题,而是考查 HTML 的语义化,是为了更好地让机器读懂 HTML。

让机器更易读懂

HTML 符合 XML 标准,但又和 XML 不一样 —— HTML 不允许像 XML 那样自定义标签名称,HTML 有自己规定的标签名称。

问题就在这里 —— HTML 为何要自己规定那么多标签名称呢,例如p div h1 ul等 —— 就是为了语义化。其实,如果你精通 CSS 的话,你完全可以全部用<div>标签来实现所有的网页效果,其他的p h1 ul等标签可以一个都不用。但是我们不推荐这么做,这样做就失去了 HTML 语义化的意义。

拿搜索引擎来说,爬虫下载到我们网页的 HTML 代码,它如何更好地去理解网页的内容呢?—— 就是根据 HTML 既定的标签。h1标签就代表是标题;p里面的就是段落详细内容,权重肯定没有标题高;ul里面就是列表;strong就是加粗的强调的内容 …… 如果我们不按照 HTML 语义化来写,全部都用<div>标签,那搜索引擎将很难理解我们网页的内容。

为了加强 HTML 语义化,HTML5 标准中又增加了header section article等标签。因此,书写 HTML 时,语义化是非常重要的,否则 W3C 也没必要辛辛苦苦制定出这些标准来。

5.CSS3 动画

CSS3 可以实现动画,代替原来的 Flash 和 JavaScript 方案。

如何使用,这里还是带着大家回顾下。

首先,使用@keyframes定义一个动画,名称为testAnimation,如下代码,通过百分比来设置不同的 CSS 样式,规定动画的变化。所有的动画变化都可以这么定义出来。

@keyframes testAnimation
{
    0%   {background: red; left:0; top:0;}
    25%  {background: yellow; left:200px; top:0;}
    50%  {background: blue; left:200px; top:200px;}
    75%  {background: green; left:0; top:200px;}
    100% {background: red; left:0; top:0;}
}

然后,针对一个 CSS 选择器来设置动画,例如针对div元素设置动画,如下:

div {
    width: 100px;
    height: 50px;
    position: absolute;

    animation-name: myfirst;
    animation-duration: 5s;
}

animation-name对应到动画名称,animation-duration是动画时长,还有其他属性:

  • animation-timing-function:规定动画的速度曲线。默认是ease
  • animation-delay:规定动画何时开始。默认是 0
  • animation-iteration-count:规定动画被播放的次数。默认是 1
  • animation-direction:规定动画是否在下一周期逆向地播放。默认是normal
  • animation-play-state :规定动画是否正在运行或暂停。默认是running
  • animation-fill-mode:规定动画执行之前和之后如何给动画的目标应用,默认是none,保留在最后一帧可以用forwards

一道题:CSS 的transition和animation有何区别?

首先transitionanimation都可以做动效,从语义上来理解,transition是过渡,由一个状态过渡到另一个状态,比如高度100px过渡到200px;而animation是动画,即更专业做动效的,animation有帧的概念,可以设置关键帧keyframe,一个动画可以由多个关键帧多个状态过渡组成,另外animation也包含上面提到的多个属性。

6.存储相关的:cookie 和 localStorage 有何区别?

cookie

cookie 本身不是用来做服务器端存储的,它是设计用来在服务器和客户端进行信息传递的,因此我们的每个 HTTP 请求都带着 cookie。但是 cookie 也具备浏览器端存储的能力(例如记住用户名和密码),因此就被开发者用上了。

使用起来也非常简单,document.cookie = ....即可。也有一些第三方库,封装了cookie的一写读取,写入删除的操作,比如js-cookie。我们在后台实战项目中使用的。

但是 cookie 有它致命的缺点:

  • 存储量太小,只有 4KB
  • 所有 HTTP 请求都带着,会影响获取资源的效率
  • API 简单,需要封装才能用

localStorage 和 sessionStorage

HTML5 标准就带来了sessionStoragelocalStorage,先拿localStorage来说,它是专门为了浏览器端缓存而设计的。其优点有:

  • 存储量增大到 5MB
  • 不会带到 HTTP 请求中
  • API 适用于数据存储 localStorage.setItem(key, value) localStorage.getItem(key)

sessionStorage的区别就在于它是根据 session 过去时间而实现,而localStorage会永久有效,应用场景不同。例如,一些需要及时失效的重要信息放在sessionStorage中,一些不重要但是不经常设置的信息,放在localStorage中。

这里在使用的时候有个注意点,针对localStorage.setItem,使用时尽量加入到try-catch中,某些浏览器是禁用这个 API 的,要注意。

Js相关

7.说一下js都有哪些数据类型,如何判断数据的类型,值类型和引用类型有什么区别?

ECMAScript 中定义了 6 种原始类型:

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol(ES6 新定义)

现在还新增了一个bigInt 类型,这个给大家讲ES6-10语法的时候,给大家讲过。

注意:原始类型是不包含 Object。像Js的数据类型可以分为两大类,值类型,和引用类型。

8.如何对js中的数据类型进行判断

typeof

typeof xxx得到的值有以下几种类型:undefined boolean number string object functionsymbol ,比较简单。这里需要注意的有三点:

  • typeof null结果是object ,实际这是typeof的一个bug,null是原始值,非引用类型
  • typeof [1, 2]结果是object,结果中没有array这一项,引用类型除了function其他的全部都是object
  • typeof Symbol()typeof获取symbol类型的值得到的是symbol,这是 ES6 新增的知识点

instanceof

用于实例和构造函数的对应。例如判断一个变量是否是数组,使用typeof无法判断,但可以使用[1, 2] instanceof Array来判断。因为,[1, 2]是数组,它的构造函数就是Array。同理:

但是instanceof也有问题
比如 
[] instanceof Array // true
[] instanceof Object // 也是true

最佳的判断方法是什么

// 使用toString方法
// 对于 Object 对象,直接调用 toString()  就能返回 [object Object] 。
// 而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。借用object的toString方法来进行判断
Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

9.值类型和引用类型的区别

除了原始类型,ES 还有引用类型,上文提到的typeof识别出来的类型中,只有objectfunction是引用类型,其他都是值类型。

根据 JavaScript 中的变量类型传递方式,又分为值类型引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的所有,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。

引用类型经常会在代码中按照下面的写法使用,或者说容易不知不觉中造成错误

var obj = {
    a: 1,
    b: [1,2,3]
}
var a = obj.a
var b = obj.b
a = 2
b.push(4)
console.log(obj, a, b)

虽然obj本身是个引用类型的变量(对象),但是内部的ab一个是值类型一个是引用类型,a的赋值不会改变obj.a,但是b的操作却会反映到obj对象上。

所以也是需要注意一下


10.原型和原型链

JavaScript 是基于原型的语言,原型理解起来非常简单,但却特别重要,下面还是通过题目来理解下JavaScript 的原型概念。

一道题:如何理解 JavaScript 的原型

对于这个问题,可以从下面这几个要点来理解和回答,下面几条必须记住并且理解

  • 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(null除外)
  • 所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象
  • 所有的函数,都有一个prototype属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值

通过代码来看一下


// 要点一:自由扩展属性
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;

// 要点二:__proto__
console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn.__proto__);

// 要点三:函数有 prototype
console.log(fn.prototype)

// 要点四:引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
console.log(obj.__proto__ === Object.prototype)

原型的一一些点

先写一个简单的代码示例。

// 构造函数
function Foo(name, age) {
    this.name = name
}
Foo.prototype.alertName = function () {
    alert(this.name)
}
// 创建示例
var f = new Foo('zhangsan')
f.printName = function () {
    console.log(this.name)
}
// 测试
f.printName()
f.alertName()

执行printName时很好理解,但是执行alertName时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName

那么如何判断这个属性是不是对象本身的属性呢?使用hasOwnProperty,常用的地方是遍历一个对象的时候。

var item
for (item in f) {
    // 高级浏览器已经在 for in 中屏蔽了来自原型的属性,但是这里建议大家还是加上这个判断,保证程序的健壮性
    if (f.hasOwnProperty(item)) {
        console.log(item)
    }
}

题目:如何理解 JS 的原型链

原型链

还是接着上面的示例,如果执行f.toString()时,又发生了什么?

// 省略 N 行

// 测试
f.printName()
f.alertName()
f.toString()

因为f本身没有toString(),并且f.__proto__(即Foo.prototype)中也没有toString。这个问题还是得拿出刚才那句话——当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找

如果在f.__proto__中没有找到toString,那么就继续去f.__proto__.__proto__中寻找,因为f.__proto__就是一个普通的对象而已嘛!

  • f.__proto__Foo.prototype,没有找到toString,继续往上找
  • f.__proto__.__proto__Foo.prototype.__proto__Foo.prototype就是一个普通的对象,因此Foo.prototype.__proto__就是Object.prototype,在这里可以找到toString
  • 因此f.toString最终对应到了Object.prototype.toString

这样一直往上找,你会发现是一个链式的结构,所以叫做“原型链”。如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么 —— Object.prototype.__proto__ === null

关于原型,原型链的知识几乎是面试中必考的点,所以大家必须要掌握,根据这张经典的图好好理解下。


11.作用域和闭包

作用域和闭包是前端面试中,最可能考查的知识点。例如下面的题目:

一道题:现在有个 HTML 片段,要求编写代码,点击编号为几的链接就alert弹出其编号

<ul>
    <li>编号1,点击我请弹出1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

一般不知道这个题目用闭包的话,会写出下面的代码:

var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
    list[i].addEventListener('click', function(){
        alert(i + 1)
    }, true)
}

实际上执行才会发现始终弹出的是6,这时候就应该通过闭包来解决:

var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
    list[i].addEventListener('click', function(i){
        return function(){
            alert(i + 1)
        }
    }(i), true)
}

现在使用ES6就更简单了

var list = document.getElementsByTagName('li');
for (let i = 0; i < list.length; i++) {
    list[i].addEventListener('click', function(){
        alert(i + 1)
    }, true)
}

要理解闭包还跌从执行上下文开始

12.执行上下文

先讲一个关于 变量提升 的知识点,面试中可能会遇见下面的问题,很多人都回答错误:

一道题:说出下面执行的结果

console.log(a)  // undefined
var a = 100

fn('zhangsan')  // 'zhangsan' 20
function fn(name) {
    age = 20
    console.log(name, age)
    var age
}

console.log(b); // 这里报错
// Uncaught ReferenceError: b is not defined
b = 100;

在一段 JS 脚本(即一个<script>标签中)执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个 全局执行上下文 环境,先把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。再次强调,这是在代码执行之前才开始的工作。

我们来看下上面的面试小题目,为什么aundefined,而b却报错了,实际 JS 在代码执行之前,要「全文解析」,发现var a,知道有个a的变量,存入了执行上下文,而b没有找到var关键字,这时候没有在执行上下文提前「占位」,所以代码执行的时候,提前报到的a是有记录的,只不过值暂时还没有赋值,即为undefined,而b在执行上下文没有找到,自然会报错(没有找到b的引用)。

另外,一个函数在执行之前,也会创建一个 函数执行上下文 环境,跟 全局上下文 差不多,不过 函数执行上下文 中会多出this arguments和函数的参数。参数和arguments好理解,这里的this需要来看一下。

总结一下:

  • 范围:一段<script>、js 文件或者一个函数
  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

13.this

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子

var a = {
    name: 'A',
    fn: function () {
        console.log(this.name)
    }
}
a.fn()  // this === a
a.fn.call({name: 'B'})  // this === {name: 'B'}
var fn1 = a.fn
fn1()  // this === window

this执行会有不同,主要集中在这几个场景中

  • 作为构造函数执行,构造函数中
  • 作为对象属性执行,上述代码中a.fn()
  • 作为普通函数执行,上述代码中fn1()
  • 用于call apply bind,上述代码中a.fn.call({name: 'B'})

下面再来讲解下什么是作用域和作用域链,作用域链和作用域也是常考的题目。

一道题:如何理解 JS 的作用域和作用域链

14.作用域

ES6 之前 JS 没有块级作用域。例如

if (true) {
    var name = 'zhangsan'
}
console.log(name)

从上面的例子可以体会到作用域的概念,作用域就是一个独立的地盘,让变量不会外泄、暴露出去。上面的name就被暴露出去了,因此,JS 没有块级作用域,只有全局作用域和函数作用域

var a = 100
function fn() {
    var a = 200
    console.log('fn', a) // fn 200
}
console.log('global', a) // global 100
fn()
// 可以看到函数作用域中的a只有在函数中才能访问到

全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样的坏处就是很容易撞车、冲突。

比如下面这样

// 张三写的代码中
var data = {a: 100}

// 李四写的代码中
var data = {x: true}

这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()立即执行函数中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

不过,ES6 中开始加入了块级作用域,使用let定义变量即可,如下:

if (true) {
    let name = 'zhangsan'
}
console.log(name)  // 报错,因为let定义的name是在if这个块级作用域

大家对于作用域这一块还有没有什么疑问的地方。

15.作用域链

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量如何得到 —— 向父级作用域寻找。

var a = 100
function fn() {
    var b = 200
    console.log(a)
    console.log(b)
}
fn()

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a) // 自由变量,顺作用域链向父作用域找
        console.log(b) // 自由变量,顺作用域链向父作用域找
        console.log(c) // 本作用域的变量 不用再向上找
    }
    F2()
}
F1()

16.闭包

讲完这些内容,我们再来看一个例子,通过例子来理解闭包。

function F1() {
    var a = 100
    return function () {
        console.log(a)
    }
}
var f1 = F1()
var a = 200
f1()

自由变量将从作用域链中去寻找,但是 依据的是函数定义时的作用域链,而不是函数执行时,以上这个例子就是闭包。闭包主要有两个应用场景:

  • 函数作为返回值,上面的例子就是
  • 函数作为参数传递,看以下例子
function F1() {
    var a = 100
    return function () {
        console.log(a)
    }
}
function F2(f1) {
    var a = 200
    console.log(f1())
}
var f1 = F1()
F2(f1)

// 闭包有个很好的作用,就是可以进行变量的保存
function fn(){
  var a= 1;
  return function(){
    console.log(a);
    a++;
  };
}
var a = fn();
a();
a();

// 比如普通的
function fn(){
  var a= 1;
  a++;
  console.log(a);
}
fn()
fn();
这个a每次都是从1开始累加

现在大家是不是对原型,作用域,原型链,作用链是不是就好理解一些了。


17.异步

异步和同步也是面试中常考的内容,来看一下。

同步 vs 异步

先看下面的 demo,根据程序阅读起来表达的意思,应该是先打印100,1秒钟之后打印200,最后打印300。但是实际运行根本不是那么回事。

console.log(100)
setTimeout(function () {
    console.log(200)
}, 1000)
console.log(300)
// 这就是异步,在js中异步分为宏任务,微任务,宏任务有setTiemout,setInterval,等,微任务,promise

再对比以下程序。先打印100,再弹出200(等待用户确认),最后打印300。这个运行效果就符合预期要求。

console.log(100)
alert(200)  // 1秒钟之后点击确认
console.log(300)

这俩到底有何区别?—— 第一个示例中间的步骤根本没有阻塞接下来程序的运行,而第二个示例却阻塞了后面程序的运行。前面这种表现就叫做 异步(后面这个叫做 同步 ),即不会阻塞后面程序的运行

异步和单线程

大家有没有想过,js为什么需要异步?

JS 需要异步的根本原因是 JS 是单线程运行的,即在同一时间只能做一件事,不能“一心二用”。

一个 Ajax 请求由于网络比较慢,请求需要 5 秒钟。如果是同步,这 5 秒钟页面就卡死在这里啥也干不了了。异步的话,就好很多了,5 秒等待就等待了,其他事情不耽误做,至于那 5 秒钟等待是网速太慢,不是因为 JS 的原因。

讲到单线程,我们再来看个面试真题:

一道题:下面代码的执行过程和结果

var a = true;
setTimeout(function(){
    a = false;
}, 100)
while(a){
    console.log('while执行了')
}

这是一个很有迷惑性的题目,不少候选人认为100ms之后,由于a变成了false,所以while就中止了,实际不是这样,因为JS是单线程的,所以进入while循环之后,没有「时间」(线程)去跑定时器了,所以这个代码跑起来是个死循环!

前端异步的场景

  • 定时 setTimeout setInterval
  • 网络请求,如 Ajax <img>加载

Ajax 代码示例

console.log('start')
$.get('./data1.json', function (data1) {
    console.log(data1)
})
console.log('end')

img 代码示例(常用于打点统计)

console.log('start')
var img = document.createElement('img')
// 或者 img = new Image()
img.onload = function () {
    console.log('loaded')
    img.onload = null
}
img.src = '/xxx.png'
console.log('end')

了解完前边的一些知识,我们来集中做几道题。

JavaScript面试题

第一道题

// 京程一灯,每日一题
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);
// 写出代码执行结果,并解释为什么

// 答案与解析
true false
对于运算符><,一般的计算从左向右
第一个题:1 < 2 等于 true, 然后true < 3true == 1 ,因此结果是true
第二个题:3 > 2 等于 true, 然后true > 1, true == 1 ,因此结果是false

第二道题

[typeof null, null instanceof Object]
// 写出代码执行的结果,并解释为什么

//答案与解析
 ["object", false]
1typeof操作符返回一个字符串,表示未经计算的操作数的类型
	类型					结果
Undefined		"undefined"
Null				"object"
Boolean			"boolean"
Number			"number"
String			"string"
Symbol			"symbol"
函数对象			"function"
任何其他对象	"object"
typeof null === 'object';// 从最开始的时候javascript就是这样
JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0typeof null就错误的返回了"object"。这算一个bug,但是被拒绝修复,因为影响的web系统太多

2instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
null不是以Object原型创建,因此返回false

第三道题

// 逗号表达式
var x = 20;
var temp = {
    x: 40,
    foo: function() {
        var x = 10;
      	console.log(this.x);
    }
};
(temp.foo, temp.foo)();

// 写出打印结果
20
逗号操作符,逗号操作符会从左到右计算它的操作数,返回最后一个操作数的值。所以(temp.foo, temp.foo)();等价于var fun = temp.foo; fun();,fun调用时this指向window,所以返回20

第四题

链式调用
// 实现 (5).add(3).minus(2) 功能
// console.log((5).add(3).minus(2)); // 6

// 这里就可以通过原型来实现,这其实就是实现一个链式调用
Number.prototype.add = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this + number;
};
Number.prototype.minus = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this - number;
};
console.log((5).add(3).minus(2));

第五题

var a = 1;        
(function a () {            
    a = 2;            
    console.log(a);        
})();

// 答案
ƒ a () {            
    a = 2;            
    console.log(a);        
}
这个也是前边讲到过的。
/*
立即调用的函数表达式(IIFE) 有一个 自己独立的 作用域,如果函数名称与内部变量名称冲突,就会永远执行函数本身;所以上面的结果输出是函数本身;
*/

第六题

var a = [0];
if(a){
    console.log(a == true);
}else{
    console.log(a);
}

/*
答案:false
当a出现在if的条件中时,被转成布尔值,而Boolean([0])为true,所以就进行下一步判断 a == true,在进行比较时,[0]被转换成了0,所以 0==true 为false
js的规则是:  
	如果比较的是原始类型的值,原始类型的值会转成数值再进行比较
	所以 0 == true 就是 0 == Number(true)  0 == 1 => false
	
'true' == true //false Number('true')->NaN  Number(true)->1
'' == 0//true
'1' == true//true  Number('1')->1

对象与原始类型值比较,对象会转换成原始类型的值再进行比较。

undefined和null与其它类型进行比较时,结果都为false,他们相互比较时结果为true。(null == undefined)
*/

第七题

var a = ?;
if(a == 1 && a== 2 && a== 3){
 	console.log(1);
}

/*
比较操作涉及多不同类型的值时候,会涉及到很多隐式转换,其中规则繁多即便是经验老道的程序员也没办法完全记住,特别是用到 `==` 和 `!=` 运算时候。所以一些团队规定禁用 `==` 运算符换用`===` 严格相等。
*/
// 答案一
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("1")
}
/*
	考察你的找茬能力,注意if里面的空格,它是一个Unicode空格字符,不被ECMA脚本解释为空格字符(这意味着它是标识符的有效字符)。所以它可以解释为
  var a_ = 1;
  var a = 2;
  var _a = 3;
  if(a_==1 && a== 2 &&_a==3) {
      console.log("1")
  }
*/
//答案二
var a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	如果原始类型的值和对象比较,对象会转为原始类型的值,再进行比较。
	对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法。
*/
// 答案三
var a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);
/*
	比较巧妙的方式,array也属于对象,
	对于数组对象,toString 方法返回一个字符串,该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。
	数组 toString 会调用本身的 join 方法,这里把自己的join方法该写为shift,每次返回第一个元素,而且原数组删除第一个值,正好可以使判断成立
*/
// 答案四
var i = 0;
with({
  get a() {
    return ++i;
  }
}) {
  if (a == 1 && a == 2 && a == 3)
    console.log("1");
}
/*
	with 也是被严重建议不使用的对象,这里也是利用它的特性在代码块里面利用对象的 get 方法动态返回 i.
*/
// 答案五
var val = 0;
Object.defineProperty(window, 'a', {
  get: function() {
    return ++val;
  }
});
if (a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	全局变量也相当于 window 对象上的一个属性,这里用defineProperty 定义了 a的 get 也使得其动态返回值。和with 有一些类似。
*/

// 答案六
let a = {[Symbol.toPrimitive]: ((i) => () => ++i) (0)};
if (a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。我们之前在定义类的内部私有属性时候习惯用 __xxx ,这种命名方式避免别人定义相同的属性名覆盖原来的属性,有了 Symbol  之后我们完全可以用 Symbol值来代替这种方法,而且完全不用担心被覆盖。
	除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。Symbol.toPrimitive就是其中一个,它指向一个方法,表示该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。这里就是改变这个属性,把它的值改为一个 闭包 返回的函数。
*/

业务中一般不会写出这种代码,重点还是知识点的考察

第八题

let a = {n: 1};
let b = a;
a.x = a = {n: 2};
console.log(a.x) 	
console.log(b.x)

答案:
undefined {n:2}

注意点:

1: 点的优先级大于等号的优先级
2: 对象以指针的形式进行存储,每个新对象都是一份新的存储地址

解析:

- `var b = a;` b 和 a 都指向同一个地址。
- `.`的优先级高于`=`。所以先执行`a.x`,于是现在的`a``b`都是`{n: 1, x: undefined}`- `=`是从右向左执行。所以是执行 `a = {n: 2}`,于是`a`指向了`{n: 2}`
- 再执行 `a.x = a`。 这里注意,`a.x` 是最开始执行的,已经是`{n: 1, x: undefined}`这个地址了,而不是一开的的那个`a`,所以也就不是`{n: 2}`了。而且`b`和旧的`a`是指向一个地址的,所以`b`也改变了。
- 但是,`=`右面的a,是已经指向了新地址的新`a`- 所以,`a.x = a` 可以看成是`{n: 1, x: undefined}.x = {n: 2}`
- 最终得出
  a = { n: 2 },
  b = {
     n: 1,
     x: { n: 2 }
  }


第九题

// 实现一个模板引擎
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template,data){
  // your code
}
// 补充代码,使代码可以正确执行

// 代码实现
function render(template, data) {
  const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
  if (reg.test(template)) { // 判断模板里是否有模板字符串
    const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
    template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
    return render(template, data); // 递归的渲染并返回渲染后的结构
  }
  return template; // 如果模板没有模板字符串直接返回
}

第十题 this指向

// 来一道面试题
var a=10;
var foo={
  a:20,
  bar:function(){
      var a=30;
      return this.a;
    }
}
console.log(foo.bar());
console.log((foo.bar)());
console.log((foo.bar=foo.bar)());
console.log((foo.bar,foo.bar)());

// 答案:
20 20 10 10

// 第一问  foo.bar()
/*
	foo调用,this指向foo , 此时的 this 指的是foo,输出20
*/
// 第二问  (foo.bar)()
/*
	给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于foo.bar(),输出20
*/
// 第三问  (foo.bar=foo.bar)()
/*
	等号运算,
	相当于重新给foo.bar定义,即
	foo.bar = function () {
    var a = 10;
    return this.a;
	}
	就是普通的复制,一个匿名函数赋值给一个全局变量
	所以这个时候foo.bar是在window作用域下而不是foo = {}这一块级作用域,所以这里的this指代的是window,输出10
*/
// 第四问  (foo.bar,foo.bar)()
/*
	1.逗号运算符,
	2.逗号表达式,求解过程是:先计算表达式1的值,再计算表达式2的值,……一直计算到表达式n的值。最后整个逗号表达式的值是表达式n的值。逗号运算符的返回值是最后一个表达式的值。
  3.其实这里主要还是经过逗号运算符后,就是纯粹的函数了,不是对象方法的引用,所以这里this指向的是window,输出10
  4.第三问,第四问,一个是等号运算,一个是逗号运算,可以这么理解,经过赋值,运算符运算后,都是纯粹的函数,不是对象方法的引用。所以函数指向的this都是windows的。
*/

如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

首先来看一下this绑定的规则,来详细看一下,这样再遇到this的问题,可以从容应对

  • 默认绑定

    • // 默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
      function sayHi(){
          console.log('Hello,', this.name);
      }
      var name = 'yideng';
      sayHi();
      //在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。
      // 如果在浏览器环境中运行,那么结果就是 Hello,yideng
      // 如果在node环境中运行,结果就是 Hello,undefined.这是因为node中name并不是挂在全局对象上的。
      
  • 隐式绑定

    • // 函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun()
      function sayHi(){
          console.log('Hello,', this.name);
      }
      var person = {
          name: 'yidneg1',
          sayHi: sayHi
      }
      var name = 'yidneg2';
      person.sayHi();
      // Hello yideng1
      // sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person)
      
    • 需要注意的是:对象属性链中只有最后一层会影响到调用位置。

    • function sayHi(){
          console.log('Hello,', this.name);
      }
      var person2 = {
          name: 'yideng1',
          sayHi: sayHi
      }
      var person1 = {
          name: 'yideng2',
          friend: person2
      }
      person1.friend.sayHi();
      // Hello yideng1
      //因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend。
      
    • **隐式绑定有一个大陷阱,**绑定很容易丢失(或者说容易给我们造成误导,我们以为this指向的是什么,但是实际上并非如此).

    • function sayHi(){
          console.log('Hello,', this.name);
      }
      var person = {
          name: 'yideng1',
          sayHi: sayHi
      }
      var name = 'yideng2';
      var Hi = person.sayHi;
      Hi();
      // Htllo yideng2
      // Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢记住这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定。
      
  • 显示绑定

    • 显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。

    • call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

    • function sayHi(){
          console.log('Hello,', this.name);
      }
      var person = {
          name: 'yideng1',
          sayHi: sayHi
      }
      var name = 'yideng2';
      var Hi = person.sayHi;
      Hi.call(person); //Hi.apply(person)
      // Hello yideng1  因为使用硬绑定明确将this绑定在了person上。
      
    • 使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,继续往下看。

    • function sayHi(){
          console.log('Hello,', this.name);
      }
      var person = {
          name: 'yideng1',
          sayHi: sayHi
      }
      var name = 'yideng2';
      var Hi = function(fn) {
          fn();
      }
      Hi.call(person, person.sayHi); 
      // Hello yideng2
      输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。
      现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。
      
      var Hi = function(fn) {
          fn.call(this);
      }
      这样就行了
      因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。
      
  • new 绑定

    • javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”

    • 前边我们提到new 操作符都干了什么

      • 创建一个空对象,构造函数中的this指向这个空对象
      • 这个新对象被执行 [[原型]] 连接
      • 执行构造函数方法,属性和方法被添加到this引用的对象中
      • 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
    • 因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。

    • function sayHi(name){
          this.name = name;
      	
      }
      var Hi = new sayHi('yideng');
      console.log('Hello,', Hi.name); // Hello yideng
      
  • 绑定优先级

    • 我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办?
    • 显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:
      • new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
      • 感兴趣的可以写个demo测试看看
  • 绑定例外

    • 凡事都有例外,this的规则也是这样。

    • 如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

    • var foo = {
          name: 'yideng1'
      }
      var name = 'yideng2';
      function bar() {
          console.log(this.name);
      }
      bar.call(null); //yideng2
      因为这时实际应用的是默认绑定规则。
      
  • 箭头函数

    • 箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:
      • 函数体内的this对象,继承的是外层代码块的this。
      • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
      • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
      • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
      • 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向

第十一题 加载页面渲染过程

回答这种概念性问题的时候,关键要抓住核心的要点,把要点说全面,然后再稍微加一些解析,要简明扼要,思路清晰,不能拖沓。

面试题目:浏览器从加载到渲染页面的过程

  • 首先回答加载的流程,回答要点
    • 浏览器根据 DNS 服务器得到域名的 IP 地址
    • 向这个 IP 的机器发送 HTTP 请求
    • 服务器收到、处理并返回 HTTP 请求
    • 浏览器得到返回内容
  • 稍加分析
    • 例如在浏览器输入https//yidengxuetang.com的时候,首先经过 DNS 解析,yidengxuetang.com对应的 IP 是101.200.185.250,然后浏览器向该 IP 发送 HTTP 请求。
    • server 端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。
  • 接下来就是渲染过程了,回答要点
    • 根据 HTML 结构生成 DOM 树
    • 根据 CSS 生成 CSSOM
    • 将 DOM 和 CSSOM 整合形成 RenderTree
    • 根据 RenderTree 开始渲染和展示
    • 遇到<script>时,会执行并阻塞渲染
  • 加以分析
    • 浏览器已经拿到了 server 端返回的 HTML 内容,开始解析并渲染。最初拿到的内容就是一堆字符串,必须先结构化成计算机擅长处理的基本数据结构,因此要把 HTML 字符串转化成 DOM 树 —— 树是最基本的数据结构之一。
    • 解析过程中,如果遇到<link href="..."><script src="...">这种外链加载 CSS 和 JS 的标签,浏览器会异步下载,下载过程和上文中下载 HTML 的流程一样。只不过,这里下载下来的字符串是 CSS 或者 JS 格式的。
    • 浏览器将 CSS 生成 CSSOM,再将 DOM 和 CSSOM 整合成 RenderTree ,然后针对 RenderTree 即可进行渲染了。大家可以想一下,有 DOM 结构、有样式,此时就能满足渲染的条件了。另外,这里也可以解释一个问题 —— 为何要将 CSS 放在 HTML 头部?—— 这样会让浏览器尽早拿到 CSS 尽早生成 CSSOM,然后在解析 HTML 之后可一次性生成最终的 RenderTree,渲染一次即可。如果 CSS 放在 HTML 底部,会出现渲染卡顿的情况,影响性能和体验。
    • 最后,渲染过程中,如果遇到<script>就停止渲染,执行 JS 代码。因为浏览器渲染和 JS 执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染 DOM 冲突。待<script>内容执行完之后,浏览器继续渲染。最后再思考一个问题 —— 为何要将 JS 放在 HTML 底部?—— JS 放在底部可以保证让浏览器优先渲染完现有的 HTML 内容,让用户先看到内容,体验好。另外,JS 执行如果涉及 DOM 操作,得等待 DOM 解析完成才行,JS 放在底部执行时,HTML 肯定都解析成了 DOM 结构。JS 如果放在 HTML 顶部,JS 执行的时候 HTML 还没来得及转换为 DOM 结构,可能会报错。

第十二题 性能优化相关

性能优化的题目也是面试常考的,这类题目有很大的扩展性,能够扩展出来很多小细节,而且对个人的技术视野和业务能力有很大的挑战。

  • 优化原则是以更好的用户体验为标准的,目标就是

    • 多使用内存、缓存或者其他方法
    • 减少 CPU 和GPU 计算,更快展现
  • 优化方向

    • 减少页面体积,提升网络加载

      • 静态资源的压缩合并(JS 代码压缩合并、CSS 代码压缩合并、雪碧图)
      • 静态资源缓存(资源名称加 MD5 戳)
      • 使用 CDN 让资源加载更快
    • 优化页面渲染

      • CSS 放前面,JS 放后面

      • 懒加载(图片懒加载、下拉加载更多)

      • 减少DOM 查询,对 DOM 查询做缓存

      • 减少DOM 操作,多个操作尽量合并在一起执行(DocumentFragment

      • 事件节流

      • 尽早执行操作(DOMContentLoaded

        • window.addEventListener('load', function () {
              // 页面的全部资源加载完才会执行,包括图片、视频等
          })
          document.addEventListener('DOMContentLoaded', function () {
              // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
          })
          
      • 使用 SSR 后端渲染,数据直接输出到 HTML 中,减少浏览器使用 JS 模板渲染页面 HTML 的时间

第十三题 算法题

现在算法题,也是经常考的,比如这里大家栏感受下一道简单的算法题。

/*
	给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效
  有效字符串需满⾜:
 	 	1. 左括号必须⽤相同类型的右括号闭合。
  	2. 左括号必须以正确的顺序闭合。
  注意空字符串可被认为是有效字符串。
  示例1:
  	输⼊: "()"
  	输出: true
  示例2:
  	输⼊: "()[]{}"
  	输出: true
  示例 3:
  	输⼊: "(]"
  	输出: false
  示例 4:
  	输⼊: "([)]"
  	输出: false
  示例 5:
  	输⼊: "{[]}"
  	输出: true
*/

// 思路
出栈、入栈的思想
1)首先,我们通过上边的例子可以分析出什么样子括号匹配是复合物条件的,两种情况。
	①第一种(非嵌套情况):{} [] ;
	②第二种(嵌套情况):{ [ ( ) ] } 。
除去这两种情况都不是符合条件的。
2)然后,我们将这些括号自右向左看做栈结构,右侧是栈顶,左侧是栈尾。
3)如果编译器中的括号是左括号,我们就入栈(左括号不用检查匹配);如果是右括号,就取出栈顶元素检查是否匹配。
4)如果匹配,就出栈。否则,就返回 false// 代码实现
var isValid = function(s){
  let stack = [];
  var obj = {
     "[": "]",
     "{": "}",
     "(": ")",
  };
  // 取出字符串中的括号
  for (var i = 0; i < s.length;i++){
    if(s[i] === "[" || s[i] === "{" || s[i] === "("){
      // 如果是左括号,就进栈
      stack.push(s[i]);
    }else{
   		var key = stack.pop();
      // 如果栈顶元素不相同,就返回false
      if(obj[key] !== s[i]){
        return false;
      }
    }
  }
  return stack.length ===  0
}
var str = "([{}])"
isValid(str); // true

现在面试,有人经常吐槽 面试造火箭,工作拧螺丝,拿这句话诟病当前一些互联网大厂的算法面试,因此就有这样的言论: 除了应付面试,学算法其实没啥用

其实这主要原因是因为,现在随着技术生态的发展,各位走在领域前沿的大牛们已经给大家备足了轮子,遇到一般的业务问题直接把人家的方案拿到用就可以了。

但是这样就有一个问题,我们有没有想过自己的价值在哪?比如说照着设计稿画出一个简单的 Button,你能完成,别的前端也能完成,甚至后后端的同学都能把效果差不多做出来,那这个时候谈何个人价值?只不过在一个随时可替代的岗位上完成了大多数人能轻易做到的事情,张三来完成,或者李四来完成,其实没什么区别。

但是现在如果面对的是一个复杂的工程问题,需要你来开发一个辅助业务的脚手架工具,改造框架源码来提高项目的扩展性,或者面对严重的性能问题能马上分析出原因,然后给出解决的思路并在不同因素中平衡,这些都不是一个业余的玩家能够在短时间内胜任的,这就是体现自己价值的地方。

回到算法本身,它代表的是你解决更加复杂问题能力的一部分。

拿大家都基本熟悉的vue为例,如果你以前没有接触过深度优先遍历递归的概念,没有看过相应的代码,那么虚拟 DOM 整个patch的源码你是基本不可能看懂的;如果你没有系统掌握过先进后出这种特点的应用,你也是很难理解 Vue 模板编译阶段为什么要用栈来检查标签是否正常闭合;同样的,如果你没有回溯这种算法的代码经验,你也是很难理解 Vue 模板编译的优化阶段,到底是怎样在从父到子深度优先遍历的过程中检查到非静态的子节点后给父节点打上标记;并且,如果你以前不知道 LRU 缓存淘汰算法究竟是个什么东西,你看到keep-alive组件的实现这里会非常纳闷:

所以这也是现在很多人,在阅读源码时非常吃力的原因。

既然这样,那我们算法这块就必须掌握,

第十四题 Vue相关

Vue响应式原理、生命周期,组件通信等等。

生命周期钩子函数

beforeCreate 钩子函数调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。

然后会执行 created 钩子函数,在这一步的时候已经可以访问到之前不能访问到的数据,但是这时候组件还没被挂载,所以是看不到的。

接下来会先执行 beforeMount 钩子函数,开始创建 VDOM,最后执行 mounted 钩子,并将 VDOM 渲染为真实 DOM 并且渲染数据。组件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。

接下来是数据更新时会调用的钩子函数 beforeUpdateupdated,这两个钩子函数没什么好说的,就是分别在数据更新前和更新后会调用。

另外还有 keep-alive 独有的生命周期,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

最后就是销毁组件的钩子函数 beforeDestroydestroyed。前者适合移除事件、定时器等等,否则可能会引起内存泄露的问题。然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的 destroyed 钩子函数。

组件通信

组件通信一般分为以下几种情况:

  • 父子组件通信
  • 兄弟组件通信
  • 跨多层级组件通信
  • 任意组件

对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。

父子通信

父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。

这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props, 而是必须通过发送事件的方式告知父组件修改数据。

另外这两种方式还可以使用语法糖 v-model 来直接实现,因为 v-model 默认会解析成名为 valueprop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定,常用于 UI 控件上,但是究其根本,还是通过事件的方法让父组件修改数据。

当然我们还可以通过访问 $parent 或者 $children 对象来访问组件实例中的方法和数据。

另外如果你使用 Vue 2.3 及以上版本的话还可以使用 $listeners.sync 这两个属性。

$listeners 属性会将父组件中的 (不含 .native 修饰器的) v-on 事件监听器传递给子组件,子组件可以通过访问 $listeners 来自定义监听器。

.sync 属性是个语法糖,可以很简单的实现子组件与父组件通信

<!--父组件中-->
<input :value.sync="value" />
<!--以上写法等同于-->
<input :value="value" @update:value="v => value = v"></comp>
<!--子组件中-->
<script>
  this.$emit('update:value', 1)
</script>

兄弟组件通信

对于这种情况可以通过查找父组件中的子组件实现,也就是 this.$parent.$children,在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信。

跨多层次组件通信

对于这种情况可以使用 Vue 2.2 新增的 API provide / inject,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。

假设有父组件 A,然后有一个跨多层级的子组件 B

// 父组件 A
export default {
  provide: {
    data: 1
  }
}
// 子组件 B
export default {
  inject: ['data'],
  mounted() {
    // 无论跨几层都能获得父组件的 data 属性
    console.log(this.data) // => 1
  }
}

任意组件

这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况

computed 和 watch 区别

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。

watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch

另外 computedwatch 还都支持对象的写法,这种方式知道的人并不多。

vm.$watch('obj', {
    // 深度遍历
    deep: true,
    // 立即触发
    immediate: true,
    // 执行的函数
    handler: function(val, oldVal) {}
})
var vm = new Vue({
  data: { a: 1 },
  computed: {
    aPlus: {
      // this.aPlus 时触发
      get: function () {
        return this.a + 1
      },
      // this.aPlus = 1 时触发
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
keep-alive 组件有什么作用

如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

对于 keep-alive 组件来说,它拥有两个独有的生命周期钩子函数,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

v-show 与 v-if 区别

v-show 只是在 display: nonedisplay: block 之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSS,DOM 还是一直保留着的。所以总的来说 v-show 在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。

v-if 的话就得说到 Vue 底层的编译了。当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景。

并且基于 v-if 的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。

组件中 data 什么时候可以使用对象

这道题目其实更多考的是 JS 功底。

组件复用时所有组件实例都会共享 data,如果 data 是对象的话,就会造成一个组件修改 data 以后会影响到其他所有组件,所以需要将 data 写成函数,每次用到就调用一次函数获得新的数据。

当我们使用 new Vue() 的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了。

响应式原理

Vue 内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 setget 的事件。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    // 可枚举
    enumerable: true,
    // 可配置
    configurable: true,
    // 自定义函数
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代码简单的实现了如何监听数据的 setget 的事件,

第十五题 聊到具体的项目

一般在面试中,比如一面、二面基本会问到具体做过的项目,然后就是追问项目的细节。一般可能会出现这些问题

  1. 发现你简历的一个项目,直接让你介绍下这个项目
  2. 让你回忆下你做过的项目中,最值得分享(最大型/最困难/最能体现技术能力/最难忘)的
  3. 如果让你设计 xx 系统/项目,你会怎么着手干

这类跟项目相关的综合性问题,既能体现候选人的技术水平、业务水平和架构能力,也能够辨别候选人是不是真的做过项目,还能够发现候选人的一些软技能。

如何进行应对
  • 首先简历中的项目,肯定是你精挑细选的,不能随便选几个,要做好充分的准备,简历中的项目,既要体现技术难度,又要想好细节。一般介绍一个项目可以按几步来
  • 介绍项目背景
    • 这个项目为什么做,当初大的环境背景是什么?还是为了解决一个什么问题而设立的项目?背景是很重要的,如果不了解背景,一上来就听一个结论性的项目,面试官可能对于项目的技术选型、技术难度会有理解偏差,甚至怀疑是否真的有过这样的项目。
    • 比如一上来就说:我们的项目采用了「backbone」来做框架,然后。。。而「backbone」已经是三四年前比较新鲜的技术,现在会有更好的选择方案,如果不介绍项目的时间背景,面试官肯定一脸懵逼。
  • 承担角色
    • 项目涉及的人员角色有哪些,自己在其中扮演的角色是什么?
    • 这里候选往往人会自己给自己挖坑,比如把自己在项目中起到的作用夸大等。一般来说,面试官细节追问的时候,如果候选人能够把细节或者技术方案等讲明白、讲清楚,不管他是真的做过还是跟别人做过,或者自己认真思考过,都能体现候选人的技术水平和技术视野。前提还是在你能够兜得住的可控范围之内做适当的「美化」。
  • 最终的结果和收益
    • 项目介绍过程中,应该介绍项目最终的结果和收益,比如项目最后经过多久的开发上线了,上线后的数据是怎样的,是否达到预期,还是带来了新的问题,遇见了问题自己后续又是怎样补救的。
  • 非常重要的:项目的总结和反思
    • 有总结和反思,才会有进步。 项目做完了往往会有一些心得和体会,这时候应该跟面试官说出来。在梳理项目的总结和反思时,可以按照下面的列表来梳理:
      • 收获有哪些?
      • 是否有做得不足的地方,怎么改进?
      • 是否具有可迁移性?
    • 比如,之前详细介绍了某个项目,这个项目当时看来没有什么问题,但是现在有更好的解决方案了,候选人就应该在这里提出来:现在看来,这个项目还有 xx 的问题,我可以通过 xx 的方式来解决。
    • 再比如:做这个项目的时候,你做得比较出彩的地方,可以迁移到其他项目中直接使用,小到代码片段,大到解决方案,总会有你值得总结和梳理的地方。
    • 介绍完项目总结这部分,也可以引导面试官往自己擅长的领域思考。比如上面提到项目中的问题,可以往你擅长的方面引导,即使面试官没有问到,你也介绍到了。

按照上面的四段体介绍项目,会让面试官感觉候选人有清晰的思路,对整个项目也有理解和想法,还能够总结反思项目的收益和问题,可谓「一箭三雕」。

没有做过大型项目怎么办

对于刚刚找工作的应届生,或者面试官让你进行一个大型项目的设计,候选人可能没有类似的经验。这时候不要用「我不会、没做过」一句话就带过。

如果是实在没有项目可以说,那么可以提自己日常做的练手项目,或者看到一个解决方案的文章/书,提到的某个项目,抒发下自己的想法。

如果是对于面试官提出来需要你设计的项目/系统,可以按照下面几步思考:

  • 有没有遇见过类似的项目
  • 有没有读过类似解决方案的文章
  • 项目能不能拆解,拆解过程中能不能发现自己做过的项目可以用
  • 项目解决的问题是什么,这类问题有没有更好的解决方案

总之,切记不要一句「不知道、没做过」就放弃,每一次提问都是自己表现的机会。

项目细节和技术点的追问

介绍项目的过程中,面试官可能会追问技术细节,所以我们在准备面试的时候,应该尽量把技术细节梳理清楚,技术细节包括:

  • 技术选型方案:当时做技术选型所面临的状况
  • 技术解决方案:最终确定某种技术方案的原因,比如:选择用 Vue 而没有用 React 是为什么?
  • 项目数据和收益
  • 项目中最难的地方
  • 遇见的坑:如使用某种框架遇见哪些坑

一般来说,做技术选型的时候需要考虑下面几个因素:

  • 时代:现在比较火的技术是什么,为什么火起来,解决了什么问题,能否用到我的项目中?
  • 团队:个人或者团队对某种技术的熟悉程度是怎样的,学习成本又是怎样的?
  • 业务需求:需求是怎样的,能否套用现在的成熟解决方案/库来快速解决?
  • 维护成本:一个解决方案的是否再能够 cover 住的范围之内?

在项目中遇见的数据和收益应该做好跟踪,保证数据的真实性和可信性。另外,遇见的坑可能是面试官问得比较多的,尤其现在比较火的一些技术(Vue、React、webpack),一般团队都在使用,所以一定要提前准备下

第十六题:软技能问题回答

  • 韧性:抗压能力,在一定项目压力下能够迎难而上,比如勇于主动承担和解决技术难题
  • 责任心:对于自己做过的项目,能够出现 bug 之类主动解决
  • 持续学习能力:IT 行业是个需要不断充电的行业,尤其 Web 前端这些年一直在巨变,所以持续学习能力很重要
  • 团队合作能力:做项目不能个人英雄主义,应该融入团队,跟团队一起打仗
  • 交流沟通能力:经常会遇见沟通需求和交互设计的工作,应该乐于沟通分享
回想下你遇见过最难打交道的同事,你是如何跟他沟通的

一般来说,工作中总会遇见一两个自己不喜欢的人,这种情况应该尽量避免冲突,从自己做起慢慢让对方感觉到自己的合作精神。

所以,遇见难打交道的同事,不要急于上报领导,应该自己主动多做一些事情,比如规划好工作安排,让他选择自己做的事情,有了结论记得发邮件确认下来,这样你们的领导和其他成员都会了解到工作的安排,在鞭笞对方的同时,也做到了职责明确。在项目当中,多主动检查项目进展,提前发现逾期的问题。

重点是突出:自己主动沟通解决问题的意识,而不是遇见问题就找领导。

当你被分配一个几乎不可能完成的任务时,你会怎么做

这种情况下,一般通过下面方式来解决:

  1. 自己先查找资料,寻找解决方案,评估自己需要怎样的资源来完成,需要多长时间
  2. 能不能借助周围同事来解决问题
  3. 拿着分析结果跟上级反馈,寻求帮助或者资源

突出的软技能:分析和解决问题,沟通寻求帮助。

业余时间都做什么?除了写码之外还有什么爱好

这类问题也是面试官的高频问题,「一个人的业余时间决定了他的未来」,如果回答周末都在追剧打游戏之类的,未免显得太不上进。

一般来说,推荐下面的回答:

周末一般会有三种状态:

  1. 和朋友一起去做做运动,也会聚会聊天,探讨下新技术之类的;
  2. 也会看一些书籍充充电,比如我最近看的 xx,有什么的想法;
  3. 有时候会闷在家用最近比较火的技术做个小项目或者实现个小功能之类的。

这样的回答,既能表现自己阳光善于社交沟通的一面,又能表现自己的上进心。

面试注意事项

提问环节

面试是一个双向选择的事情,所以面试后一般会有提问环节。在提问环节,候选人最好不要什么都不问,更不要只问薪水待遇、是否加班之类的问题。

其实这个时候可以反问面试官了解团队情况、团队做的业务、本职位具体做的工作、工作的规划,甚至一些数据(可能有些问题不会直面回答)。

还可以问一些关于公司培训机会和晋升机会之类的问题。如果是一些高端职位,则可以问一下:自己的 leader 想把这个职位安排给什么样的人,希望多久的时间内可以达到怎样的水平。

面试后的总结和思考

面试完了多总结自己哪里做得不好,哪里做得好,都记录下来,后续扬长避短

  • 24
    点赞
  • 169
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨说前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值