面试题

Mr.ZhaoAbout 14 min

1. var 和 let、const 的区别

  • var 是 ES6 之前的语法,let、const 是 ES6 语法
  • var 和 let 定义的是变量,可修改;const 定义的是常量,不可修改
  • var 有变量提升,let、const 没有
  • var 没有块级作用域,let、const 有
// var 变量提升
console.log('a', a); // a undefined
var a = 100;

// let 没有变量提升
console.log('b', b); // 报错
let b = 200;
// var 没有块级作用域
for (var i = 0; i < 10; i++) {
    var j = i + 1;
}
console.log(i, j); // 10

// let 有块级作用域
for (let i = 0; i < 10; i++) {
    let j = i + 1;
}
console.log(i, j); // 报错

2. typeof 可以判断哪些类型?

  • undefined、string、number、boolean、symbol
  • object(typeof null === 'object'
  • function
// 判断所有值类型
let a;
console.log(typeof a); // 'undefined'
const str = 'abc';
console.log(typeof str); // 'string'
const n = 100;
console.log(typeof n); // 'number'
const b = true;
console.log(typeof b); // 'boolean'
const s = Symbol('s');
console.log(typeof s); // 'symbol'

// object 类型
let o = {
    a: 100,
    b: 200,
};
console.log(typeof o); // object
console.log(typeof null); // object

// function 类型
let f = function () {
    console.log('你好!');
};
console.log(typeof f); // 'function'

3. 列举强制类型转换和隐式类型转换

  • 强制类型转换: parseIntparseFloattoString
  • 隐式类型转换: if==+ 拼接字符串

4. 手写深度比较

// 实现如下效果
const obj1 = {
    a: 10,
    b: {
        x: 100,
        y: 200,
    },
};
const obj2 = {
    a: 10,
    b: {
        x: 100,
        y: 200,
    },
};
isEqual(obj1, obj2) === true;

手写实现isEqual

// 判断是否是 object
function isObject(obj) {
    return typeof obj === 'object' && obj !== null;
}
// 全相等
function isEqual(obj1, obj2) {
    if (!isObject(obj1) || !isObject(obj2)) {
        // 值类型,不是对象或数组
        return obj1 === obj2;
    }
    if (obj1 === obj2) {
        // 两个引用类型全相等(同一个地址)
        return true;
    }
    // 两个都是引用类型,不全相等
    // 1. 先取出 obj1 obj2 的 keys,比较个数
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if (obj1Keys.length !== obj2Keys.length) {
        // keys 个数不相等,则不是全等
        return false;
    }
    // 2. 以 obj1 为基准,和 obj2 依次递归比较
    for (let key in obj1) {
        // 递归比较
        const res = isEqual(obj1[key], obj2[key]);
        if (!res) {
            // 遇到一个不相等的,则直接返回 false
            return false;
        }
    }
    // 3. 都相等,则返回 true
    return true;
}

5. split()和join()的区别

console.log('1-2-3'.split('-')); // [ '1', '2', '3' ]
console.log([1, 2, 3].join('-')); // 1-2-3

6. 数组的 push、pop、unshift、shift 方法的作用

push 向数组的末尾添加一个或更多元素,并返回新的长度

pop 删除数组的最后一个元素并返回删除的元素

unshift 向数组的开头添加一个或更多元素,并返回新的长度

shift 删除并返回数组的第一个元素

注意以下几点:

  • 函数作用是什么?
  • 返回值是什么?
  • 对原数组是否造成影响?
  • 如何对原数组不造成影响?concat slice map filter

数组 API 的纯函数和非纯函数:

纯函数:1. 不改变原来的数组; 2. 返回一个数组

  • concat,连接两个或更多的数组,并返回结果
  • map,通过指定函数处理数组的每个元素,并返回处理后的数组
  • filter,检测数值元素,并返回符合条件所有元素的数组
  • slice,选取数组的一部分,并返回一个新数组
const arr = [100, 200, 300];
const arr1 = arr.concat([400, 500]); // [ 100, 200, 300, 400, 500 ]
const arr2 = arr.map(num => num * 10); // [ 1000, 2000, 3000 ]
const arr3 = arr.filter(num => num > 100); // [ 200, 300 ]
const arr4 = arr.slice(-1); // [ 300 ]

非纯函数

情况1,改变了原数组:

  • push,向数组的末尾添加一个或更多元素,并返回新的长度
  • reverse,反转数组的元素顺序
  • sort,对数组的元素进行排序
  • splice,从数组中添加或删除元素

情况2,未返回数组:

  • push,向数组的末尾添加一个或更多元素,并返回新的长度
  • forEach,数组每个元素都执行一次回调函数
  • reduce,将数组元素计算为一个值(从左到右)
  • some,检测数组元素中是否有元素符合指定条件

7. 数组 slice 和 splice 的区别?

slice,选取数组的一部分,并返回一个新数组

splice,从数组中添加或删除元素

// slice()
const arr1 = [10, 20, 30, 40, 50];
const arr2 = arr1.slice(); // arr2 和 arr1 不是一个地址,纯函数,重要!!!

// arr.slice(start, end)
const arr1 = [10, 20, 30, 40, 50];
const arr2 = arr1.slice(1, 4); // [20, 30, 40]

// arr.slice(start)
const arr1 = [10, 20, 30, 40, 50];
const arr2 = arr1.slice(2); // [30, 40, 50]

// 负值
const arr1 = [10, 20, 30, 40, 50];
const arr2 = arr1.slice(-2); // [40, 50]
// arr.splice(index, howmany, item1, ....., itemX)
const arr1 = [10, 20, 30, 40, 50];
const arr2 = arr1.splice(1, 2, 'a', 'b', 'c'); // [20, 30]
// arr1 会被修改,不是纯函数,即有副作用

8. [10, 20, 30].map(parseInt) 的结果是什么?

console.log([10, 20, 30].map(parseInt)); 

结果为:

[ 10, NaN, NaN ]

解析:

// 拆解开就是
[10, 20, 30].map((num, index) => {
    return parseInt(num, index);
    // parseInt 第二个参数是进制
    // 如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
    // 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN
});
// 可以对比
[10, 20, 30].map((num, index) => {
    return parseInt(num, 10);
}); // 结果为:[ 10, 20, 30 ]

9. Ajax 请求中 get 和 post 的区别

  • get 一般用于查询操作,post 一般用于提交操作
  • get 的参数在 url 上,post 的参数在请求体内
  • 安全性:post 请求易于防止 CSRF

10. call 和 apply 的区别

call语法: 函数名.call(要改变的 this 指向,要给函数传递的参数1,要给函数传递的参数2, ...)

apply语法: 函数名.apply(要改变的 this 指向,[要给函数传递的参数1, 要给函数传递的参数2, ...])

11. 事件委托是什么?

详见:事件委托open in new window

12. 闭包是什么,有什么特性,对页面有什么影响

  • 一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
  • 闭包的应用场景:函数作为参数被传入,函数作为返回值被返回
  • 关键点:自由变量的查找,要在函数定义的地方,而不是执行的地方
  • 对页面的影响:变量内存得不到释放,可能会造成内存积累(不一定是泄露)
// 自由变量示例 —— 内存会被释放
// 自由变量是指在一个函数或代码块中引用了但未在该函数或代码块内部进行定义的变量
let a = 0;
function fn1() {
    let a1 = 100;
    function fn2() {
        let a2 = 200;
        function fn3() {
            let a3 = 300;
            return a + a1 + a2 + a3;
        }
        fn3();
    }
    fn2();
}
fn1();

// 闭包 函数作为返回值 —— 内存不会被释放
function create() {
    let a = 100;
    return function () {
        console.log(a);
    };
}
let fn = create();
let a = 200;
fn(); // 100

// 闭包 函数作为参数 —— 内存不会被释放
function print(fn) {
    let a = 200;
    fn();
}
let a = 100;
function fn() {
    console.log(a);
}
print(fn); // 100

13. 如何阻止事件冒泡和默认行为

event.stopPropagation()

event.preventDefault()

14. 添加、删除、替换、插入、移动 DOM 节点的方法

详见:操作节点open in new window

15. 怎样减少 DOM 操作?

  • 缓存 DOM 查询结果

    // 不缓存 DOM 查询结果
    for (let = 0; i < document.getElementsByTagName('p').length; i++) {
        // 每次循环,都会计算 length ,频繁进行 DOM 查询
    }
    
    // 缓存 DOM 查询结果
    const pList = document.getElementsByTagName('p');
    const length = pList.length;
    for (let i = 0; i < length; i++) {
        // 缓存 length ,只进行一次 DOM 查询
    }
    
  • 多次操作,合并到一次插入

    const listNode = document.getElementById('list');
    
    // 创建一个文档片段,此时还没有插入到 DOM 树中
    const frag = document.createDocumentFragment();
    
    // 执行插入
    for(let x = 0; x < 10; x++) {
        const li = document.createElement("li");
        li.innerHTML = `List item ${x}`;
        frag.appendChild(li);
    }
    
    // 都完成之后,再插入到 DOM 树中
    listNode.appendChild(frag);
    
    

16. 解释 JSONP 的原理,以及为什么不是真正的 Ajax

  • 浏览器的同源策略,什么是跨域?

  • 哪些 HTML 标签能绕过跨域

    • <img>可以做打点统计,因为统计方并不一定是同域的,除了能跨域之外,<img>几乎没有浏览器兼容问题,它是一个非常古老的标签
    • <script><link>可以使用CDN,CDN基本都是其他域的链接
    • <script>还可以实现JSONP,能获取其他域接口的信息
  • JSONP 原理:详见:JSONPopen in new window

17. document load 和 DOMContentLoaded 的区别

window.addEventListener('load', function () {
    // 页面的全部资源加载完才会执行,包括图片、视频等
});
document.addEventListener('DOMContentLoaded', function () {
    // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
});

18. == 和 === 的不同

  • == 会尝试进行类型转换
  • === 严格相等
  • 所有的地方都用 === 除了判断是 null 或者 undefined 时用 ==

19. 函数声明与函数表达式的区别?

const res = sum(10, 20);
console.log(res); // 30

// 函数声明
// 函数声明会进行变量提升,即可以在声明之前调用函数
function sum(x, y) {
    return x + y;
};
const res = sum(100, 200);
console.log(res); // 报错!!!

// 函数表达式
// 函数表达式不会进行变量提升,必须在定义之后才能调用函数
const sum = function(x, y) {
    return x + y;
};

20. new Object() 和 Object.create() 的区别

  1. 语法形式:
    • new Object() 是通过 new 关键字调用 Object 构造函数创建新对象的方式。例如:const obj = new Object();
    • Object.create() 是使用一个现有的对象作为新对象的原型来创建新对象的方式。例如:const obj = Object.create(proto);
  2. 原型链:
    • new Object() 创建的新对象将继承 Object.prototype 上的属性和方法,并且 obj.__proto__ 指向 Object.prototype
    • Object.create() 创建的新对象将继承传入的原型对象上的属性和方法,并且 obj.__proto__ 指向该原型对象
  3. 参数:
    • new Object() 不接受任何参数
    • Object.create() 接受一个参数作为新对象的原型,该参数可以为 null 或者一个对象。如果传入 null,则创建的新对象不会继承任何属性和方法

示例:

// 使用 new Object() 创建新对象
const obj1 = new Object();
obj1.name = 'obj1';
obj1.sayHello = function () {
    console.log(`Hello,I am ${this.name}`);
};
console.log(obj1); // {name:'obj1',sayHello: f()},obj1.__proto__ === Object.prototype

// 使用 Object.create() 创建新对象
const proto = { name: 'proto' };
const obj2 = Object.create(proto);
console.log(obj2); // {},obj2.__proto__ === proto
obj2.sayHello = function () {
    console.log(`Hello,I am ${this.name}`);
};
obj2.sayHello(); // Hello,I am proto

// 使用 Object.create() 创建新对象,并传入 null
const obj3 = Object.create(null);
console.log(obj3); // {}
console.log(obj3.__proto__); // undefined

21. 对作用域上下文和 this 的理解

const User = {
    count: 1,
    getCount: function () {
        return this.count;
    },
};
console.log(User.getCount()); // 1  this 指向 User
const func = User.getCount;
console.log(func()); // undefined  this 指向 window

22. 对作用域和自由变量的理解

let i;
for (i = 1; i <= 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 0);
}
// 4
// 4
// 4
  1. 声明变量 i 并初始化为 undefined
  2. 进入 for 循环,初始化 i1
  3. 调用 setTimeout 函数,将一个匿名函数作为参数传递给它。由于设置了延时时间为 0,该函数会被放入事件队列中,在当前任务执行完毕后被调度执行。
  4. for 循环继续,循环条件判断 i <= 3 为真,执行循环体内的代码。
  5. 打印输出 i 的值,此时 i4
  6. 增加 i 的值,i 变为 2
  7. 调用 setTimeout 函数,将另一个匿名函数作为参数传递给它。
  8. for 循环继续,打印输出 i 的值,此时 i4
  9. 增加 i 的值,i 变为 3
  10. 调用 setTimeout 函数,将另一个匿名函数作为参数传递给它。
  11. for 循环继续,打印输出 i 的值,此时 i4
  12. 增加 i 的值,i 变为 4
  13. for 循环结束,退出循环。

