前端面试题集锦2

206 阅读16分钟

实现观察者模式

<script>
    // 定义set集合
    let Observes = new Set();
    // 把观察者函数放入set集合
    const observe = (fn) => {
        Observes.add(fn);
    }
    // 返回原始对象代理,拦截赋值操作
    const observeable = (obj) => {
        return new Proxy(obj, {
            set: setFun
        })
    }
    function setFun(target, key, value, receiver) {
        // 获取对象赋值操作
        const result = Reflect.set(target, key, value, receiver);
        // 执行所有观察者函数
        Observes.forEach(observe => {
            observe()
        });
        // 执行赋值操作
        return result;
    }
	
    let obj = observeable({ name: '1234' });
    observe(function() {
        console.log(1);
    })
    observe(function() {
        console.log(2);
    })
    observe(function() {
        console.log(3);
    })
    observe(function() {
        console.log(4);
    })
    obj.name = '4321';
</script>

// 输出:1 2 3 4

为不具备Interator接口的对象提供遍历方法

function* objectEntries(obj) {
    let keys = Reflect.ownKeys(obj);
    for (const key of keys) {
        yield [key, obj[key]];
    }
}

const obj = {name: 'tom', age: 26, city: 'newyork'}
for (const [key, value] of objectEntries(obj)) {
    console.log(`${key}-${value}`)
}

// 输出:name-tom age-26 city-newyork

51、逗号表达式

<script>
	var x = 20;
	var temp = {
		x: 40,
		foo: function () {
			var x = 10;
			console.log(this.x);
		}
	};
	(temp.foo, temp.foo)();
        temp.foo();
</script>

// 输出:20 40

解析:逗号操作符,从左至右计算它的操作数,返回最后一个操作数的值

注意:(temp.foo, temp.foo)() 上面一行的分号不能省略,否则会报 Cannot read property 'foo' of undefined

52、扁平化数组

function flattenDeep(arr) {
    return arr.flat(Infinity);
}
function flattenDeep(arr) {
    var str = '[' + arr.toString() + ']';
    return eval(str);
}
function flattenDeep(arr) {
    return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []) 
}

53、数组去重

// 方式一
function uniq(arr) {
    return arr.filter((item, index, a) => index === a.indexOf(item))
}

// 方式二
function uniq(arr) {
    return [...new Set(arr)]
}

// 方式三
function uniq(arr) {
    return arr.reduce((prev, item) => {
        if (prev.includes(item)) {
            return prev;
        } else {
            return [...prev, item]
        }
    }, [])
}

54、new操作符手动实现

1) new操作符做了什么
new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new关键字会进行如下操作:
创建一个空的简单的JavaScript对象(即{});
链接该对象(即设置该对象的构造函数)到另一个对象;
将步骤1创建的对象作为this的上下文环境;
如果该函数没有返回对象,则返回this2)代码实现(简单实现)
function newOperator(ctor) {
    if (typeof ctor !== 'function') {
        throw 'newOperator function the first param must be a function';
    }
    var args = Array.prototype.slice.call(arguments, 1);
    // 1、创建一个空的简单的JavaScript对象
    var obj = {};
    // 2、链接该新创建的对象(即设置该对象的__proto__)到该函数的prototype对象上
    obj.__proto__ = ctor.prototype;
    // 3、将步骤1新创建的对象作为this的上下文
    var result = ctor.apply(obj, args);
    // 4、如果该函数没有返回对象,则返回新创建的对象
    var isObject = typeof result === 'object' && result != null;
    var isFunction = typeof result === 'function';
    return isObject || isFunction ? result : obj;
}

// 测试
function Person(name, age) {
    this.name = name;
    this.age = age;
}
let p1 = newOperator(Person, 'tom', 26);
console.log('p1: ', p1);

// 更完整的实现
/**
* 模拟实现 new 操作符
* @param {Function} ctor [构造函数]
* @return {Object|Function|Regx|Date|Error} [返回结果]
*/
function newOperator(ctor) {
    if (typeof !== 'function') {
        throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1、创建一个全新对象
    // 2、并且执行 [[prototype]] 链接
    // 3、通过 new 创建的每个对象将最终被 [[prototype]] 链接到这个函数的 prototype 对象上
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...argumrnts]、Array.from(arguments)
    // 除去 ctor 构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 4、生成的新对象会绑定到函数调用的 this
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 5、这些类型中合并起来只有 Object 和 Function两种类型,typeof null也是object,所以要不等于
    // null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult == 'function';
    if (isObject || isFunction) {
        return ctorReturnResult;
    }
    // 6、如果函数没有返回类型 'Object'(包含 Function、Array、Date、RegExg、Error),那么
    // new 表达式中的函数调用会自动返回这个新的对象
    return newObj;
}

55、实现 (5).add(3).minus(2) 功能

