- 看题目手写
- code
- 2
- 写一个 isPrime()函数
- 请实现一个 flattenDeep 函数,把嵌套的数组扁平化~~
- 请实现一个 uniq 函数,实现数组去重
- 实现 (5).add(3).minus(2) 功能
- 如何在不使用%摸运算符的情况下检查一个数字是否是偶数
- 完成 plus 函数,通过全部的测试用例
- 写出执行结果,并解释原因
- 不借助中间变量交换两个变量的值
- 实现一个 isNegtiveZero 函数,只检查+0 和-0,-0 则返回 true,+0 返回 false
- 补全代码
- 一个简单的算法题目
- 写出执行结果,并解释原因
- 写出执行结果,并解释原因
- 写出执行结果,并解释原因
- 写出执行结果,并解释原因
- [手写代码]实现 Promise.all 方法
- 有效括号算法题
- 写出执行结果,并解释原因
- 写出执行结果,并解释原因
- 写出执行结果,并解释原因
- 补充代码,使代码可以正确执行
- Script 放在底部还会影响 dom 的解析和渲染吗?Script 内部的代码执行会等待 css 加载完吗?css 加载会影响 DOMContentLoaded 么?
- 写出下面代码 null 和 0 进行比较的代码执行结果,并解释原因
- 关于数组 sort,下面代码的正确打印结果是什么,并解释原因
- 介绍防抖与节流的原理,并动手实现
- 关于隐式转换,下面代码的执行结果是什么?并解释原因
- 请写出如下代码的打印结果
- 对于 length 下面代码的输出结果是什么?并解释原因
- 对于扩展运算符,下面代码的执行结果是什么?并解释原因
- 写出类数组转换结果,并解释原因
- 写出下面代码 1,2,3 的大小判断结果
- 以下两段代码会抛出异常吗?解释原因?
- 请问 React 调用机制一共对任务设置了几种优先级别?每种优先级都代表的具体含义是什么?在你开发过程中如果遇到影响主 UI 渲染卡顿的任务,你又是如何利用这些优先级的?
- Vue 父组件可以监听到子组件的生命周期吗?如果能请写出你的实现方法。
- Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
- 既然 Vue 通过数据劫持可以精准探测数据在具体 dom 上的变化,为什么还需要虚拟 DOM diff 呢?
- Vue 组件中写 name 选项有除了搭配 keep-alive 还有其他作用么?你能谈谈你对 keep-alive 了解么?(平时使用和源码实现方面)
- 说一下 React Hooks 在平时开发中需要注意的问题和原因?
- Promise.all 中任何一个 Promise 出现错误的时候都会执行 reject,导致其它正常返回的数据也无法使用。你有什么解决办法么?
- 请能尽可能多的说出 Vue 组件间通信方式?在组件的通信中 EventBus 非常经典,你能手写实现下 EventBus 么?
- 请讲一下 react-redux 的实现原理?
- 写出下面代码的执行结果,并解释原因
- React 中 setState 后发生了什么?setState 为什么默认是异步?setState 什么时候是同步?
- 哪些方法会触发 react 重新渲染?重新渲染 render 会做些什么?
- 有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行3次,再次执行无效,如何实现
- 实现add函数,让add(a)(b)和add(a,b)两种调用结果相同
- 5个fetch请求,请求完成后要求立即执行,但最终的输出顺序要按照要求输出ABCDE
看题目手写
请实现如下函数
var addSix = createBase(6);
addSix(10); // returns 16
addSix(21); // returns 27
// addSix是一个函数,也就是说 createBase 函数的返回是一个函数
//---
function createBase(baseNumber) {
return function(N) {
// we are referencing baseNumber here even though it was declared
// outside of this function. Closures allow us to do this in JavaScript
return baseNumber + N;
};
}
var addSix = createBase(6);
addSix(10);
addSix(21);
// 1. 实现⼀个带并发限制的异步调度器Scheduler,
// 保证同时运⾏的任务最多有两个。完善代码中 Scheduler类,使得以下程序能正确输出
// output: 2 3 1 4
// ⼀开始,1、2两个任务进⼊队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
class Scheduler {
constructor() {
this.stack = [];
this.queue = [];
}
add(promiseCreater) {
const p = function(resolve){
const handle = () => {
this.stack.filter(f => f !== p);
const target = this.queue.shift();
if(target) {
this.stack.push(target);
target();
}
resolve();
}
promiseCreater().then(handle);
}.bind(this);
return new Promise((resolve) => {
const exe = () => {p(resolve)}
if(this.stack.length === 2) {
this.queue.push(exe);
}else {
exe();
this.stack.push(exe);
}
});
}
}
const timeout = time => new Promise((resolve) => {
setTimeout(resolve, time);
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => {
return timeout(time);
}).then(() => {
console.log(order);
})
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 给定⻓度为n的整形数组,给定⼩于n的数k,找到数组中出现次数⼤于等于 n 的数字
function find(arr: number[], k: number): number[] {
//TODO
const map = {};
const result = [];
arr.forEach(i => {
if(map[i]) {
map[i] += 1;
}else {
map[i] = 1;
}
if(map[i] >= k ) {
result.push(i);
}
});
return [...new Set(result)];
}
//example const arr = [1, 2, 3, 4, 2, 1]; find(arr, 3);
// [1,2]
实现一个 Symbol,满足以下条件:
● 返回的值不能相同
● 如果我们希望使⽤同⼀个 Symbol 值,可以使⽤ Symbol.for。它接受⼀个字符串作为参数,然后搜索
有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则返回⼀个新的 Symbol
值
● Symbol 无法作为构造函数使用,即 new Symbol 会报异常
const MySymbol = (() => {
const map = {};
return (keyword) => {
const value = Math.random() * Date.now();
if(map[keyword]) {
return map[keyword];
}else {
map[keyword] = value;
}
const obj = {
value,
for: () => {
return value;
}
}
return obj;
}
})()
请实现如下的函数,可以批量请求数据,所有的URL地址在 urls 参数中,同时可以通过 max 参数控制
请求的并发数,当所有的请求结束之后,需要执⾏ callback 回调函数。发请求的函数可以直接使⽤
fetch 即可。
const batchFetch = (urls, max, callback) => {
const stack = [];
const result = [];
let nextIdx = 0;
let finishNum = 0;
const next = () => {
const idx = nextIdx;
const url = urls[idx];
const setData = (data) => {
finishNum += 1;
result[idx] = data;
stack.filter(u => u !== url);
if(finishNum === urls.length) {
callback(result);
} else if(stack.length < max) {
next();
}
}
if(url) {
nextIdx++;
stack.push(url);
fetch(url).then(data => {
setData(data)
}).ctach(err => {
setData(err)
});
}
}
while(stack.length < max || nextIdx <= urls.length) {
next();
}
}
给定单项链表,求距离终点 k 的节点,要求空间复杂度为 O(1)
// 假设链表长度为10, k = 4;
// 那么就是从头开始数第 6(10 - 4)个
const getNode = (head, k) => {
const next = head;
const kNode = head;
let step = 0;
while(next) {
next = next.next;
if(step < k) {
step++;
}else {
kNode = head.next;
}
}
return step === k ? kNode : null;
}
int a[]={21,11,45,56,9,66,77,89,78,68,100,120,111} 请查询数组中有没有比它前面元素都大,比它后面的元素都小的数,没有打印-1,有显示其索引。要求时间复杂度和空间复杂度最大都是O(N)。
function cal(arr) {
let leftMax = arr[0];
let val = leftMax;
let valIdx = 0;
arr.forEach((n, idx) => {
if(n > leftMax) {
leftMax = n;
if(val === null) {
valIdx = idx;
val = n;
}
}else {
if(n < val) {
valIdx = -1;
val = null;
}
}
});
return valIdx;
}
// 实现一个fibonacci函数,输入数字n,
// 输出fibonacci数列的第n项数字,并给该函数加入缓存功能。
function a() {
const map = {
0: 1,
1: 1,
};
return function(n) {
if(map[n - 1]) {
return map[n - 1];
}else {
const dp = [1,1];
for(let i = 2; i < n; i++) {
dp[i] = dp[i - 2] + dp[i - 1];
map[i] = dp[i];
}
return dp[n - 1];
}
}
}
function myNew(f, ...args) {
const o = Object.create({});
o.__proto__ = f.prototype;
const result = f.call(o, args);
return result instanceof f ? result : o;
}
// 给定一个字符串里面只有"R" "G" "B" 三个字符,请排序,最终结果的顺序是R在前 G中 B在后。
// 要求:空间复杂度是O(1),且只能遍历一次字符串。
function f(str) {
let lastR = 0;
let lastG = -1;
const arr = str.split('');
arr.forEach((s, idx) => {
arr.splice(idx,1);
if(s === 'R') {
arr.splice(lastR,0,'R');
lastR += 1;
lastG += 1;
}else if(s === 'G') {
if(lastG === - 1) {
arr.splice(lastR,0,'G');
lastG = lastR + 1
}else {
arr.splice(lastG,0,'G');
lastG += 1;
}
}else {
arr.splice(lastG, 0, 'B');
}
});
return arr.join('');
}
// 方法二:不用遍历,直接 sort 然后反转数组
function f2(str) {
return str.split('').sort().reverse().join('');
}
实现sleep
某个时间后就去执行某个函数,使用Promise封装
function sleep(fn, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fn);
}, time);
});
}
let saySomething = (name) => console.log(`hello,${name}`)
async function autoPlay() {
let demo = await sleep(saySomething('111'),1000)
let demo2 = await sleep(saySomething('222'),1000)
let demo3 = await sleep(saySomething('333'),1000)
}
autoPlay()
模板引擎实现
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
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; // 如果模板没有模板字符串直接返回
}
## 下面代码中 a 在什么情况下会打印 1?
```js
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
code
var a = {
i: 1,
toString() {
return a.i++;
}
}
if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
let a = [1,2,3];
a.toString = a.shift;
if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
<a name="t8jz1"></a>
#### 实现 (5).add(3).minus(2) 功能。
```markdown
例: 5 + 3 - 2,结果为 6
Number.prototype.add = function(n) {
return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
return this.valueOf() - n;
};
冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
冒泡算法的原理:
升序冒泡: 两次循环,相邻元素两两比较,如果前面的大于后面的就交换位置
降序冒泡: 两次循环,相邻元素两两比较,如果前面的小于后面的就交换位置
```js
// 升序冒泡
function maopao(arr){
const array = [...arr]
for(let i = 0, len = array.length; i < len - 1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
}
}
}
return array
}
看起来没问题,不过一般生产环境都不用这个,原因是效率低下,冒泡排序在平均和最坏情况下的时间复杂度都是 O(n^2),最好情况下都是 O(n),空间复杂度是 O(1)。因为就算你给一个已经排好序的数组,如[1,2,3,4,5,6] 它也会走一遍流程,白白浪费资源。所以有没有什么好的解决方法呢? 答案是肯定有的:加个标识,如果已经排好序了就直接跳出循环。
function maopao(arr){
const array = [...arr]
let isOk = true
for(let i = 0, len = array.length; i < len - 1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
isOk = false
}
}
if(isOk){
break
}
}
return array
}
<a name="bi9iZ"></a>
#### 要求设计 LazyMan 类,实现以下功能。
```markdown
```js
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch
LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
class LazyManClass {
constructor(name) {
this.name = name
this.queue = []
console.log(`Hi I am ${name}`)
setTimeout(() => {
this.next()
},0)
}
sleepFirst(time) {
const fn = () => {
setTimeout(() => {
console.log(`等待了${time}秒...`)
this.next()
}, time)
}
this.queue.unshift(fn)
return this
}
sleep(time) {
const fn = () => {
setTimeout(() => {
console.log(`等待了${time}秒...`)
this.next()
},time)
}
this.queue.push(fn)
return this
}
eat(food) {
const fn = () => {
console.log(`I am eating ${food}`)
this.next()
}
this.queue.push(fn)
return this
}
next() {
const fn = this.queue.shift()
fn && fn()
}
}
function LazyMan(name) {
return new LazyManClass(name)
}
<a name="jH9H0"></a>
#### 已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。
```markdown
<img src="1.jpg" style="width:480px!important;”>
## code
```css
max-width: 300px
transform: scale(0.625,0.625)
<a name="xPsIO"></a>
#### 编程算法题
```javascript
用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。如:输入整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量,输入函数必须只有一个参数传入,必须返回字符串。
function fun(num) {
let num1 = num / 10;
let num2 = num % 10;
if (num1 < 1) {
return num;
} else {
num1 = Math.floor(num1);
return `${num2}${fun(num1)}`;
}
}
var a = fun(12345);
console.log(a);
console.log(typeof a);
循环打印红黄绿
下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
三个亮灯函数:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。
(1)用 callback 实现
const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
callback()
}, timer)
}
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', Function.prototype)
})
})
这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?
上面提到过递归,可以递归亮灯的一个周期:
const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step)
})
})
}
step()
注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。
(2)用 promise 实现
const task = (timer, light) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = () => {
task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。
(3)用 async/await 实现
const taskRunner = async () => {
await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()
}
taskRunner()
2. 实现每隔一秒打印 1,2,3,4
// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
3. 小孩报数问题
有30个小孩儿,编号从1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩 重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?
function childNum(num, count){
let allplayer = [];
for(let i = 0; i < num; i++){
allplayer[i] = i + 1;
}
let exitCount = 0; // 离开人数
let counter = 0; // 记录报数
let curIndex = 0; // 当前下标
while(exitCount < num - 1){
if(allplayer[curIndex] !== 0) counter++;
if(counter == count){
allplayer[curIndex] = 0;
counter = 0;
exitCount++;
}
curIndex++;
if(curIndex == num){
curIndex = 0
};
}
for(i = 0; i < num; i++){
if(allplayer[i] !== 0){
return allplayer[i]
}
}
}
childNum(30, 3)
有这样一个函数 A,要求在不改变原有函数 A 功能以及调用方式的情况下,使得每次调用该函数都能在控制台打印出“HelloWorld”
function A() { console.log(“调用了函数A”); }
## 1
```js
~(function(){
Function.prototype.before = function(beforeFn){
return (...args) => {
// 先执行传入的beforeFn的函数
beforeFn.apply(this, args);
// 再执行调用beforeFn的函数
const res = this.apply(this, args);
return res;
}
}
Function.prototype.after = function(afterFn){
return (...args) => {
// 先执行调用的after的函数
const res = this.apply(this, args);
// 再执行传入的afterFn函数
afterFn.apply(this, args);
return res;
}
}
function A() {
console.log("调用了函数A");
}
const fn = A.before(() => {
console.log('before');
}).after(() => {
console.log('after');
});
fn();
})();
2
function A() {
console.log("调用了函数A");
}
const B = A;
A = () => {
console.log('HelloWorld');
B()
}
A()
<a name="a3J6A"></a>
#### 请你完成一个 safeGet 函数,可以安全的获取无限多层次的数据
> // 请你完成一个safeGet函数,可以安全的获取无限多层次的数据,一旦数据不存在不会报错,会返回 undefined,例如
> var data = { a: { b: { c: "yideng" } } };
> safeGet(data, "a.b.c"); // => yideng
> safeGet(data, "a.b.c.d"); // => undefined
> safeGet(data, "a.b.c.d.e.f.g"); // => undefined
```javascript
// 普通方式遍历
function safeGet(obj,str){
let arr = str.split('.');
let temp = obj;
for(let i = 0;i<arr.length;i++){
let target = temp[arr[i]];
if(!target) return undefined;
if(i === arr.length-1){
return target;
}
temp = target;
}
}
// reduce 方式
const safeGet = (o,path) =>{
try{
return path.split('.').reduce((pre,cur)=>pre[cur],o);
}catch(e){
return undefined;
}
}
写一个 isPrime()函数
写一个isPrime()函数,当其为质数时返回true,否则返回false。 提示:质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
function isPrime(number) {
// If your browser doesn't support the method Number.isInteger of ECMAScript 6,
// you can implement your own pretty easily
if (typeof number !== 'number' || !Number.isInteger(number)) {
// Alternatively you can throw an error.
return false;
}
if (number < 2) {
return false;
}
if (number === 2) {
return true;
} else if (number % 2 === 0) {
return false;
}
var squareRoot = Math.sqrt(number);
for(var i = 3; i <= squareRoot; i += 2) {
if (number % i === 0) {
return false;
}
}
return true;
}
请实现一个 flattenDeep 函数,把嵌套的数组扁平化~~
flattenDeep([1, [2, [3, [4]], 5]]); //[1, 2, 3, 4, 5] // 请实现一个flattenDeep函数,把嵌套的数组扁平化
// 直接利用 ES6 新增数组方法 flat
function flattenDeep(arr){
return arr.flat(Infinity)
}
// 可以利用 reduce 和 concat
function flattenDeep(arr){
arr.reduce((prev,cur)=>{
return prev.concat(Array.isArray(cur) ?
flattenDeep(cur) :
cur)},[])
}
// 使用 stack 无限反嵌套多层嵌套数组
function flattenDeep(input){
const stack = [...input];
const res = [];
while(stack.length){
//使用 pop 从stack 取出并移除值
const next = stack.pop();
if(Array.isArray(next)){
//使用 push 送回内层数组中的元素,不会改变原始输入 origin input
stack.push(next);
}else{
res.push(next);
}
}
//使用 reverse 恢复原数组的顺序
return res.reverse();
}
// 利用数组的 toString 方法返回 以 ,分割的字符串形式但是这个方法会有一个问题,就是如果你的数组中不仅有字符串还有数字,那么经过 split 后都会是 字符串形式的数据,map 之后又会都转为数字,所以和原数组可能有差别。
function flattenDeep(arr){
return arr.toString().split(',').map(item=>+item);
}
请实现一个 uniq 函数,实现数组去重
uniq([1, 2, 3, 5, 3, 2]); //[1, 2, 3, 5] // 请实现一个 uniq 函数,实现数组去重
// 利用ES6新增数据类型Set
// 1、
function uniq(arry){
return [...new Set(arry)]
}
// 2、
function unique10(arr) {
//Set数据结构,它类似于数组,其成员的值都是唯一的
return Array.from(new Set(arr)); // 利用Array.from将Set结构转换成数组
}
// 利用数组的indexOf下标属性来查询
function uniq(arry){
var result = [];
for(var i = 0; i < arry.length; i++){
if(result.indexOf(arry[i]) === -1){
//如果result中没有arry[i],则添加到数组中
result.push(arry[i])
}
}
return result
}
// 利用数组原型对象上的includes方法
function uniq(arry){
var result = [];
for(var i = 0; i < arry.length; i++){
if(!result.includes(arry[i]) === -1){
//如果result中没有arry[i],则添加到数组中
result.push(arry[i])
}
}
return result
}
// 利用reduce
function uniq(arry){
return arry.reduce((prev,cur)=>prev.includes(cur) ? prev : [..prev,cur],[])
}
// 利用Map
function uniq(arry){
let map = new Map();
let result = new Array();
for(let i = 0; i < arry.length; i++){
if(map.has(arry[i])){
map.set(arry[i],true)
}else{
map.set(arry[i],false)
result.push(arry[i])
}
}
return result
}
// 利用数组原型对象上的 filter 和 includes方法组合
function unique(arr) {
var newArr = []
newArr = arr.filter(function (item) {
return newArr.includes(item) ? '' : newArr.push(item)
})
return newArr
}
// 利用数组原型对象上的 forEach 和 includes方法组合
function unique(arr) {
let newArr = []
arr.forEach(item => {
return newArr.includes(item) ? '' : newArr.push(item)
})
return newArr
}
实现 (5).add(3).minus(2) 功能
// 实现 (5).add(3).minus(2) 功能 console.log((5).add(3).minus(2)); // 6
Number.prototype.add = function(number){
if(typeof number !== 'number'){
throw TypeError('the add function need a number');
}
return this + number;
}
Number.prototype.ninus = function(number){
if(typeof number !== 'number'){
throw TypeError('the ninus function need a number');
}
return this - number;
}
如何在不使用%摸运算符的情况下检查一个数字是否是偶数
// 位运算 & 运算符将两个数字都转化为二进制,它使用按位与运算符比较每个位
function isEven(num){
if(num & 1){
return false;
}else{
return true;
}
}
// 递归方式
function isEven(num){
if(num<0 || num ===1){
return false;
}
if(num === 0){
return true;
}
return isEven(num-2);
}
完成 plus 函数,通过全部的测试用例
“use strict”; function plus(n) {} module.exports = plus; // 测试用例如下 (“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()); }); });
// 答案&解析
参考答案:答案不唯一
"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;
写出执行结果,并解释原因
var yideng_a = Function.length; var yideng_b = new Function().length; console.log(yideng_a === yideng_b);
// Function本身期望有一个参数的情况
const a = Function('console.log("aaa")')// 如果报错把它保存成文件再打开
a();// "aaa"
console.log(Function.length)// 1
// 这里创建的函数并不包含期望值
console.dir(a);// length 0
// 同样这样设置不期望有参数
console.dir(new Function())// length 0
// 这么设置期望有两个参数
const add = new Function("a", "b", "return a+b");
console.log(add(1, 2))// 3
console.log(add.length)// 2
// 答案&解析
①每个 JavaScript 函数实际上都是一个 Function 对象。运行 (function(){}).constructor === Function,true 便可以得到这个结论。
②全局的 Function 对象没有自己的属性和方法,但是,因为它本身也是一个函数,所以它也会通过原型链从自己的原型链 Function.prototype 上继承一些属性和方法。
③length是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。与之对比的是, arguments.length 是函数被调用时实际传参的个数。
④length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。
形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
Function 构造器本身也是个Function。他的 length 属性值为 1 。该属性 Writable: false, Enumerable: false, Configurable: true.Function 原型对象的 length 属性值为 0 。
不借助中间变量交换两个变量的值
不借助中间变量交换两个变量的值 比如 let a = 1,b = 2;交换a,b的值
// 答案&解析
1)利用加法
let a = 1,b = 2;
b = a + b;
a = b - a;
b = b - a;
缺点:利用加法 a+b;有溢出风险
2)利用减法
let a = 1,b = 2;
b = a - b;
a = a - b;
b = a + b;
这样就解决了加法溢出的风险,理论上已经很完美了,继续往下看
3)es6解构赋值
let a = 1,b = 2;
[a,b]=[b,a]
4)按位异或^
这里用到了异或这个位运算的性质,即相同则为 0,不同则为 1
对于两个数字,a 和 b。则有 a ^ a ^ b 就等于 b 。我们可以利用这个性质来完成交换。
let a = 1,b = 2;
b = a ^ b;
a = a ^ b; // a = a ^ a ^ b
b = a ^ b; // b = a ^ b ^ b
过程解释:
a = 1 -> 01
b = 2 -> 10
a ^ a -> 01 ^ 01 -> 肯定是00,因为相同为0
a ^ a ^ b -> 00 ^ 10 -> 还是 10 -> b
a ^ b ^ b->
①过程:01 ^ 10 ^ 10 -> 11 ^ 10 -> 01 -> a
②其实这里涉及到离散数学的异或运算性质:交换律:a ^ b ^ c <=> a ^ c ^ b
还有其它性质:任何数于0异或为任何数 0 ^ n => n,相同的数异或为0: n ^ n => 0
5)逗号表达式
逗号表达式是将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值。
利用这个性质,先完成一次赋值操作,然后将赋值操作的返回值变为0. 就可以完成赋值操作
let a = 1,b = 2;
a = b + ((b=a),0);
实现一个 isNegtiveZero 函数,只检查+0 和-0,-0 则返回 true,+0 返回 false
// 实现一个isNegtiveZero函数,只检查+0和-0,-0则返回true,+0返回false function isNegtiveZero(num) { // 代码实现 }
// 答案与解析
在 JavaScript 中, Number 是一种 定义为 64位双精度浮点型(double-precision 64-bit floating point format) (IEEE 754)的数字数据类型,首位是符号位,然后是52位的整数位和11位的小数位。如果符号位为1,其他各位均为0,那么这个数值会被表示成“-0”。
所以JavaScript的“0”值有两个,+0和-0。
1)解题思路
①看到+0和-0,大概想尝试把该数字通过toString()转化成字符串,在使用indexOf('-')判断是否等于0,或者charAt(0)判断是否等于-。很不幸,数值在进行toString()的时候就自动将其转为0了,所以此方法行不通。
②可以尝试另外一个思路,计算机在进行四则及与或模等数值运算时,符号本身也参与运算,JavaScript亦是如此。而使用0对一个数做加减操作对本身是无影响的,乘法虽然得到±0的结果,但是又回到了问题本身对±0的判断了,因此我们可以考虑到除法,加上数值本身有Infinity和-Infinity的区分,分别表示正无穷和负无穷。我们很容易想到使用一个数值来除以±0得到±Infinity。我们使用-1/0或1/-0都得到-Infinity的结果。
③同样的,JavaScript提供很多函数供你使用,但结果不外乎都是借助一个数值进行判断。如:Math.pow(-0, -1) === -Infinity,Math.atan2(-0, -1) === -Math.PI
2)参考答案
①实现方式一
function isNegtiveZero(num) {
if (num !== 0) {
throw new RangeError("The argument must be +0 or -0");
}
return 1 / num === -Infinity;
}
console.log(isNegtiveZero(+0));
console.log(isNegtiveZero(-0));
②实现方式2
ECMAScript2015添加了一个方法Object.is用于对两数值进行比较,可以用于比较±0
Object.is(+0, 0) === true;
Object.is(-0, 0) === false;
function isNegtiveZero(num) {
if (num !== 0) {
throw new RangeError("The argument must be +0 or -0");
}
return !Object.is(num, 0);
}
console.log(isNegtiveZero(+0));
console.log(isNegtiveZero(-0));
补全代码
/ 说明:该文件名未知,位于当前项目内的dist/scripts文件夹内 要求:一句话补全代码,获取它的完整位置:http://xx.com/dis/scripts/xx.js 注:非node环境,node可以使用__dirname / const url = ✍️代码书写处; export default url;
// 答案与解析
const url = import.meta.url
es2020新特性
一个简单的算法题目
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,这两个整数可能有多种组合,找出其中一组组合即可,并返回他们的数组下标。
示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
// 最容易想到的就是暴力枚举,我们可以利用两层 for 循环来遍历每个元素,并查找满足条件的目标元素。不过这样时间复杂度为 O(N^2),空间复杂度为 O(1),时间复杂度较高,我们要想办法进行优化。我们可以增加一个 Map 记录已经遍历过的数字及其对应的索引值。这样当遍历一个新数字的时候去 Map 里查询,target 与该数的差值是否已经在前面的数字中出现过。如果出现过,那么已经得出答案,就不必再往下执行了。
/**
@param {number[]} nums
@param {number} target
@return {number[]}
*/
const twoSum = function (nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const diff = target - nums[i];
if (map.has(diff)) {
return [map.get(diff), i];
}
map.set(nums[i], i);
}
}
写出执行结果,并解释原因
3.toString() 3..toString() 3…toString()
// 答案与解析
报错 "3" 报错
运算符优先级问题
点运算符会被优先识别为数字常量的一部分,然后才是对象属性访问符
在JavaScript中,3.1,3.,.1都是合法的数字
3.toString() 会被JS引擎解析成 (3.)toString() 报错
3..toString() 会被JS引擎解析成 (3.).toString() "3"
3...toString() 会被JS引擎解析成 (3.)..toString() 报错
写出执行结果,并解释原因
function yideng() {} const a = {}, b = Object.prototype; console.log(a.prototype === b); console.log(Object.getPrototypeOf(a) === b); console.log(yideng.prototype === Object.getPrototypeOf(yideng));
// 答案
false true false
//知识点
proto(隐式原型)与prototype(显式原型)
1)是什么?
①显式原型 explicit prototype property:
每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。(需要注意的是,通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性)
②隐式原型 implicit prototype link:
JavaScript中任意对象都有一个内置属性[[prototype]],在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__来访问。ES5中有了对于这个内置属性标准的Get方法Object.getPrototypeOf()。(注意:Object.prototype 这个对象是个例外,它的__proto__值为null)
③二者的关系:
隐式原型指向创建这个对象的函数(constructor)的prototype
2)作用是什么?
①显式原型的作用:用来实现基于原型的继承与属性的共享。
②隐式原型的作用:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找。
3)__proto__的指向:
__proto__的指向到底如何判断呢?根据ECMA定义 'to the value of its constructor’s "prototype" ' ----指向创建这个对象的函数的显式原型。
所以关键的点在于找到创建这个对象的构造函数,接下来就来看一下JS中对象被创建的方式,一眼看过去似乎有三种方式:(1)对象字面量的方式 (2)new 的方式 (3)ES5中的Object.create()。
但是本质上只有一种方式,也就是通过new来创建。为什么这么说呢,首先字面量的方式是一种为了开发人员更方便创建对象的一个语法糖,本质就是 var o = new Object(); o.xx = xx;o.yy=yy;
//解析:
1)a.prototype === b =>false
prototype属性是只有函数才特有的属性,当你创建一个函数时,js会自动为这个函数加上prototype属性,值是一个空对象。而实例对象是没有prototype属性的。所以a.prototype是undefined,第一个结果为false。
2)Object.getPrototypeOf(a) === b =>true
首先要明确对象和构造函数的关系,对象在创建的时候,其__proto__会指向其构造函数的prototype属性
Object实际上是一个构造函数(typeof Object的结果为"function"),使用字面量创建对象和new Object创建对象是一样的,所以a.__proto__也就是Object.prototype,所以Object.getPrototypeOf(a)与a.__proto__是一样的,第二个结果为true
3)yideng.prototype === Object.getPrototypeOf(yideng) =>false
关键点:f.prototype和Object.getPrototypeOf(f)说的不是一回事
①f.prototype 是使用使用 new 创建的 f 实例的原型:
f.prototype === Object.getPrototypeOf(new f()); // true
②Object.getPrototypeOf(f)是 f 函数的原型:
Object.getPrototypeOf(f) === Function.prototype; //true
所以答案是 false
写出执行结果,并解释原因
const lowerCaseOnly = /^[a-z]+$/; console.log(lowerCaseOnly.test(“yideng”)); console.log(lowerCaseOnly.test(null)); console.log(lowerCaseOnly.test());
// 答案
true true true
// 解析
test方法的参数会被调用toString强制转换成字符串
此题转换的字符串是null、undefined
写出执行结果,并解释原因
function captureOne(re, str) { var match = re.exec(str); return match && match[1]; } var numRe = /num=(\d+)/gi, 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);
// 答案
true false true
// 解析
1)exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。
2)但是在Javascript中使用exec进行正则表达式全局匹配时要注意:
①在全局模式下,当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把正则表达式对象的 ②lastIndex 属性设置为匹配文本的最后一个字符的下一个位置。
③这就是说,您可以通过反复调用 exec() 方法来遍历字符串中的所有匹配文本。
④当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。
3)所以在全局模式下,如果在一个字符串中完成了一次模式匹配之后要开始检索新的字符串,就必须手动地把 lastIndex 属性重置为 0。
[手写代码]实现 Promise.all 方法
// 核心思路
①接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
②这个方法返回一个新的 promise 对象,
③遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
④参数所有回调成功才是成功,返回值数组与参数顺序一致
⑤参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
// 实现代码
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了~
//代码实现
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if(!Array.isArray(promises)){
throw new TypeError(argument must be a array)
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value=>{
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter == promiseNum) {
return resolve(resolvedResult)
}
},error=>{
return reject(error)
})
}
})
}
// test
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1)
}, 1000)
})
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(2)
}, 2000)
})
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(3)
}, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
console.log(res) // [3, 1, 2]
})
有效括号算法题
/* 给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效 有效字符串需满⾜:
1. 左括号必须⽤相同类型的右括号闭合。
2. 左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。 示例1: 输⼊: “()” 输出: true 示例2: 输⼊: “()[]{}” 输出: true 示例 3: 输⼊: “(]” 输出: false 示例 4: 输⼊: “([)]” 输出: false 示例 5: 输⼊: “{[]}” 输出: true */
var isValid = function(s) {
const stack = [],
map = {
"(":")",
"{":"}",
"[":"]"
}
for(const x of s) {
if(x in map) {
stack.push(x)
continue
}
if(map[stack.pop()] !== x) return false
}
return !stack.length
}
写出执行结果,并解释原因
function yideng(n, o) { console.log(o); // ? return { yideng: function (m) { return yideng(m, n); }, }; } const a = yideng(0); a.yideng(1); a.yideng(2); a.yideng(3); const b = yideng(0).yideng(1).yideng(2).yideng(3); const c = yideng(0).yideng(1); c.yideng(2); c.yideng(3);
// 答案
undefined 0 0 0
undefined 0 1 2
undefined 0 1 1
// 解析
闭包知识考查
return返回的对象的fun属性对应一个新建的函数对象,这个函数对象将形成一个闭包作用域,使其 能够访问外层函数的变量n及外层函数fun,
关键点:
理清执行的是哪个yideng函数,为了不将yideng函数与yideng属性混淆,等价转换下代码
function yideng(n,o){
console.log(o);
return {
yideng:function(m){
return yideng(m,n);
}
}
}
const a=yideng(0);a.yideng(1);a.yideng(2);a.yideng(3);
const b=yideng(0).yideng(1).yideng(2).yideng(3);
const c = yideng(0).yideng(1).yideng(2);c.yideng(3);
1)第一行a代码执行过程解析,
①const a=yideng(0);调用最外层的函数,只传入了n,所以打印o是undefined
②a.yideng(1);调用yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数_yideng_(1,0);所以o为0;
③a.yideng(2);调用yideng(2)时m为2,但依然是调用a.yideng,所以还是闭包了第一次调用时的n,所以内部调用第一层的_yideng_(2,0);所以o为0
④a.yideng(3);同③
所以是undefined 0 0 0
2)第二行b代码执行过程解析
①第一次调用第一层_yideng_(0)时,o为undefined;
②第二次调用 .yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数_yideng_(1,0);所以o为0;
③第三次调用 .yideng(2)时m为2,此时当前的yideng函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层_yideng_(1,0)时,n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层_yideng_函数_yideng_(2,1),所以o为1;
④第四次调用 .yideng(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层_yideng_函数为_yideng_(3,2);所以o为2;
所以是undefined 0 1 2
3)第三行c代码执行过程解析
①在第一次调用第一层_yideng_(0)时,o为undefined;
②第二次调用 .yideng(1)时m为1,此时yideng闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层_yideng_函数fun(1,0);所以o为0;
③第三次调用 .yideng(2)时m为2,此时yideng闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层_yideng_函数_yideng_(2,1);所以o为1;
④第四次.yideng(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数_yideng_(3,1),所以o还为1
所以是undefined 0 1 1
写出执行结果,并解释原因
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));
// 答案
3 ["a","b","c"] 3 ["a","b","c"]
//解析
这个题其实主要就是考察的reverse会返回该数组的引用,但是容易被迷惑,导致答错,如果知道这个点,就不会掉坑里了。
1)reverse
MDN 上对于 reverse() 的描述是酱紫的:
The reverse method transposes the elements of the calling array object in place, mutating the array, and returning a reference to the array.
reverse 方法颠倒数组中元素的位置,改变了数组,并返回该数组的引用。
2)slice
slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
写出执行结果,并解释原因
var F = function () {}; Object.prototype.a = function () { console.log(“yideng”); }; Function.prototype.b = function () { console.log(“xuetang”); }; var f = new F(); F.a(); F.b(); f.a(); f.b();
var F = function () {};
Object.prototype.a = function () {
console.log("yideng");
};
Function.prototype.b = function () {
console.log("xuetang");
};
var f = new F();
F.a();
F.b();
f.a();
f.b();
补充代码,使代码可以正确执行
const str = “1234567890”; function formatNumber(str) { // your code } console.log(formatNumber(str)); //1,234,567,890 // 补充代码,使代码可以正确执行
// 1
function formatNum(num){
const arr = `${num}`.split('');
return arr.reduceRight((pre, cur, rightIndex) => {
const index = arr.length-1 - rightIndex;
if ((index+1)%3 !== 0 || index === arr.length -1 ) {
pre.unshift(cur)
} else {
pre.unshift(cur);
pre.unshift(',');
}
return pre
},[]).join('')
}
//2
function numFormat(num) {
return num.toString().replace(/(\d)(?=(?:\d{3})+(\.[0-9]+)*$)/g, ($0, $1, $2) => $2 ? $1 + ',' : $1)
}
console.log(numFormat(12345678.3123))
Script 放在底部还会影响 dom 的解析和渲染吗?Script 内部的代码执行会等待 css 加载完吗?css 加载会影响 DOMContentLoaded 么?
<!ODCTYPE html>
<link href=”https://cdn/css/bootstrap.css” ref=”stylesheet” />京程一灯
写出下面代码 null 和 0 进行比较的代码执行结果,并解释原因
console.log(null == 0); console.log(null <= 0); console.log(null < 0);
null>0 //null转化为number,为0,所以0>0结果为false。
null>=0 //null转化为number,为0>=0,所以结果为true。
null==0// null在做相等判断时,不进行转型,所以null和0为不同类型数据,结果为false
关于数组 sort,下面代码的正确打印结果是什么,并解释原因
const arr1 = [“a”, “b”, “c”]; const arr2 = [“b”, “c”, “a”]; console.log( arr1.sort() === arr1, arr2.sort() == arr2, arr1.sort() === arr2.sort() );
答案: true true false
原因: 1. sort()会改变原数组, 因此, 前两个的答案为true
2. 因为arr1和arr2是数组, 属于引用类型, 二者存放的地址不同, 所以即便sort之后内部的值相同, 但是由于地址不同,因此结果为false
介绍防抖与节流的原理,并动手实现
// 京程一灯,每日一题 const debounce = (fn, delay) => { // 介绍防抖函数原理,并实现 // your code }; const throttle = (fn, delay = 500) => { // 介绍节流函数原理,并实现 // your code };
关于隐式转换,下面代码的执行结果是什么?并解释原因
let a = []; let b = “0”; console.log(a == 0); console.log(a == !a); console.log(b == 0); console.log(a == b);
let a = [];
let b = "0";
console.log("" == 0); // true
console.log("" == false); // true
console.log("0" == 0); // true
console.log("" == "0"); // false
请写出如下代码的打印结果
var obj = {}; var x = +obj.yideng?.name ?? “京程一灯”; console.log(x);
?. 会返回undefined, 因为前面有个 + , 转化为 NaN
对于 length 下面代码的输出结果是什么?并解释原因
function foo() { console.log(length); } function bar() { var length = “京程一灯”; foo(); } bar();
输出结果是0,因为foo函数是由window对象调用,打印的length是window对象下的length属性0。foo只是在bar函数内部调用,并不是在bar函数内部声明,所以无法获取到bar函数声明的length变量
答案:0
foo 由window对象调用,length无法访问到bar函数的变量,window.length为0
对于扩展运算符,下面代码的执行结果是什么?并解释原因
let ydObject = { …null, …undefined }; console.log(ydObject); let ydArray = […null, …undefined]; console.log(ydArray);
let ydObject = { ...null, ...undefined };
console.log(ydObject);//Object { } 空对象
let ydArray = [...null, ...undefined];
console.log(ydArray);//报错
第一个打印一个空对象
第二个打印报错,是因为在数组和函数中使用展开语法时,只能用于可迭代对象
写出类数组转换结果,并解释原因
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));
[0, 1, undefined, 3]
[0, 1, undefined, 3]
Array.from(伪数组对象, 可迭代对象),Array.prototype.slice.call(伪数组对象)能把类数组对象转化成数组,arrLike规定了长度4的数组。
写出下面代码 1,2,3 的大小判断结果
console.log(1 < 2 < 3); console.log(3 > 2 > 1);
console.log(true < 3);
console.log(true > 1);
以下两段代码会抛出异常吗?解释原因?
let yd = { x: 1, y: 2 }; // 以下两段代码会抛出异常吗? let ydWithXGetter1 = { …yd, get x() { throw new Error(); }, };
let ydWithXGetter2 = { …yd, …{ get x() { throw new Error(); }, }, };
请问 React 调用机制一共对任务设置了几种优先级别?每种优先级都代表的具体含义是什么?在你开发过程中如果遇到影响主 UI 渲染卡顿的任务,你又是如何利用这些优先级的?
Vue 父组件可以监听到子组件的生命周期吗?如果能请写出你的实现方法。
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
Vue.set(object, propertyName, value); vm.$set(object, propertyName, value);
答案
1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题
Vue使用了Object.defineProperty实现双向数据绑定
在初始化实例时对属性执行 getter/setter 转化
属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加或删除)
所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,
最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
既然 Vue 通过数据劫持可以精准探测数据在具体 dom 上的变化,为什么还需要虚拟 DOM diff 呢?
// 1
假设有一个 input ,通过 v-model 双向绑定了 data.form.value ,当 data.form.value 的 setter 触发时,直接操作 dom:input.value = mValue 就行了,为啥还需要 vdom ?
答案:
1)应用不可能只有表单控件值改变,还有其他的元素的改动,难道也要在 setter 里面自己做吗?MVVM 重回 MVC
2)vdom 实现的批量 dom 更新可以提供一个可靠的 dom 改动性能下限
3)代码维护性天壤之别
// 2
我觉得双向数据绑定只是检测数据data的变化,而虚拟DOM是真正实现DOM更新的,这两个东西的职责完全不同的,缺一不可。所以不存在为什么还要用Vdom diff.
// 3
前置知识: 依赖收集、虚拟 DOM、响应式系统
现代前端框架有两种方式侦测变化,一种是 pull ,一种是 push
pull: 其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用setStateAPI显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。
push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知。因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher(具体如何创建的Watcher可以先了解下Vue双向数据绑定的原理如下图)
一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的。
Vue 组件中写 name 选项有除了搭配 keep-alive 还有其他作用么?你能谈谈你对 keep-alive 了解么?(平时使用和源码实现方面)
答案
一、组件中写 name 选项有什么作用?
项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
DOM 做递归组件时需要调用自身 name
vue-devtools 调试工具里显示的组见名称是由vue中组件name决定的
二、keep-alive使用
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染
一般结合路由和动态组件一起使用,用于缓存组件;
提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
三、keep-alive实现原理
1)首先看下源码
// 源码位置:src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件实例数量上限
},
created () {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的健集合
},
destroyed () {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
// .....
}
}
大概的分析源码,我们发现与我们定义组件的过程一样,先是设置组件名为keep-alive,其次定义了一个abstract属性,值为true。这个属性在vue的官方教程并未提及,其实是一个虚组件,后面渲染过程会利用这个属性。props属性定义了keep-alive组件支持的全部参数。
2)接下来重点就是keep-alive在它生命周期内定义了三个钩子函数了
created
初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合
destroyed
删除缓存VNode还要对应执行组件实例的destory钩子函数。
删除this.cache中缓存的VNode实例。不是简单地将this.cache置为null,而是遍历调用pruneCacheEntry函数删除。
// src/core/components/keep-alive.js
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy() // 执行组件的destory钩子函数
}
cache[key] = null
remove(keys, key)
}
mounted
在mounted这个钩子中对include和exclude参数进行监听,然后实时地更新(删除)this.cache对象数据。pruneCache函数的核心也是去调用pruneCacheEntry。
3)render
// src/core/components/keep-alive.js
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// check pattern
const name: ?string = getComponentName(componentOptions) // 组件名
const { include, exclude } = this
if ( // 条件匹配
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null // 定义组件的缓存key
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) { // 已经缓存过该组件
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key) // 调整key排序
} else {
cache[key] = vnode // 缓存组件对象
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
// 超过缓存数限制,将第一个删除(LRU缓存算法)
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。
第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。
最后就是再次渲染执行缓存和对应钩子函数了
说一下 React Hooks 在平时开发中需要注意的问题和原因?
Promise.all 中任何一个 Promise 出现错误的时候都会执行 reject,导致其它正常返回的数据也无法使用。你有什么解决办法么?
1)在单个的catch中对失败的promise请求做处理
2)把reject操作换成 resolve(new Error("自定义的error"))
3)引入Promise.allSettled
const promises = [
fetch('/api1'),
fetch('/api2'),
fetch('/api3'),
];
Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));
// "fulfilled"
// "fulfilled"
// "rejected"
4)安装第三方库 promise-transaction
// 它是promise事物实现 不仅仅能处理错误还能回滚
import Transaction from 'promise-transaction';
const t = new Transaction([
{
name: 'seed',
perform: () => Promise.resolve(3),
rollback: () => false,
retries: 1, // optionally you can define how many retries you like to run if initial attemp fails for this step
},
{
name: 'square',
perform: (context) => {
return Promise.resolve(context.data.seed * context.data.seed);
},
rollback: () => false,
},
]);
return t.process().then((result) => {
console.log(result); // should be value of 9 = 3 x 3
});
请能尽可能多的说出 Vue 组件间通信方式?在组件的通信中 EventBus 非常经典,你能手写实现下 EventBus 么?
一、Vue组件通信方式
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练
Vue 组件间通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信
下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
1.props / $emit
适用于父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
2.ref 与 $parent / $children
适用于父子组件通信
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$parent / $children:访问父 / 子实例
3.EventBus ($emit / $on)
适用于父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
4.$attrs/$listeners
适用于隔代组件通信
$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
5.provide / inject
适用于隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
6.Vuex
适用于父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
二、手写实现简版EventBus
// 组件通信,一个触发与监听的过程
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)
})
// 触发事件
emitter.emit('ages', 18) // 18
请讲一下 react-redux 的实现原理?
1.Provider
Provider的作用是从最外部封装了整个应用,并向connect模块传递store
2.connect
负责连接React和Redux
1)获取state
connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
2)包装原组件
将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent
3)监听store tree变化
connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染
写出下面代码的执行结果,并解释原因
Object.prototype.yideng = “京程一灯”; var a = 123; a.b = 456; console.log(a.yideng); console.log(a.b);
console.log(a.yideng); // 京程一灯
console.log(a.b) ; //undefined
解释
var a // 声明提升
Object.prototype.yideng = "京程一灯"; // 向对象添加属性,此时a对象身上有此属性
a = 123; //赋值,a为数字类型
a.b = 456; // 数字类型无法这样赋值,所以打印undefined
解析:
var 定义赋值 a 的时候, a 的变量提升成为了 Number类型;Number本身就是一个对象,可以继承 Object 的属性,因此 a.proto 是存在 yideng 这个键的;a 本身作为 Number 类型的值,是不可以设置 键的,所以 a.b 是不存在的
React 中 setState 后发生了什么?setState 为什么默认是异步?setState 什么时候是同步?
一、React中setState后发生了什么
在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。
经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。
在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
二、setState 为什么默认是异步
假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnode diff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。
三、setState 什么时候是同步
在setTimeout或者原生事件中,setState是同步的。
哪些方法会触发 react 重新渲染?重新渲染 render 会做些什么?
一、哪些方法会触发 react 重新渲染?
1.setState() 方法被调用
setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候一定会重新渲染吗?
答案是不一定。当 setState 传入 null 的时候,并不会触发 render。
class App extends React.Component {
state = {
a: 1
};
render() {
console.log("render");
return (
<React.Fragement>
<p>{this.state.a}</p>
<button
onClick={() => {
this.setState({ a: 1 }); // 这里并没有改变 a 的值
}}
>
Click me
</button>
<button onClick={() => this.setState(null)}>setState null</button>
<Child />
</React.Fragement>
);
}
}
2.父组件重新渲染
只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render。
3.forceUpdate()
默认情况下,当组件的state或props改变时,组件将重新渲染。如果你的render()方法依赖于一些其他的数据,你可以告诉React组件需要通过调用forceUpdate()重新渲染。
调用forceUpdate()会导致组件跳过shouldComponentUpdate(),直接调用render()。这将触发组件的正常生命周期方法,包括每个子组件的shouldComponentUpdate()方法。
forceUpdate就是重新render。有些变量不在state上,当时你又想达到这个变量更新的时候,刷新render;或者state里的某个变量层次太深,更新的时候没有自动触发render。这些时候都可以手动调用forceUpdate自动触发render
二、重新渲染 render 会做些什么?
会对新旧 VNode 进行对比,也就是我们所说的DoM diff。
对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
遍历差异对象,根据差异的类型,根据对应对规则更新VNode
React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法 ,但是这个过程仍然会损耗性能。
三、总结
React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以我们的业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。
这里提下优化的点
1.shouldComponentUpdate 和 PureComponent
在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。
2.利用高阶组件
在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能
3.使用 React.memo
React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件 。
4.合理拆分组件
微服务的核心思想是:以更轻、更小的粒度来纵向拆分应用,各个小应用能够独立选择技术、发展、部署。我们在开发组件的过程中也能用到类似的思想。试想当一个整个页面只有一个组件时,无论哪处改动都会触发整个页面的重新渲染。在对组件进行拆分之后,render 的粒度更加精细,性能也能得到一定的提升。
有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行3次,再次执行无效,如何实现
function sayHi() {
console.log('hi')
}
function threeTimes(fn) {
let times = 0
return () => {
if (times++ < 3) {
fn()
}
}
}
const newFn = threeTimes(sayHi)
newFn()
newFn()
newFn()
newFn()
newFn() // 后面两次执行都无任何反应
通过闭包变量 `times` 来控制函数的执行
实现add函数,让add(a)(b)和add(a,b)两种调用结果相同
// 1
function add(a, b) {
if (b === undefined) {
return function(x) {
return a + x
}
}
return a + b
}
// 2
function curry(fn, ...args1) {
// length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。
if (fn.length == args1.length) {
return fn(...args1)
}
return function(...args2) {
return curry(fn, ...args1, ...args2)
}
}
function add(a, b) {
return a + b
}
console.log(curry(add, 1)(2)) // 3
console.log(curry(add, 1, 2)) // 3
5个fetch请求,请求完成后要求立即执行,但最终的输出顺序要按照要求输出ABCDE
function run(fetchs = []) {
return new Promise((resolve, reject) => {
const result = new Array(fetchs.length).fill(null)
fetchs.forEach((fetch, i) => {
fetch
.then(res => {
result[i] = res
if (!result.includes(null)) {
resolve(result)
}
})
.catch(err => {
reject(err)
})
})
})
}
function delay(str) {
return new Promise(resolve => {
setTimeout(() => {
resolve(str)
}, Math.random() * 1000)
})
}
run([delay('A'), delay('B'), delay('C'), delay('D'), delay('E')]).then(res => {
console.log(res) // ["A", "B", "C", "D", "E"]
})