前端面试攻略

549 阅读15分钟

全局作用域和函数作用域

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

猜猜输出啥,undefined,因为函数作用域内发生了变量提升,函数内的代码相当于
var a; console.log(a);

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

猜猜输出啥,a=2,因为变量提升是直接到函数作用域顶部,在输出a之前,a已经赋值2,且函数作用域内a是局部变量

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

输出 1,因为a是传参进来的,相当于在函数作用域内a已经声明和赋值了 另外:

    (function () {
        a=5;
        console.log(window.a) // undefined,发生了变量提升
        var a = 10;
    })() // 这个自执行保证了 `var a` 发生变量提升时,不会污染到全局window.a

函数的声明优先级低于变量:

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

function sum() {
  console.log(2);
}

sum() // 1

变量提升

函数声明提升会被后面的覆盖

foo(); // 3
function foo() {
    console.log( 1 );
}
function foo() {
    console.log( 3 );
}

if-else语句里面的函数声明都有效

foo(); // "b"
var a = true;
if (a) {
    function foo() { console.log("a"); }
} else {
    function foo() { console.log("b"); }
}

引用传递和值传递

函数调用中,传递是一个数值,我们称为 “值传递”。 函数调用中,传递是对象,一般称为 “引用传递”。引用类型:Object Array Fucntion;对于传参而言,传入的东西是不变的。

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

这里会输出1,a的值没有改变

   var a = [1, 2, 3];
   function changeA(a) {
       a[0] = 2;
   }
   changeA(a);
   console.log(a);

这里输出[2, 2, 3],传入的是指针,指针没有改变,而值发生了变化
再看看下面的例子,如果数组以...arr的方式传递,会发生什么

   var a = [1, 2, 3];
   function changeA(...a) {
       a[0] = 2;
   }
   changeA(a);
   console.log(a); // [1, 2, 3]

严格相等(===)的特殊值

引用类型全等于永远返回false,因为变量存储的是地址值,比如new Object

NaN === NaN                // false
+0 === -0                  // true
+0 === 0                   // true
-0 === 0                   // true
+Infinity === Infinity     // true
+Infinity === -Infinity    // false
-Infinity === Infinity     // false
null === undefined         // false
[] === []                  // false
{} === {}                  // false

隐式类型转换

Javascript是弱类型语言,之所以不同的数据类型之间可以做运算,是因为JavaScript引擎在运算之前会悄悄的把他们进行了隐式类型转换的。
以下假设为比较 x == y的情况,Type(x)指的是x的数据类型,Type(y)指的是y的类型,最终返回值只有true或false。

  1. Type(x)与Type(y)相同时,进行严格相等比较
  2. x是undefined,而y是null时,返回true
  3. x是null,而y是undefined时,返回true
  4. Type(x)是Number而Type(y)是String时,进行x == ToNumber(y)比较
  5. Type(x)是String而Type(y)是Number时,进行ToNumber(x) == y比较
  6. Type(x)是Boolean时,进行ToNumber(x) == y
  7. Type(y)是Boolean时,进行x == ToNumber(y)
  8. Type(x)是Number或String其中一种,而Type(y)是个Object时,进行x == ToPrimitive(y)比较
  9. Type(x)是个Object,而Type(y)是Number或String其中一种时,进行ToPrimitive(x) == y比较
  10. 其他情况,返回false

参考教程

其实第一次读的时候,还是有点理解不了。还是系统的剖析一下吧,简单点来说,比较x == y这个表达式,当 xy 相同,直接进行值比较。当 xy 不同,最终一定会把 xy 转换为要么number、要么string再做值比较。

xy 都是 基础类型 时,会转化为相同的基础类型再比较,boolean类型会转化为Number类型再做比较,当有一方是Number时,最终会转化为Number类型再比较,Number类型的优先级是最高的,所以需要熟悉一下Number(x)这个函数,Number(x)+x效果是一样的,枚举一下可能遇见的情况吧:

  • Number('1.5') //1.5
  • Number('1,2') //NaN
  • Number({}) //NaN
  • Number([]) //0
  • Number([2]) //2
  • Number(true) //1
  • Number(null) //0
  • Number(undefined) //NaN

xy 两个都是引用类型,比如数组、对象、Function,会直接返回false,因为 xy 储存的是地址。

xy 一方是引用类型时,会进行 ToPrimitive(x || y) 的转化。如果 x.valueOf 能返回基础数据类型,则会优先调用 valueOf 方法,如果不能返回基础数据类型,则会调用 toString 方法。toString 一定会返回基础数据类型。