Number.prototype.add = function(number) {
    if (typeof number !== 'number') {
        throw new Error('请输入正确的数字!');
    }
    return this.valueOf + number;
}
Number.prototype.minus = function(number) {
    if (typeof number !== 'number') {
        throw new Error('请输入正确的数字');
    }
    return this.valueOf - number;
}

(5).add(3).minus(2); // 6


/**
* 扩展
* 大数加减,通过Number原生的安全极值来进行判断,超出则直接取安全极值
* 多位数小数加减:取JS安全极值位数-2作为最高兼容小说位数
*/
Number.MAX_SAFE_DIGITS = Number.MAX_SAFE_INTEGER.toString().length - 2;
Number.prototype.digits = function() {
    let result = (this.valueOf().toString().split('.')[1] || '').length;
    return result > Number.MAX_SAFE_DIGITS ? Number.MAX_SAFE_DIGITS : result
}
	
Number.prototype.add = function (i = 0) {
    if (typeof i !== 'number') {
        throw new Error('请输入正确的数字');
    }
    const v = this.valueOf();
    const thisDigits = this.digits();
    const iDigits = i.digits();
    const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
    const result = (v * baseNum + i * baseNum) / baseNum;
    if (result > 0) {
        return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result;
    } else {
	return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result;
    }
};
Number.prototype.minus = function (i = 0) {
    if (typeof i !== 'number') {
        throw new Error('请输入正确的数字');
    }
    const v = this.valueOf();
    const thisDigits = this.digits();
    const iDigits = i.digits();
    const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
    const result = (v * baseNum - i * baseNum) / baseNum;
    if (result > 0) {
        return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result;
    } else {
        return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result;
    }
};
	
console.log((5).add(3).minus(6.234345)); // 1.765655

56、Map、Set、WeakMap和WeakSet的区别

  1. Map                                                                                                                                键值对集合,键名可以为任意类型,可以遍历
  2. WeakMap                                                                                                                      只接受对象作为键名(null除外);键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;不能遍历,方法有get、set、has、delete
  3. Set                                                                                                                                  成员唯一,无序且不重复;[value, value],方法有 add、delete、has
  4. WeakSet                                                                                                                         成员都是对象,成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏;不能遍历,方法有 add、delete、has

57、如何在不使用%运算符的情况下检查一个数字是否为偶数

// 与运算 &
function isEven(num) {
    const isInteger = (num|0) === num;
    if (num & 1 || !isInteger) {
        return false;
    } else {
        return true;
    }
}

// 5 -> 0101    1 -> 0001
// 5 & 1 -> 0001 -> 1


// 递归
function isEven(num) {
    // 取绝对值
    const number = Math.abs(num);
    if (number === 1) {
        return false;
    }
    if (number === 0) {
        return true;
    }
    return isEven(number - 2);
}
	
// Math.round(), 奇数除以2肯定会有小数
function isEven(num) {
    const isInteger = (num|0) === num;
    if (!isInteger) {
        return false;
    }
    const number = num / 2;
    return parseInt(number) === Math.round(number);
}

58、Object.seal()和Object.freeze()方法的区别

当对一个对象使用Object.freeze()时,该对象的属性是不可变的,也就是我们不能更改或编辑这些属性的值,而Object.seal()是可以改变现有的属性。

(1)Object.freeze()

Object.freeze()可以冻结一个对象。一个被冻结的对象再也不能修改;不能像这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性。不能修改已有属性的值。此外,已冻结对象的原型不能修改。freeze()返回和传入参数相同的对象。

(2)Object.seal()

Object.seal()方法封闭一个对象,组织添加新属性并将所有现有属性标记为不可配置,当前属性的值只要是可写的就可以改变。

(3)相同点

  • ES5新增;
  • 对象不可能扩展,也就是不能再添加新属性或方法;
  • 对象已有属性不允许被删除;
  • 对象属性特性不能重新配置

(4)不同点

  • Object.seal()方法生成的密封对象,如果属性是可写的,那么可以修改属性值;
  • Object.freeze()方法生成的冻结对象,属性都是不可写的,也就是属性值无法修改。

59、完成plus函数,通过全部的测试用例

// assign-4.js
'use strict';
function plus(n) {
	// 声明一个数组专门用来存储所有的参数
	var _args = [].slice.call(arguments);
	// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
	var _adder = function () {
		_args.push(...arguments);
		return _adder;
	}
	// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
	_adder.toString = function () {
		return _args.reduce(function (a, b) {
			return a + b;
		});
	};
	return _adder;
}
module.exports = plus;


// test.js
'use strict';
var assert = require('assert');
var plus = require('./lib/assign-4');

describe('测试用例', function() {
	it('plus(0) === 0', function() {
		assert.equal(0, plus(0).toString());
	})
	it('plus(1)(1)(2)(3)(5) === 12', function() {
		assert.equal(12, plus(1)(1)(2)(3)(5).toString());
	})
	it('plus(1)(4)(2)(3) === 10', function() {
		assert.equal(10, plus(1)(4)(2)(3).toString());
	})
	it('plus(1, 1)(2, 2)(3)(4) === 13', function() {
		assert.equal(13, plus(1, 1)(2, 2)(3)(4).toString());
	})
})

