前端面试题整理(一)

196 阅读7分钟

javascript

数据类型

数据类型

  • 基本类型: number / string / boolean / undefined / null / symbol
  • 引用类型: object / array / function

数据类型检测

typeof

  • string number boolean object undefined function symbol
  • 区分不了null object array

instanceof

  • instanceof 是通过原型链来判断数据类型的。
  • 如: [] instanceof Array
  • 再如: let str = new String('test'); str instanceof String

Object.prototype.toString.call()

  • Object原型链上的toString方法。
  • 返回[object type]
  • [object Number / String / Object / Array / Undefined / Null / Boolean]

深拷贝和浅拷贝

浅拷贝和深拷贝只是针对引用类型的,如Object/ array /function。

浅拷贝只是复制了一层对象属性,而深拷贝则复制了对象的所有属性层级。改变初始值的引用类型值,浅拷贝的值会随着更改,深拷贝到值也不会被改变。

浅拷贝

  • Object.assign(obj)

深拷贝

  • JSON.parse(JSON.stringfy(obj)) => 对象上的函数属性不会被拷贝到新对象上。
  • { ...obj } => 可以拷贝所有的基本类型和引用类型。

js实现一个深拷贝

function clone(source){
    // 如果是源对象是null, 则返回null
    if( !typeof source === 'object'|| source === null) return source;
    // 非引用类型的源数据,如字符串和数字、布尔值,直接返回。
    if( typeof source !== 'object' ) return source;
    // 判断源数据是对象还是数组,然后区分赋值。 Array.isArray
    let target = Array.isArray(source) ? [] : {};
    // 获取对象所有的属性名或者数组的下标
    for (const key in source) {
        // 判断是不是私有属性
        if (source.hasOwnProperty(key)) {
            const el = source[key];
            // 如果值是引用类型,则继续重新遍历
            if( typeof el === 'object'){
                target[key] = clone(source[key]);
            }else{
                // 否则,则赋值到新对象中
                target[key] = source[key];
            }
        }
    };
    return target;
}

作用域

变量声明提升

  • var 声明的变量和function会被提升到js代码的顶部。
  • 只是提升变量,赋值并不会提升,在其声明之前打印的话,会打印出undefined。
  • const 和 let 声明的变量和常量,则没有变量声明提升的问题,在声明之前使用,会报错。
  • 函数声明的优先级高于变量声明,如果存在同名的函数和变量,在变量未赋值的情况下,函数会覆盖变量。

作用域链

  • 因为函数的嵌套,而出现了作用域的层级关系,在函数执行时候,所使用到的变量如果在当前函数作用域内没有找到,则会向其上一级函数的作用域内寻找,如果还没有,则继续向上,直到找到全局作用域window为止,这就是作用域链。

闭包

  • 闭包就是将函数内部和函数外部链接起来,可以在函数外部使用函数内部的变量。
  • 同时让这些变量一直保存在内存中。
  • 函数内部抛出一个函数,抛出的函数使用着函数内部的变量。在外部接收抛出的函数,函数内部声明的变量不会被释放,会一直存在内存中。
function outer(){
    let num = 100;
    return function inner(){
        console.log( num ++ );
    }
};

let func = outer();

func();
func();

原型和继承

js创建对象的几种方式

  • let obj = {};
  • let obj = Object.create({a: 1});
  • let obj = new Object();

js如何实现一个类

  • 构造函数

    // 会使用到prototype,书写比较麻烦
    function Person(name, age){
        this.name = name;
        this.age = age;
        this.showName = () => {
            console.log( this.name );
        }
    };
    Person.prototype.showInfo = function(){
        return this.name + ' --- ' + this.age
    }
    
  • Class(es6语法糖)

    class Person{
        constructor(name, age){
            this.name = name;
            this.age = age;
            this.showName = () => {
                console.log( name );
            }
        };
        showInfo(){
            return this.name + ' --- ' + this.age
        }
    }
    