找几个例子说说:

true == true
Number(true) == true => 1 == Number(true) => 1 == 1
左值 true 先转化为数字,右值 true 再转化为数字,然后进行值比较
答案:true


true == '123'
Number(true) == '123' => 1 == Number('123') => 1 == 123
true先转化为数字,再把'123'转化为数字123,再比较左值和右值
答案:false


'123' == 2
Number('123') == 2 => 123 == 2
'123'先转化为数字,再比较左值和右值
答案:false


[1,2] == 2
toToPrimitive([1,2]) == 2 => '1,2' == 2 => Number('1,2') == 2 => NaN == 2
只解析一项,NaN也是Number类型的一种,NaN与任何数字比较,返回false
答案:false

数组类型

[1,2,3].valueOf() //[1,2,3] 不是基础数据类型  
[1,2,3].toString() //1,2,3,数组类型toPrimitive会调用这个方法

对象类型

var c = {a:1}; //必须赋值,{a:1}.valueOf()会报错
c.valueOf() //{a:1}不是基础数据类型
c.toString() //[object Object],toPrimitive会调用这个方法
c == '[object Object]' //true

解释一下为啥 c == '[object Object]' 返回true

toPrimitive(c) == '[object Object]' => '[object Object]' == '[object Object]'

函数类型

function b() { return 2;}
b.valueOf() //返回函数本身,即function b() { return 2;}  
b.toString() //返回字符串,即'function b() { return 2;}',toPrimitive采用这个

b == 'function b() { return 2;}' //true

状态码

不介绍常见的状态码,主要针对1XX到5XX能加分的详细说明
100 - Continue 初始的请求已经接受,客户应当继续发送请求的其余部分

  • 用于客户端在发送 post 数据给服务器时,看服务器是否处理 post 的数据,如果不处理,客户端则不上传 post 是数据,反之则上传。在实际应用中,通过 post 上传大数据时,才会使用到 100-continue 协议
  • 如果客户端有 post 数据要上传,可以考虑使用 100-continue 协议。在请求头中加入 {“Expect”:”100-continue”}
  • 如果在发送 100-continue 前收到了 post 数据(客户端提前发送 post 数据),则不发送 100 响应码

101 - 服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求

  • 比如请求使用了webSocket协议

202 - 接受和处理、但处理未完成

204 - 服务器成功处理了请求,但没有返回任何内容

206 - 服务器已经完成了部分用户的GET请求

  • 在请求mp4文件的时候会返回这个,原因是这个东西会一部分一部分返回

303 - 临时重定向,和302状态码有着相同的功能,但是303明确表示客户端应当采用get方式请求资源

307 - 临时重定向,和302状态码有着相同的功能,当301、302、303响应状态码返回时,几乎所有浏览器都会把post改成get,并删除请求报文内的主体,之后请求会自动再次发送。307会遵照浏览器标准,不会从post变为get。但是对于处理响应时的行为,各种浏览器有可能出现不同的情况。

400 - 语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求

  • 比如应该用https协议
  • 比如上传的json数据语法错误

401 - 当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息,浏览器据此显示用户名字/密码对话框,然后在填写合适的Authorization头后再次发出请求

403 - Forbidden,服务器已经理解请求,但是拒绝执行它

405 - 请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表

410 - Gone,被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的

414 - Request-URI Too Long,请求的URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务

415 - Unsupported Media Type,对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝

421 - too many connections,从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)

500 - Internal Server Error,作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应

502 - Bad Gateway,作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应

503 - Service Unavailable,由于临时的服务器维护或者过载,服务器当前无法处理请求

504 - Gateway Timeout,作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应

DNS解析

一个域名的DNS记录会在本地有两种缓存:浏览器缓存和操作系统(OS)缓存,会优先访问浏览器缓存,如果未命中则访问OS缓存,最后再访问DNS服务器(一般是ISP提供),然后DNS服务器会递归式的查找域名记录,然后返回。
DNS记录会有一个ttl值(time to live),单位是秒,意思是这个记录最大有效期是多少。经过实验,OS缓存会参考ttl值,但是不完全等于ttl值,而浏览器DNS缓存的时间跟ttl值无关,每种浏览器都使用一个固定值。 修改hosts文件之后,为啥有时会立刻生效,有时却一直不生效呢?在修改hosts文件后,所有OS中DNS缓存会被清空,而浏览器缓存则不发生变化,在 chrome://net-internals/#dns 中点击 Clear Host Cache 会清空OS缓存。如果发现DNS更改不成功,可以静待几十秒。