60、解释下面代码的意思及所用到的技术点

<script>
    [].forEach.call($$('*'), function(a){
        a.style.outline = '1px solid #' + (~~(Math.random() * (1<<24))).toString(16)
    })
</script>

解析:给页面中每个元素添加1像素的随机颜色的边框
(1)$$:现代浏览器的一个命令行api,相当于document.querySelectorAll2[].forEach.call($$('*'), function(a){ //... })
    通过使用call、apply方法可以实现在类似NodeLists这样的类数组对象上调用数组方法
(3)(~~(Math.random() * (1 << 24))).toString(16)
    ~~ 取反,连续两次操作相当于 parseInt
    1 << 24 位运算,得到 2 ^ 24 - 1
    toString(16) 转换成十六进制字符串

61、Function.length

var fun_a = Function.length;
var fun_b = new Function().length;
console.log(fun_a);
console.log(fun_b);

// 输出:1 0

解析:length是函数对象的一个属性值,指函数有多少个必须要传入的属性值,即形参的个数。形参的个数不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。即:

(function (a, b, c = 1){}).length; // 2

(function (a, b = 1, c){}).length; // 1

Function构造器本身也是个Function。它的length属性值为1,该属性 Writable: false, Enumerable: false, Configurable: true.

Function.prototype对象的length属性值为0

62、不借助中间变量交换两个变量的值

// 例,不借助中间变量交换ab的值
let a = 1;
let b = 2;

(1) 利用加法(a+b有溢出风险)
a = a + b;
b = a - b;
a = a - b;

(2) 利用减法
a = a - b;
b = a + b;
a = b - a;

(3) ES6解构
[a, b] = [b, a]

(4) 按位异或^(异或位运算,相同则为0,不同则为1b = a ^ b;
a = a ^ b; // a = a ^ a ^ b
b = a ^ b; // b = a ^ b ^ b

(5) 逗号表达式
a = b + ((b = a), 0);

63、判断+0和-0

(1)
function isNegtiveZero(number) {
    if (number !== 0) {
        throw new RangeError('The argument must be +0 or -0');
    }
    return 1 / number === +Infinity;
}

(2)
function isNegtiveZero(number) {
    if (number !== 0) {
        throw new RangeError('The argument must be +0 or -0');
    }
    return Object.is(number, 0);
}

64、非node环境获取完整路径

// 补全单引号处代码,获取当前文件的完整位置
const url = '';
export default url;

const url = import.meta.url; // ES2020新特性 

65、写出输出结果

<script>
	class Demo {
		static str = 'Hello World';
		sayStr = () => {
			throw new Error('Need to implement');
		}
	}
	class D1 extends Demo {
		constructor() {
			super();
		}
		sayStr() {
			console.log(D1.str);
		}
	}
	const D2 = new Demo();
	console.log(D1.str);
	D2.sayStr();
</script>

// 输出:'Hello World'    'Need to implement'

66、从整数数组中找出和为目标值的一个组合

// 给定一个整数数组 nums 和一个目标值 target,从数组中找出和为目标值的一个组合

(1)
function towSum(nums, target) {
    let map = new Map();
    for (let i = 0, length = nums.length; i < length; i++) {
        let num = target - nums[i];
        if (map.has(num)) {
            return [map.get(num), i]
        } else {
            map.set(nums[i], i);
        }
    }
}

(2)
function twoSum(nums, target) {
    let length = nums.length;
    for (let i = 0; i < length; i++) {
        let second = nums.pop();
        if (nums.indexOf(target - second) !== -1) {
            return [nums.indexOf(target - second), nums.length];
        }
    }
}

console.log([1, 2, 3, 4], 7); // 2 3

67、3.toString()、3..toString()、3...toString()

3.toString() // 报错
3..toString() // '3'
3...toString() // 报错

68、写出执行结果,并说明原因

function fun() {}
const a = {], b = Object.prototype;
console.log(a.prototype === b);
console.log(Object.getPrototypeOf(a) === b);
console.log(fun.prototype === Object.getPrototypeOf(fun));

// 输出:false true false

解析:__proto__(隐式原型)和prototype(显式原型)

(1)what?

  • 显示原型:每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性执行函数的原型对象。(需要注意的是,通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性)
  • 隐式原型:JavaScript中任意对象都有一个内置属性[[prototype]],ES5之前没有标准方法访问这个内置属性,当时大多浏览器都可以通过__proto__访问。ES5中有了对于这个内置属性标准的Get方法 Object.prototypeOf()(Object.prototype是个例外,它的__proto__是null)
  • 两者的关系:隐式原型指向创建这个对象的函数(constructor)的prototype

(2)作用

  • 显示原型:用来实现基于原型的继承与属性的共享
  • 隐式原型:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj对象中的x属性时,如果在obj中找不到,就会沿着__proto__依次查找

(3)__proto__ 的指向

根据ECMA定义,“to the value of its constructor‘s ’prototype‘” -- 指向创建这个对象的函数的显示原型,所以关键点在于找到创建这个对象的构造函数,JS中对象被创建的方式:

  • 对象字面量方式
  • new 的方式
  • ES5中Object.create()

当时本质上只有一种:也就是通过new,因为字面量方式就是为了方便开发人员开发的一个语法糖,本质就是 var o = new Object(), o.xx = '', o.yy = ''

说明:

  1. a.prototype === b -> false                                                                                           prototype是函数才有的属性,当你创建一个函数时,js会自动为这个函数加上prototype属性,值是一个空对象,而实例对象是没有prototype属性的,a.prototype -> undefined
  2. Object.getPrototypeOf(a) === b  -> true                                                                    首先要明确对象和构造函数的关系,对象在创建时,其__proto__会指向其构造函数的prototype属性,Object实际上是一个构造函数(typeof Object的结果为’function‘),使用字面量创建对象和new Object创建对象是一样的,所以a.__proto__也就是Object.prototype。
  3. fun.prototype === Object.getPrototypeOf(fun) -> false                                             fun.prototype和Object.getPrototypeOf(fun)不是一回事
  • fun.prototype是使用new创建的f实例的原型,fun.prototype === Object.getPrototypeOf(new fun()) -> true
  • Object.getPrototypeOf(fun)是fun函数的原型,Object.getPrototypeOf(fun) -> Function.prototype -> true

69、写出执行结果(正则 test 方法)

<script>
	const lowerCaseOnly = /^[a-z]+$/;
	console.log(lowerCaseOnly.test('demo'));
	console.log(lowerCaseOnly.test(null));
	console.log(lowerCaseOnly.test());
</script>

// 输出:true true true

解析:test参数会被调用toString强制转换成字符串,后两个得到的字符串为'null'、'undefined'

70、写出执行结果(正则exec方法)

<script>
    function captureOne(re, str) {
        var match = re.exec(str);
        return match && match[1];
    }
    var numRe = /num=(\d+)/ig,
        wordRe = /yideng=(\w+)/i,
        a1 = captureOne(numRe, 'num=1'),
        a2 = captureOne(wordRe, 'yideng=1'),
        a3 = captureOne(numRe, 'NUM=2'),
        a4 = captureOne(wordRe, 'YIDENG=2'),
        a5 = captureOne(numRe, 'Num=3'),
        a6 = captureOne(wordRe, 'YiDeng=3');

    console.log(a1 === a2);
    console.log(a3 === a4);
    console.log(a5 === a6);
</script>

// 输出:true false true

解析:

(1) exec()方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或null;

(2) 但是在JavaScript中使用exec进行正则表达式全局匹配时要注意

  • 在全局模式下,当exec()找到了与表达式相匹配的文本时,在匹配后,它把正则表达式对象的lastIndex属性设置为匹配文本的最后一个字符的下一个位置。
  • 这就是说,你可以通过反复调用exec()方法来遍历字符串的所有匹配文本。
  • 当exec()再也找不到匹配的文本时,它将返回null,并把lastIndex属性重置为0。

(3) 所以在全局模式下,如果在一个字符串中完成了一次模式匹配之后要开始检索新的字符串,就必须手动的把lastIndex属性重置为0

例:

function captureOne(re, str) {
    var match = re.exec(str);
    return match && match[1];
}
var numRe = /num=(\d+)/ig;

var a1 = captureOne(numRe, 'num=1');
console.log('numRe.lastIndex: ', numRe.lastIndex); // 5
var a3 = captureOne(numRe, 'NUM=2');
console.log('numRe.lastIndex: ', numRe.lastIndex); // 0
var a5 = captureOne(numRe, 'Num=3');
console.log('numRe.lastIndex: ', numRe.lastIndex); // 5

71、实现Promise.all方法

  • Promise.all(iterable)方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都 "完成(resolve)" 或参数中不包含promise时回调完成(resolve);如果参数中有一个promise失败(rejected),此实例回调失败(reject),失败的原因是第一个失败promise的结果

  • Promise.all(iterable)通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便可以等待所有任务完成

  • 例:

    var p1 = Promise.resolve(3); var p2 = 1337; var p3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo'); });

    Promise.all([p1, p2, p3]).then(values => { console.log(values); // [3, 1337, "foo"] });

核心思路

  1. 接收一个Promise实例的数组或具有iterator接口的对象作为参数
  2. 这个方法返回一个新的Promise对象
  3. 遍历传入的参数,用Peomise.resolve()将参数 ‘包一层’,使其变成一个promise对象
  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致
  5. 参数数组中一个失败,则触发失败状态,第一个触发失败的Promise错误信息作为Promise.all的错误信息

实现代码:

一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能无显示,这就需要看当前页面的耦合程度~

// 代码实现
function promiseAll(promises) {
    return new Promise(function (resolve, reject) {
        if (!Array.isArray(promises)) {
            throw new Error('argument must be a array');
        }
        var resolvedCounter = 0;
        var resolvedResult = [];
        var promiseNum = promises.length;
        for (let = 0; i < promisesNum; i++) {
            Promise.resolve(promises[i]).then(value => {
                resolvedCounter++;
                resolvedResult[i] = value;
                if (resolvedCounter === promiseNum) {
                    return resolve(resolvedResult);
                }
            }, error => {
                return reject(error);
            })
        }
    })
}

// 测试
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function() {
        console.log(1);
        resolve(1);
    }, 1000)
});
let p2 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log(2);
        resolve(2);
    }, 2000)
});
let p3 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log(3);
        resolve(3);
    }, 3000)
});
promiseAll([p3, p1, p2]).then(res => {
    console.log(res);
});