因此,最终的输出结果会是连续三次打印输出 4。这是因为 setTimeout 中的函数是在循环结束后才被调用,而此时 i 的值已经变为 4。因此,无论延时时间设置为多少,输出结果都会是 4

23. 以下代码,分别 alert 出什么?

let a = 100;
function test() {
    alert(a);
    a = 10;
    alert(a);
}
test();
alert(a);
100
10
10
  1. 声明变量 a 并赋值为 100
  2. 定义函数 test()
  3. test() 函数中,第一个 alert(a) 会弹出一个对话框显示变量 a 的值,即 100
  4. 将变量 a 的值更新为 10
  5. 第二个 alert(a) 会弹出一个对话框显示变量 a 的新值,即 10
  6. 调用 test() 函数。
  7. 在全局作用域中,第三个 alert(a) 会弹出一个对话框显示变量 a 的当前值,即 10

24. 手写 trim 函数,保证浏览器兼容性

String.prototype.trim = function (){
    return this.replace(/^\s+/,"").replace(/\s+$/,"");
};

25. 如何获取多个数值中的最大值?

Math.max(10, 30, 20, 40);
// Math.min 可以获取最小值
function max() {
    const nums = Array.prototype.slice.call(arguments); //变为数组
    let max = 0;
    nums.forEach((n) => {
        if (n > max) {
            max = n;
        }
    });
    return max;
}