查找浏览器缓存的DNS服务器

浏览器在获取网站域名的实际IP地址后会对其IP进行缓存,减少网络请求的损耗。每种浏览器都有一个固定的DNS缓存时间,其中Chrome的过期时间是1分钟,在这个期限内不会重新请求DNS。chrome://net-internals/#dns

OS DNS缓存

OS缓存会参考DNS服务器响应的TTL值,但是不完全等于TTL值

ISP DNS服务器

先检查一下自己的缓存中有没有这个地址,有的话就直接返回,没有的话就去根域找,从根域开始递归查询IP

根域递归

根域的地址是写死在ISP DNS服务器上的,根域即是/;比如www.a.com这样的域名,先去根域找.com的服务器对应IP,然后.com的服务器对应IP找到a.com的服务器IP...

正则

先通关正则,打好基础,否则会看不明白,戳我。在正则优化的情况下,使用new RegExp会比较快。原因是new RegExp会在代码执行的过程中编译正则,编译就是在内存中开辟一个空间存放变量或函数,字面量有个废弃的compile方法也可以做到这个事情。

腾讯QQ

腾讯QQ号从10000开始,最少5位

[1-9][0-9]{4,}

匹配前后空格

\s表示匹配一个空白符,包括空格、制表符、换页符、换行符和其他 Unicode 空格,具体点就是[ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004​ \u2005\u2006​\u2007\u2008​\u2009\u200a​\u2028\u2029​​\u202f\u205f​ \u3000

/(^\s+)|(\s+$)/g

数字分隔

将阿拉伯数字每三位一逗号分隔,如:15000000转化为15,000,000

'1500000000000'.replace(/\B(?=(\d{3})+$)/g,',')

匹配中文字符

[\u4e00-\u9fa5]

匹配一个域名

DNS规定,域名中的标号都由英文字母和数字组成,每一个标号不超过63个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过255个字符。

/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/

查找英文句子中所有的单词

分析:第一,不区分大小写;第二,带'的也算在里面,比如don't;第三,带-的也算在里面,比如x-ray

var str = "When you are old and grey and full of sleep, don't make me think. And hid his face amid a crowd of stars.";

str.match(/[a-z]+([-'][a-z]+)?/ig);

获取url上某个参数名为name的正则

分析:参数名=值,不同参数间用&分隔开

var name= 'rqlang';
    reg = new RegExp('[&?]'+name+'=([^& ]+)')
    str = 'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E6%AD%A3%E5%88%99%E8%8E%B7%E5%8F%96%E6%9F%90%E4%B8%AA%E5%8F%82%E6%95%B0&oq=%25E6%25AD%25A3%25E5%2588%2599%25E8%258E%25B7%25E5%258F%2596%25E6%259F%2590%25E4%25B8%25AA%25E5%258F%2582%25E6%2595%25B0&rsv_pq=baac736e00031a97&rsv_t=515dwC%2Bf5ZRACPl1sr7KbYkxhUwe9G1VxfTPLWRBaQ9vh4Oa8jc6nfh0pQg&rqlang=cn&rsv_enter=1&inputT=2550&rsv_sug3=107&rsv_sug1=103&rsv_sug7=100&rsv_sug2=0&rsv_sug4=3469'
    
val = str.match(reg)[1];

升级一下,获取url中所有的参数名

var propReg = /([?&][^=]+)/g,
    str = location.search,
    arr = str.match(propReg);

arr.forEach(function(val, index, arr) {
    arr[index] = arr[index].replace(/^(&|\?)/, '');
});  

console.log(arr); // 所有的参数名

再升级一下,获取url中所有参数对键值,以对象的方式展现

var reg = /([&\?][^=]+)=([^& ]+)/g,
    str = location.search,
    arr = str.match(reg);
    
    arr.reduce(function(obj, val, index, arr) { 
        var reg = /[&?]([^=]+)=([^& ]+)/, 
            tmpArr = val.match(reg); 
        
        obj[tmpArr[1]] = tmpArr[2]; 
        return obj;
    }, {});

常用的函数

获取cookie值

cookie的特征,以';'为分隔,末字段也许不带';',prop值前面匹配一个或0个空格

function getCookie(name) {
    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");

    if(arr=document.cookie.match(reg))
        return unescape(arr[2]);
    else
        return null;
}

手写一个ajax请求

readyState各种值,记不住没关系,记住这几个状态都是围绕着XMLHttpRequest对象本身,且最后一个状态是4就可以了:

  • 0,未初始化,(XMLHttpRequest)对象已经创建,但还没有调用open()方法
  • 1,载入/正在发送请求,即调用open方法
  • 2,载入完成/数据接收,即send()方法执行完成
  • 3,loading,交互/解析数据,此阶段解析接收到的服务器端响应数据
  • 4,响应内容解析完成,可以在客户端调用了

var xmlHttp = new XMLHttpRequest();

xmlHttp.onreadystatechange = function () {
  // readyState
  if (this.readyState == 4 && this.status == 200) {
    console.log(1);
  } 
}
xmlHttp.open('GET', 'https://www.afea.com', false);
xmlHttp.setRequestHeader('X-Requested-With');
xmlHttp.withCredentials = true; // 跨域请求携带cookie
xmlHttp.send(null);

简单实现双向数据绑定

var data = {};

object.defineProperty(data, 'text', {
   set(value) {
       $input.value = value;
       this.value = value;
   } 
});

$input.onChange = function () {
    data.text = e.target.value; 
}

实现一个累加器

形如sum(1,2)(3)(4,5)的样子:

sum(1,2)         // 返回3
sum(3)(1,2)      // 返回6
sum(1,3)(5)(2)   // 返回11

思路是利用闭包,改写toString方法。

function add () {
  var total = 0;
  var args1 = [...arguments];

  var sum = function (...args) {
    total = args1.concat(args).reduce((total, a) => total += a, total)
    return sum;
  }

  sum.toString = function() { // sum方法调用,return必然会调用toString方法
    return total;
  }

  return sum;
}

重写bind功能

bind原是Function.prototype上的方法,可以修改函数的this指向

Function.prototype.bind = function (context) {
    // this指向调用函数
    if (typeof this !== 'function') {
        throw new TypeError('not a function');
    }
    var args = Array.prototype.slice.call(arguments, 1);
  
    return function () {
        return this.apply(context, args.concat(Array.prototype.slice.call(arguments)))
    }
}

实现一个对象或者数组的深拷贝

浅拷贝只复制指向某个对象的引用地址,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。

function deepClone(target) {
    if (typeof target !== 'object' ) return target;
    
    var arr_tmp = target instanceof Array ? [] : {};
  
    for (var i in target) {
        arr_tmp[i] = typeof target[i] !== 'object' ? target[i] : deepClone(target[i]);
    }
  
    return arr_tmp;
}

实现数组的扁平化

递归实现,如果当前元素是数组,则继续递归,否则加入最终返回的扁平化数组

const flattern = (arr) => arr.reduce((a, item) => Array.isArray(item) ? a.concat(flattern(item)) : a.concat(item), [])

循环利用队列

const flattern  = (arr) => {
  var finalArray = [];

  if (!Array.isArray(arr)) return;

  while(arr.length) {
    var target = arr.shift();

    if (Array.isArray(target)) {
      arr = target.concat(arr);
    } else {
      finalArray.push(target)
    }
  }
  return finalArray;
}

防抖(debounce)

防抖主要是为了限制函数的执行频次,以优化函数执行频次过高导致的响应速度跟不上触发频率的问题。如果倒计时没有结束,则清空倒计时,再重新计时。有个弊端,如果事件不断循环触发,并且小于等待时间,则不可能执行回调事件,所以后来又催生了节流。

function debounce(fn, wait, immediate) {
    var timer;
  
    return function() {
        var that = this, args = arguments;
        
        if (immediate) {
            fn.apply(that, args);
            immediate = false;
        }
        
        clearTimeout(timer);
        
        timer = setTimeout(function() {
            fn.apply(that, args);
        }, wait);
    }
}

节流(Throttle)

节流主要是为了限制函数的执行频次,以优化函数执行频次过高导致的响应速度跟不上触发频率的问题。要记住这个概念,可以联想水龙头滴水,水的一定量的,会一滴一滴的流出去,但是不一定会流光,时间间隔是一定的。直接上代码:

function throttle(fn, wait, immediate) {
    var timer, previous = new Date().getTime();
  
    return function() {
        var that = this, args = arguments, now = new Date().getTime();
        
        if (immediate) {
            fn.apply(that, args);
            immediate = false;
        }
        
        if (wait >= now-previous) { // 到一定时间一定执行一次
            previous = now;
            fn.apply(that, args);
            clearTimeout(timer);
        } else {
            timer = setTimeout(function() {
                previous = new Date().getTime();
                fn.apply(that, args);
            }, wait);
        }
    }
}

异步调用链

要求:写一个对象,实现调用a.work().sleep(20).lunch(),会输出

work
sleep 20 seconds
(wait 20 seconds)
lunch

用队列实现链式调用,当调用链很长的时候递归,其实是会报栈溢出的,最好在run里加个setTimeout 0

let a = {
    query: [],
    status: false, 
    run: function () { // 关键点,递归执行函数
        setTimeout(() => {
            if (this.status) return;    // 如果队列还在运行中,则返回
    
            if (this.query.length > 0) {
                this.query.shift()(this.run.bind(this))
            }
        }, 0)
    },
    work: function () {
        this.query.push((fn) => {
            console.log('work');
            fn();
        });
        this.run();
        return this;
    },
    lunch: function () {
        this.query.push((fn) => {
            console.log('lunch');
            fn();
        });
        this.run();
        return this;
    },
    sleep: function (time) {
        this.query.push((fn) => {
        this.status = true;  // 只有异步会阻塞队列执行而已,所以status的更新放在这里
            console.log(`sleep ${time} seconds`);
            setTimeout(() => {
                this.status = false;
                fn();
            }, time * 1000);
        });
        this.run();
        return this;
    }
}

数据结构和算法

二分查找

假设有一个有序数组,需要查找一个数值为3的元素,如果存在,返回第一个元素的下标,否则返回-1。

function binarySearch(arr, target) {
    var low = 0, high = arr.length-1, mid;
  
    while(low < high) {
        mid = ~~((low + high)/2);
    
        if(arr[mid] < target) {
            low = mid+1;
        } else {
            high = mid;
        }
    }
  
    if (arr[low] === target) return low;
        else return -1;
}

插入排序

类似整理扑克牌,将每一张牌插到其他已经有序的牌中适当的位置。冒泡和选择就不用说了,一个正方向一个反方向,两个for循环搞定的。

for (var i = 1; i < arr.length; i++) {
    for(var j=i;j > 0 && arr[j] < arr[j-1]; j--;) {
       [arr[j], arr[j-1]] = [arr[j-1], arr[j]]
    }
}

希尔排序

原理戳这,插入排序的晋级版,以gap为界限分为一组,每一组进行插入排序计算,一开始时,一般来说gap=length/2,所以稳定的复杂度为nlogn

function heerSort(arr) {
    var gap = ~~(arr.length/2); // 取整竟然比/优先级高,只能用括号补了
  
    for (var i=gap;i>0;i=~~(i/2))
        for (var j=i;j<arr.length;j+=i)
            for(var k=j;k>=i&&arr[k]<arr[k-i];k-=i)
                [arr[k],arr[k-i]]=[arr[k-i],arr[k]];
}

归并排序

原理戳这,是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

function mergeSort(arr, low, high, temp) {
    if (low < high) {
        var mid = parseInt((low+high)/2);
        
        mergeSort(arr, low, mid, temp);
        mergeSort(arr, mid+1, high, temp);
        merge(arr, low, high, temp);
    }
}

function merge(arr, low, high, temp) {
    var mid = parseInt((low+high)/2),
        i   = low,
        j   = mid+1,
        k   = 0;
  
    while (i<=mid&&j<=high) {
        if (a[i] < a[j]) {
            temp[k++] = a[i++];
        } else {
            temp[k++] = a[j++];
        }
    }
    
    while(i<=mid) {
        temp[k++] = a[i++];
    }
  
    while(j<=high) {
        temp[k++] = a[j++]
    }
  
    for (var i=0;i<k;i++) {
        a[low+i] = temp[i];
    }
}

快速排序

简单点来说,就是以一个数为基准(通常是最左边的数),把这个序列小于这个数的数放在这个数的左边,如果大于这个数,则放在右边。平均性能O(nlogn),最坏性能是O(n2)相当于插入排序,在正序和逆序的时候出现,递归划分为有一边为0个

function quickSort(arr, low, high) {
    var i = low,
        j = high,
        temp = arr[i];
  
    if(i>=j) return;
  
    while(i<j) {
        while(i<j&&arr[j]>=temp)
            j--;
            
        if(i<j)
            arr[i]=arr[j];

        while(i<j&&arr[i]<=temp)
            i++;
        if(i<j)
            arr[j]=arr[i];
    }
    
    arr[i]=temp;
  
    quickSort(arr, low,i-1);
    quickSort(arr, i+1, high);
}