// 输出:1 2 3 [3, 1, 2]

72、有效括号算法题

<script>
/**
* 1、左括号必须与相同类型的右括号闭合
* 2、左括号必须以正确的顺序闭合
* 注意空字符串可被认为是有效字符串
* '()' -> true  '()[]{}' -> true  '(]' -< false  '([)]' -> false  '{[]}' -> 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;
}

73、写出执行结果并解释原因

<script>
    function fun(n, o) {
        console.log(o);
        return {
            fun: function(m) {
                return fun(m, n);
            }
        }
    }
    const a = fun(0);
    a.fun(1);
    a.fun(2);
    a.fun(3);
    const b = fun(0).fun(1).fun(2).fun(3);
    const c = fun(0).fun(1);
    c.fun(2);
    c.fun(3);
</script>

// 输出:
// undefined 0 0 0
// undefined 0 1 2
// undefined 0 1 1

解析:闭包知识考察,

74、写出执行结果并解释原因

<script>
    var arr1 = 'ab'.split('');
    var arr2 = arr1.reverse();
    var arr3 = 'abc'.split('');
    arr2.push(arr3);
    console.log(arr1.length);
    console.log(arr1.slice(-1));
    console.log(arr2.length);
    console.log(arr2.slice(-1));
</script>

// 输出:3 [['a', 'b', 'c']] 3 [['a', 'b', 'c']]

解析:reverse方法颠倒数组元素位置,改变数组并返回数组引用

75、写出执行结果并解释原因

<script>
    var F = function() {};
    Object.prototype.a = function() {
        console.log('hello');
    }
    Function.prototype.b = function() {
        console.log('world');
    }
    var f = new F();
    F.a();
    F.b();
    f.a();
    f.b();
</script>

// 输出:hello world hello 报错

解析:F是一个构造函数,f是构造函数的实例

F instanceof Object -> true、F instanceof Function -> true

f不是Function的实例,因为它本来就不是构造函数

76、JavaScript比较方式

<script>
    const a = [1, 2, 3],
          b = [1, 2, 3],
          c = [1, 2, 4],
          d = '2',
          e = '11';
    console.log([a == b, a === b, a > c, a < c, d > e]); 
</script>

// 输出:[false, false, false, true, true]

解析:

  • JavaScript有两种比较方式:严格比较运算符和转换类型比较运算符。 对于严格比较运算符(===)来说,仅当两个操作数的类型相同且值相等为true,而对于被广泛使用的 比较运算符(==)来说,会在进行比较之前,将两个操作数转换成相同的类型。对于关系运算符(比如 <=)来说, 会先将操作数转为原始值,使它们类型相同,再进行比较运算。 当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域) 时才相等,即他们在栈内存中的引用地址相同。 
  • 数组(也就是对象)进行比较时,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用valueOf 方法;如果返回的还是对象,再接着调用toString方法。

关于valueOf、toString的调用顺序

  1. JavaScript中对象到字符串的转换经历的过程如下:                                                       如果对象具有toString()方法。javascript会优先调用此方法,如果返回的是一个 原始值(原始值包括null、undefined、布尔值、字符串、数字),javascript会将这个原始值转为字符串并返回。 如果对象不具有toString()方法,或者调用toString()方法返回的不是原始值,则javascript会判断是否存在 valueOf()方法,如若存在则调用此方法,如果返回的是原始值,javascript会将元件你是指转换为字符串作为结果。 如果javascript无法调用toString()valueOf()返回原始值得时候,则会报一个类型错误异常得警告。比如: String([1, 2, 3]);
  2. JavaScript中对象转化为数字得过程:                                                                javascript优先判断对象是否具有valueOf()方法,如具有则调用,若返回一个原始值,javascript会将原始值 转换为数字并作为结果。如果对象不具有valueOf()方法,javascript则会调用toString()方法,若返回得是 原始值,javascript会将原始值转换为数字并作为结果。如果javascript无法调用toString()valueOf()返回原始值, 则会报一个类型错误异常得警告。比如:Number([1, 2, 3]);

77、补充代码

const str = '1234567890';
function formatNumber(str) {
    // ...
}

formatNumber(str); // 1,234,567,890


(1)
function formatNumber(str) {
    let length = str.length;
    let result = [];
    while(length >= 3) {
        result.unshift(str.slice(length-3, length));
        length -= 3;
    }
    str.length % 3 && result.unshift(str.slice(0, str.length % 3));
    return result.toString();
}

(2)
function formatNumber(str) {
    return str.split('').reverse().reduce((prev, next, index) => {
        return ((index % 3) ? next : (next + ',')) + prev;
    })
}

(3)
function formatNumber(str) {
    return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

(4)
function formatNumber(str) {
    return Number(str).toLocaleString('en-US');
}

(5)
// Intl对象是ECMAScript国际化API的一个命名空间,它提供了精确的字符串对比,数字格式化,
// 日期和时间格式化。Collator、NumberFormat和DateTimeFormat对象的构造函数是Intl对象的属性
// Intl的兼容性不太好
function formatNumber(str) {
    return new Intl.NumberFormat().format(str);
}

78、DOMContentLoaded

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Document</title></head><body>    <!--         1.Script放在底部还会 dom 的解析和渲染吗?        2.Script内部的代码会等待 css 加载完吗?        3.Css加载会影响 DOMContentLoaded 么?     -->    <link href="https://cdn/css/bootstrap.css" rel="stylesheet"/>    <h1>Demo</h1>    <script>        console.log('DOMContentLoaded');    </script></body></html>

// 1、script代码放在底部会影响渲染,但不会影响解析
// 2、script内的代码会等待css加载
// 3、css代码中若无script代码段,就不会影响DOMContentLoaded

79、null和0进行比较

<script>
    console.log(null == 0);
    console.log(null <= 0);
    console.log(null < 0);
</script>

// false true false

解析:

  1. javascript中null不等于零,也不是零;
  2. null值等于undefined,剩下它俩和谁都不相等;
  3. 关系运算符,在设计上总是需要运算元尝试转为一个number,而相等运算符在设置及上,则没有这方面的考虑。所以计算null<=0或者>=0的时候触发Number(null),它将被视为0.

80、数组sort

<script>
    const arr1 = ['a', 'b', 'c'];
    const arr2 = ['b', 'c', 'a'];
    console.log(
	arr1.sort() === arr1, 
	arr2.sort() == arr2,
	arr1.sort() === arr2.sort()
    );
</script>

// 输出:true true false

解析:sort对数组进行排序并返回数组的引用,对象的比较与顺序无关

81、手动实现防抖和节流

// 函数防抖
const debounce = (fn, delay) => {
    let timer = null;
    return (..args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, delay);
    }
}

// 函数节流
// 定时器实现
const throttle = (fn, delay = 500) => {
    let flag = true;
    return (...args) => {
        if (!flag) return;
        flag = false;
        setTimeout(() => {
            fn.apply(this, args);
            falg = true;
        }, delay)
    }
}
// 时间戳实现
const throttle = (fn, delay = 500) => {
    let preTime = Date.now();
    return (...args) => {
        const nowTime = Date.now();
        if (nowTime - preTime >= delay) {
            preTime = Date.now();
            fn.apply(this, args);
        }
    }
}

(1)防抖函数

  • 原理:事件触发n秒后再执行,如果在这n秒内再次触发,则重新计时。
  • 适用场景:
  1. 按钮提交场景,防止多次提交按钮,只执行最后提交的一次;
  2. 服务端验证场景,表单验证需要服务端配合,只执行一段连续输入事件的最后一次,还有搜索联想词功能类似。

(2)节流函数

  • 原理:规定在单位时间,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。防抖是延迟执行,节流是间隔执行,函数节流即每个一段时间就执行一次。
  • 适用场景
  1. 拖拽场景:固定时间内只执行一次,防止高频次触发位置变动
  2. 缩放场景:监控浏览器resize
  3. 动画场景:避免短时间内多次触发动画引起性能问题

82、隐式转换

<script>
	let a = [];
	let b = '0';
	console.log(a == 0);
	console.log(a == !a);
	console.log(b == 0);
	console.log(a == b);
</script>

// 输出:true true true false

解析:

  1. 对象与原始值类型比较,对象类型会依照ToPrimitive规则转换成原始类型的值再进行比较。①[] == 0 => [].valueOf().toString() == 0 => ''==0,数组[]是对象类型,所以会进行ToPrimitive操作,即调用valueOf再调用toString,数组转换为空字符串‘’;② ‘’ == 0 => Number('') == 0 => 0 == 0 => true。空字符串在和数字0比较时,比较的是原始类型的值,原始类型的值会转化成数值再进行比较,得到true。
  2. [] == ![],!的优先级高于==,所以先将![]转换为boolean值,null、undefined、NaN、以及空字符串取反都为true,其余为false,[] == false,有一个操作数时布尔值,则在比较相等性之前先将其转换为数值 [] == 0
  3. 如果比较的时原始值类型,原始类型的值会转成数值再进行比较
  4. [] == '0' => ‘’ == ‘0’ => false

知识点:ToString、ToNumber、ToBoolean、ToPrimitive转换规则

  • ToString:(不是对象的toString方法),指的是其他类型的值转化为字符串的操作,null => 'null',undefined => 'undefined',布尔类型:true => 'true'、false => ‘false’,数字类型:10 => '10'、1e21 => '1e+21',数组:转换为字符串是将所有的元素通过‘,’连接起来,[1, 2] => '1,2',空数组转化为空字符串,数组中的null和undefined会被当做空字符串处理,普通对象:相当于直接使用Object.prototype.toString(),返回"[object Object]"
  • ToNumber:其它类型转换为数字类型的操作
  1. null => 0
  2. undefined => NaN
  3. 字符串:纯数字转为对应数字,空字符串转为0,其它转为NaN
  4. 布尔:true => 1、false => 0
  5. 数组:先被转为原始类型,也就是ToPrimitive,然后根据转换的原始类型按照上面的规则处理
  6. 对象:同数组的处理
  • TBoolean:其他类型转化为布尔类型的操作,js中的假值只有false、null、undefined、空字符、0和NaN,其它值转为布尔型都为true
  • ToPrimitive:对象类型(如:数组、对象)转换为原始类型的操作,
  1. 当对象类型需要被转化为原始类型时,它会先查找对象的valueOf方法,如果valueOf方法返回原始类型的值,则ToPrimitive的结果就是这个值;
  2. 如果valueOf不存在或valueOf返回的不是原始类型的值,就会尝试调用对象的toString方法,也就是会遵循对象的ToString规则,然后使用toString的返回值作为ToPrimitive的结果,如果valueOf和toString都没有返回原始类型的值,则会抛出异常;
  3. 注意:对于不同类型的对象来说,ToPrimitive的规则有所不同,比如Date对象会先调用toString

83、写出打印结果

<script>
	var obj = {};
	var x = +obj.hello?.world ?? '测试';
	console.log(x);
</script>

// 输出:NaN

解析:?省去判断key的麻烦,obj.hello?.world遇到不存在的值返回undefined,+undefined强制转化number NaN,??空值合并操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

84、写出输出结果

<script>
	function foo() {
		console.log(length);
	}
	function bar() {
		var length = 'hello world';
		foo();
	}
	bar();
</script>

// 输出:0(页面iframe数量)

解析:函数作用域是函数执行时创建的,函数执行结束后,函数作用域就随之销毁。

85、写出执行结果

<script>
	let obj = { ...null, ...undefined };
	console.log(obj);
	let arr = [..null, ...undefined];
	console.log(arr);
</script>

// 输出:{} 抛出异常

解析:对象会忽略null和undefined,数组会抛出异常。这是ECMA的规范定义。同时null只能等与undefined(==),其余谁也不等

86、写出类数组转换结果

<script>
	const arrLike = {
		length: 4,
		0: 0,
		1: 1,
		'-1': 2,
		3: 3,
		4: 4,
	}
	console.log(Array.from(arrLike));
	console.log(Array.prototype.slice.call(arrLike));
</script>

// 输出: [0, 1, undefined, 3]  [0, 1, empty, 3]

解析

  1. 类数组是一个拥有length属性,并且其他属性为非负整数的普通对象,类数组不能直接调用数组方法

  2. 类数组转换为数组的方式:Array.from()Array.prototype.slice.call()、使用Array.prototype.forEach()进行属性遍历并组成新的数组

  3. 转换须知:①准换后的长度由length属性决定,索引不连续时转换结果是连续的,会自动补位;②仅考虑0或正整数索引;③使用slice转换产生稀疏数组。

稀疏数组是指索引不连续,数组长度大于元素个数的数组。

var arr = new Array(5);
console.log(arr); // [empty × 5] empty非js基础数据类型 arr[1] => undefined
arr.forEach(i => {console.log(i)}) // 无log输出

访问稀疏数组的缺失元素时会返回undefined,是因为js在发现元素缺失时会临时赋值undefined,类似js变量的声明提前

稀疏数组比密集数组访问速度慢内存利用率高

87、写出执行结果

<script>
	console.log(1 < 2 < 3);
	console.log(3 > 2 > 1);
</script>

// 输出:true false

解析:运算符<、>从左向右

1 < 2 < 3 => true < 3 => 1 < 3 => true

3 > 2 > 1 => true > 1 => 1 > 1 => false

88、以下代码会抛出异常吗?

<script>
	let obj = {x: 1, y: 2};
	let Getter1 = {
		...obj,
		get x() {
			throw new Error();
		}
	}
	let Getter2 = {
		...obj,
		...{
			get x() {
				throw new Error();
			}
		}
	}
</script>

// Getter1不会报错 Getter2会报错

解析:

第一段代码相当于:

let Getter1 = {};
Object.assign(Getter1, obj);
Object.defineProperty(Getter1, 'x', {
    get() { throw new Error() },
    enumerable: true,
    configurable: true,
})

// 第二段代码在结构时 x 被调用了
// 原因是读取一个属性时会去对象的[[get]]中查找是否有该属性名

89、请问React调用机制一共对任务设置了几种优先级别?每种优先级都代表的具体含义是什么?在你开发过程中如果遇到影响主UI渲染卡顿的任务,你又是如何利用这些优先级的?

90、Vue父组件监听子组件的生命周期

  • $emit触发方式

  • @hook方式,如下

    // Parent.vue <Child @hook:mounted='doSomthing'> doSomething() { // ... }

    // Child.vue mounted() { // ... }

91、Vue为什么要用vm.$set来解决对象新增属性不能响应的问题,说出以下代码的实现原理

Vue.set(object, propertyName, value);
vm.$set(object, propertyName, value);

解析:

  • Vue使用了Object.defineProperty实现双向数据绑定(Vue2)
  • 在初始化实例时对属性进行 getter/setter 转化
  • 属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加和删除)

所以Vue提供了Vue.set(object, propertyName, value)/vm.$set(object, propertyName, value)

Vue源码位置:vue/src/core/instance/index.js

vm.$set实现原理:

  • 如果目标是数组,则直接使用数组的splice方法触发响应式
  • 如果目标是对象,会先判断属性是否存在、对象是否是响应式
  • 最终如果要对属性进行响应式处理,则通过defineReactive(已定义方法)方法进行响应式处理

92、既然Vue可以通过数据劫持来精准探测数据在dom上的变化,为什么还需要虚拟DOM diff

解析:

现代前端框架有两种方式侦测变化,一种是pull、一种是push

**pull:**其代表为React,我们通常使用setState API显示更新,然后React会进行一层层的Virtual DOM diff操作找出差异,然后Patch到DOM上,React一开始就不知道哪里发生了变化,只知道有变化了,然后进行暴力的diff操作,另一个代表的就是angular的脏检查操作

**push:**Vue响应式系统则是push的代表,当vue初始化时会对数据data进行依赖收集,一旦数据发生了变化,响应系统就会立刻得知。因此Vue一开始就知道哪里发生了变化,因为通常绑定一个数据就需要一个Watcher,一旦我们绑定数据细粒度过高的话就会产生大量的Watcher,这会带来内存和依赖追踪的开销,而细粒度过高则无法精准侦测变化,因此Vue的设计是采用中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后再组件内部进行Virtual DOM diff获取更具体的差异,而Virtual DOM diff是pull操作,Vue是通过push+pull的方案进行变化侦测的。

93、Vue组件中写name选项除了有keep-alive还有其他作用吗?谈谈你对keep-alive的理解(平时使用和源码实现方面)

  • 组件中写name选项的作用
  1. 项目使用keep-alive时,可搭配组件name进行缓存过滤
  2. DOM做递归组件时需要调用自身name
  3. vue-devtools调试工具里显示的组件名称是由vue中的name决定的
  • keep-alive使用
  1. Vue的一个内置组件,可以是被包含的组件保留状态,避免重新渲染
  2. 一般结合路由或动态组件使用,用于缓存组件
  3. 提供include和exclude属性,两者都支持字符串或正则表达式,exclude比include优先级高
  4. 对应两个钩子函数activated和deactivated,组件被激活时触发activated,组件被移除是触发deactivated

95、Promise.all在执行过程中任何一个Promise出现错误的时候都会执行reject,导致其它正常返回的数据无法使用,有什么解决方案

  1. 在单个的catch中对失败的promise请求做处理
  2. 把reject操作换成resolve(new Error('自定义error'))
  3. 引入Promise.allSettled
  4. 使用第三方库promise-transaction

96、vue组件通信方式有哪些?请手写EventBus

Vue组件间通信主要指:父子组件通信、隔代组件通信、兄弟组件通信

通信方式:

  • props / $emit 适用于父子组件通信
  • ref、parent、$children 适用于父子组件通信
  • $attrs/$listeners 适用于隔代组件通信
  • provide/inject 适用于隔代组件通信
  • vuex 适用于父子、兄弟、隔代组件通信

手写实现简版EventBus

<script>
    class EventEmitter {
        constructor () {
            // 存储事件
            this.events = this.events || new Map()
        }
        // 监听事件
        addListener (type, fn) {
            if (!this.events.get(type)) {
                this.events.set(type, fn)
            }
        }
        // 触发事件
        emit (type) {
            let handle = this.events.get(type)
            handle.apply(this, [...arguments].slice(1))
        }
    }
	
    let emitter = new EventEmitter();
    emitter.addListener('ages', age => {
        console.log('age: ', age);
    })
    emitter.emit('ages', 18) 
</script>

输出:age: 18

98、写出执行结果

<script>
	Object.prototype.test = '测试';
	var a = 123;
	a.b = 456
	console.log(a.test);
	console.log(a.b);
</script>

// 测试 undefined

解析:由于自动装箱,将属性分配给基本类型是有效的,但是属性被添加到了临时包装器对象上,而非基本类型,因此无法获取属性