原型链

  • 当我们使用对象上的一个属性的时候,会现在对象自身寻找这个属性,如果找不到,则会沿着对象的隐式原型__proto__逐层向上寻找,直到找到null为止。这条由__proto__形成的链子,就是原型链。
  • prototype __proto__ constructor new

继承

js如何实现继承

  • Object.create(obj, {})
  • {}.__proto__ = prototype
  • new Constructor | class
  • Object.setPrototypeOf(obj, prototype)

new和this

new操作符做了什么

  1. 创建一个实例对象{}
  2. 将this指向该实例对象,同时该实例对象继承了构造函数的原型
  3. 然后属性和方法都添加到了this引用的对象中。
  4. 新创建的对象由this所引用,最后隐式的返回this.

new的模拟实现

this对象的理解

  • this总是指向函数的直接调用者
  • 在普通函数中,this指向window。
  • 在箭头函数中,this指向父级作用域。
  • 对于构造函数,this指向调用的实例。

call、apply和bind

  • 都是改变函数的this指向问题。
  • call(this, p1, p2, ... ),会立刻执行函数,可以传递多个参数。
  • bind(this, p1, p2, ... ), 不会立刻执行函数,可以传递多个参数。
  • apply(this, [p1, p2, ...]),传递的参数是一个数组格式

数据处理

数据去重

let arr = [1,2,3,4,5,6,7,2,3,4,7,8,9,7];
// es6  Set & Array.from()
let set = new Set(arr);
Array.from(set);
// map & includes & push
function counterSingle (list) {
    let _list = [];
    list.map( i => !_list.includes(i) && _list.push( i ) );
    return _list;
}
counterSingle(arr);

数据排序