26. 如何用 JS 实现继承?

详见:继承open in new window

27. 程序中捕获异常的方法

try {
    // todo
} catch (ex) {
    console.error(ex); // 手动捕获 catch
} finally {
    // todo
}
// 自动捕获 catch(但对跨域的 js 如 CDN 的,不会有详细的报错信息)
window.onerror = function (message, source, lineNom, colNom, error) {
}

28. 什么是 JSON ?

首先,JSON 是一种数据格式标准,本质是一段字符串,独立于任何语言和平台

注意,JSON 中的字符串都必须用双引号

{
    "name": "张三",
    "info": {
        "single": true,
        "age": 30,
        "city": "北京"
    },
    "like": ["篮球", "音乐"]
}

其次,JSON 是 JS 中一个内置的全局变量,有 JSON.parse(JSON 转为 JavaScript 对象) 和 JSON.stringify(JavaScript 对象转为 JSON) 两个 API

29. 获取当前页面 url 参数

自己实现:

// const url = 'https://www.xxx.com/path/index.html?a=100&b=200&c=300#anchor';
function query(name) {
    const search = location.search.substr(1); // 去掉前面的 `?`
    const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
    const res = search.match(reg);
    if (res === null) {
        return null;
    }
    return decodeURIComponent(res[2]);
}
console.log( query('a') );
console.log( query('c') );

