常考的前端基础面试题(笔记)_京程无967ccc,架构师花费近一年时间整理出来的前端核心知识

36 阅读40分钟

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

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

让人更易读懂

对于人来说,代码可读性、语义化就是一个非常广泛的概念了,例如定义 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==truefalse
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;` ba 都指向同一个地址。
- `.`的优先级高于`=`。所以先执行`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 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。

最后

一个好的心态和一个坚持的心很重要,很多冲着高薪的人想学习前端,但是能学到最后的没有几个,遇到困难就放弃了,这种人到处都是,就是因为有的东西难,所以他的回报才很大,我们评判一个前端开发者是什么水平,就是他解决问题的能力有多强。

分享一些前端面试题以及学习路线给大家

开源分享:docs.qq.com/doc/DSmRnRG…