let arr = [1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 7, 8, 9, 7];
// api
arr.sort( (a, b) =>  a -b );
// 重写
function arraySort(arr){
    for( var i = 0; i < arr.length; i ++ ){
        for( var j = i + 1; j < arr.length; j ++ ){
            if( arr[i] > arr[j] ){
                let temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    };
    return arr;
};
arraySort(arr);

递归求和

let arr = [1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 7, 8, 9, 7];
let number = 100;
let num = arr.reduce((acc, cur) => acc + cur );
console.log( num );

let totalNumber = 0;
for( var i = 0; i <= number;  i++ ){
    totalNumber += i;
};
console.log( totalNumber );

递归求积

function multiple (number) {
    return number * (number === 1 ? 1 : multiple( number - 1 ));
}
console.log( multiple(5));

计算数组各项的重复次数

let arr = [1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 7, 8, 9, 7];
// 计算重复次数
let obj = {};
for (const value of arr) {
    obj[value] ? obj[value] ++ : obj[value] = 1;
};
console.log( obj ); 

斐波那契数列

function fbnq(len){
    let list = [ 1, 1 ];
    for( var i = 1; i < len - 1; i ++){
        list.push( list[i] + list[ i - 1 ] );
    };
    return list;
};

fbnq(10);

数组最大差值

// Math
maxDiffer = () => {
    let list = [20, 40, 123, 123, 34, 45, 454, 30];
    let max = Math.max( ...list );
    let min = Math.min( ...list );
    return max - min;
}
// 手写
maxDiffer = () => {
    let list = [20, 40, 123, 123, 34, 45, 454, 30];
    let max = list[0];
    let min = list[0];
    for( var i = 1; i < list.length; i ++ ){
        if( list[i] > max ){
            max = list[i];
        }
        if( list[i] < min ){
            min = list[i];
        }
    }
    return max - min;
}

console.log( maxDiffer() )

Event Loop

堆、栈、队列

  • 堆: 数据结构,是利用完全二叉树维护的一组数据。
  • 栈:是一种线性表,遵循着先进后出的原则,从尾部插入和执行。
  • 队列:遵循着先进先出的原则,从表的尾部插入,头部删除。
  • js执行栈: js是单线程语言,有一个主线程(main thread)和执行栈(call-stack),所有的主线程任务都会被放到执行站中等待主线程执行。

宏任务和微任务

  1. 宏任务(macro-task):当前调用栈中执行的全部任务被称为宏任务,包括script全部的代码、settTimeout、setInterval等。
  2. 微任务(micro-task):当前(此次事件循环中)宏任务执行完成后,下一次要被执行的任务称为微任务,包括promise、async/await等。
  3. 不同类型的任务会进入相对于的事件队列(Event Queue),宏任务会被放到回调队列(消息队列 callback queue)中,由事件触发线程维护;微任务会被放到微任务队列中,由js引擎线程维护。

一句话解析什么是event loop

  • js是单线程,其多线程和异步就是靠event loop(事件轮询)来实现的。
  • js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中。
  • 当程序调用外部的API(如ajax, setTimeOut)时,会将该异步任务挂起,放到消息队列中,继续执行执行栈中的任务。遇到promise、async/await的时候,会放到微任务队列中。
  • 主线程执行完之后,会先看微任务队列中是否有任务,有的话会立即执行,在执行过程中遇到的微任务,也会同步执行。
  • 执行完微任务队列,或者微任务队列未空时,则会执行消息任务中的任务。如在执行的时候遇到异步任务,则会继续放到消息队列中。
  • 方法和程序在执行完之后,该方法和程序会从执行站中被弹出。
  • 主线程每次将执行站清空之后,都会去事件队列中查看是都有任务需要执行,如果有,就取出一个放到执行栈中执行,这个循环的过程就是Event Loop(事件循环)。

浏览器页面渲染过程

  1. 浏览器解析html代码,创建一个DOM树。并行请求css、js、img。
  2. 浏览器解析css代码,计算出最终的样式数据,构建出cssDOM树(rendering tree)。
  3. css优先级: html中的css > 内联样式 > 外部样式 > 用户设置 > 浏览器默认样式
  4. 等到渲染树创建好之后,浏览器会根据渲染树的结果将页面绘制在屏幕上。
  5. css和js往往会多次修改dom和cssdom。
  • dom树和渲染树的区别: 渲染树有样式,例如display:none的节点就会从渲染树上移除。

浏览器缓存

浏览器缓存的认识

  1. 强缓存
    • 主要是利用Expires 和 Cache-Control这两个字段,在有效期内,会直接读取本地的缓存文件,而不会向浏览器发送请求。
    • Expires 是一个时间点,依赖的是客户端的时间,在到达这个时间店之后,缓存资源会过期,要向服务器发送请求。
    • Cache-Control 是一个相对时间,可以设置max-age,在多少秒之后过期。其优先级也比Expires更高。
  2. 协商缓存
    • 由服务器来确定缓存资源是否可用,需要服务器和客户端一起配合使用。
    • 服务器会在response header中返回last-modified 以及 ETag 字段。
    • last-modified 表示资源在服务器中最后一次的修改时间,再次请求该资源的时候,请求头会带上 if-Modified-Since,询问该资源是否有被更新。
    • 每次文件修改后服务端那边会生成一个新的 ETag ,当再次请求该资源时候,浏览器的request header中会带上If-None-Match ,这值就是之前返回的ETag ,询问该资源 ETag 是否变动,有变动的话,则需要更新资源。

常用的前端缓存

  1. cookie

    • 存储在客户端,有大小限制,4kb左右。
    • 可以设置过期事件,默认未浏览器关闭的时间。
  2. Session

    • 存储在服务器,较为安全。
  3. SessionStorage

    • 客户端本地存储,浏览器关闭后就会被清除。大小为5MB
  4. LocalStorage

    • 客户端本地存储,不手动清除的话,会一直保存在浏览器中。

css

盒模型

  • 盒模型由内到外分别是: content 、 padding、 border、 margin
  • 在ie中, width = content + padding + border
  • 在标准的盒子模型中: width = content
  • box-sizing : border-box | content-box(默认)

居中

水平居中

  • 行内元素: text-align: center;
  • 块状元素: margin:0 auto;
  • position: relative; left: 50%; transform: translateX( -50% )
  • display: flex; justify-content: center

垂直居中

  • position: relative; top: 50%; transform: translateY(-50%);
  • display: flex; align-items: center;
  • height: 100px; line-height: 100px;