新的 API URLSearchParams

const pList = new URLSearchParams(location.search);
pList.get('a');

30. 将 url 参数解析为 JS 对象?

自己编写:

function queryToObj() {
    const res = {};
    const search = location.search.substr(1); // 去掉前面的 `?`
    search.split('&').forEach(paramStr => {
        const arr = paramStr.split('=');
        const key = arr[0];
        const val = arr[1];
        res[key] = val;
    })
    return res;
}

新的 API URLSearchParams

function queryToObj() {
    const res = {};
    const pList = new URLSearchParams(location.search);
    pList.forEach((val, key) => {
        res[key] = val;
    })
    return res;
}

31. 实现数组 flatern ,考虑多层级

function flat(arr) {
    // 验证 arr 中,还有没有深层数组,如 [1, [2, 3], 4]
    const isDeep = arr.some(item => item instanceof Array);
    if (!isDeep) {
        return arr; // 没有深层的,则返回
    }

    // 多深层的,则 concat 拼接
    const res = Array.prototype.concat.apply([], arr); // 回归上文,apply 和 call 的区别
    return flat(res); // 递归调用,考虑多层
}
flat([[1,2], 3, [4,5, [6,7, [8, 9, [10, 11]]]]]);

32. 数组去重

要考虑:

  • 顺序是否一致?
  • 时间复杂度

ES5 语法手写:

// 写法一
function unique(arr) {
    const obj = {};
    arr.forEach(item => {
        obj[item] = 1; // 用 Object ,去重计算高效,但顺序不能保证。以及,非字符串会被转换为字符串!!!
    })
    return Object.keys(obj);
}
unique([30, 10, 20, 30, 40, 10]);
// 写法二
function unique(arr) {
    const res = [];
    arr.forEach(item => {
        if (res.indexOf(item) < 0) { // 用数组,每次都得判断是否重复(低效),但能保证顺序
            res.push(item);
        }
    })
    return res;
}
unique([30, 10, 20, 30, 40, 10]);

用 ES6 Set:

// 数组去重
function unique(arr) {
    const set = new Set(arr);
    return [...set];
}
unique([30, 10, 20, 30, 40, 10]);

33. 手写深拷贝

详见:深拷贝open in new window

【注意】Object.assign 不是深拷贝,可以顺带讲一下用法

  • Object.assign(obj1, {...})
  • const obj2 = Object.assign({}, obj1, {...})

34. 介绍一下 RAF requestAnimationFrame

想用 JS 去实现动画,老旧的方式是用 setTimeout 实时刷新,但这样效率非常低,而且可能会出现卡顿。

  • 想要动画流畅,更新频率是 60帧/s ,即每 16.6ms 更新一次试图。太慢了,肉眼会感觉卡顿,太快了肉眼也感觉不到,资源浪费。
  • 用 setTimeout 需要自己控制这个频率,而 requestAnimationFrame 不用自己控制,浏览器会自动控制
  • 在后台标签或者隐藏的<iframe>中,setTimeout 依然会执行,而 requestAnimationFrame 会自动暂停,节省计算资源

35. 对前端性能优化有什么了解?一般都通过那几个方面去优化的?

原则:

  • 多使用内存、缓存或者其他方法
  • 减少 CPU 计算、较少网络

方向:

  • 加载页面和静态资源
  • 页面渲染