社区精选面试题

152 阅读21分钟

['1', '2', '3'].map(parseInt) what & why ?

<!-- 首先让我们回顾一下,map函数的第一个参数callback -->

var new_array = arr.map(function callback(currentValue[, index[, array]]) { 
	// Return element for new_array 
}[, thisArg])

<!-- 这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。 -->

<!-- 而parseInt则是用来解析字符串的,使字符串成为指定基数的整数。 -->

parseInt(string, radix)

<!-- 接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。 -->

<!-- 了解这两个函数后,我们可以模拟一下运行情况 -->

['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})
parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN
parseInt('3', 2) //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN

<!-- map函数返回的是一个数组,所以最后结果为[1, NaN, NaN] -->

Number() & parseInt() & parseFloat()

写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

1. 官网推荐推荐的使用key,应该理解为“使用唯一id作为key2. key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的
3. 正是因为带唯一key时每次更新都不能找到可复用的节点,不但要销毁和创建vnode,在DOM里添加移除节点对性能的影响更大。所以会才说“不带key可能性能更好”

什么是防抖和节流?有什么区别?如何实现?

<!-- 防抖 -->
<!-- 触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间 -->
<!-- 思路:每次触发事件时都取消之前的延时调用方法 -->

function debounce(fn) {
	let timeout = null; // 创建一个标记用来存放定时器的返回值
	return function () {
		clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
		timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
			fn.apply(this, arguments);
		}, 500);
	};
}
function sayHi() {
	console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖

<!-- 节流 -->
<!-- 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率 -->
<!-- 思路:每次触发事件时都判断当前是否有等待执行的延时函数 -->

function throttle(fn) {
	let canRun = true; // 通过闭包保存一个标记
	return function () {
		if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
		canRun = false; // 立即设置为false
		setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
			fn.apply(this, arguments);
			// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
			canRun = true;
		}, 500);
	};
}
function sayHi(e) {
	console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

  • Set 和 Map 主要的应用场景在于数据重组数据储存
  • Set 是一种叫做 集合 的数据结构
  • Map 是一种叫做 字典 的数据结构

1. 集合(Set)

  • ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。
  • Set 本身是一种构造函数,用来生成 Set 数据结构
<!-- 举个例子: -->

const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]
  • Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。
  • 向 Set 加入值的时候,不会发生类型转换
  • Set 实例属性
    • constructor: 构造函数
    • size:元素数量
      let set = new Set([1, 2, 3, 2, 1])
      console.log(set.length)	// undefined
      console.log(set.size)	// 3
      
  • Set 实例方法 -- 操作方法
    • add(value):新增,相当于 array里的push
    • delete(value):存在即删除集合中value
    • has(value):判断集合中是否存在 value
    • clear():清空集合
      let set = new Set()
      set.add(1).add(2).add(1)
      set.has(1)	// true
      set.has(3)	// false
      set.delete(1)	
      set.has(1)	// false
      
      Array.from 方法可以将 Set 结构转为数组
      const items = new Set([1, 2, 3, 2])
      const array = Array.from(items)
      console.log(array)	// [1, 2, 3]
      // 或
      const arr = [...items]
      console.log(arr)	// [1, 2, 3]
      
  • Set 实例方法 -- 遍历方法(遍历顺序为插入顺序)
    • keys():返回一个包含集合中所有键的迭代器
    • values():返回一个包含集合中所有值得迭代器
    • entries():返回一个包含Set对象中所有元素得键值对迭代器
    • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值
      let set = new Set([1, 2, 3])
      console.log(set.keys())	// SetIterator {1, 2, 3}
      console.log(set.values())	// SetIterator {1, 2, 3}
      console.log(set.entries())	// SetIterator {1, 2, 3}
      
      for (let item of set.keys()) {
      	console.log(item);
      }	// 1	2	 3
      for (let item of set.entries()) {
      	console.log(item);
      }	// [1, 1]	[2, 2]	[3, 3]
      
      set.forEach((value, key) => {
      		console.log(key + ' : ' + value)
      })	// 1 : 1	2 : 2	3 : 3
      console.log([...set])	// [1, 2, 3]
      
      Set 可默认遍历,默认迭代器生成函数是 values() 方法
      Set.prototype[Symbol.iterator] === Set.prototype.values	// true
      
      所以, Set可以使用 map、filter 方法
      let set = new Set([1, 2, 3])
      set = new Set([...set].map(item => item * 2))
      console.log([...set])	// [2, 4, 6]
      
      set = new Set([...set].filter(item => (item >= 4)))
      console.log([...set])	//[4, 6]
      
      因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)
      let set1 = new Set([1, 2, 3])
      let set2 = new Set([4, 3, 2])
      
      let intersect = new Set([...set1].filter(value => set2.has(value)))
      let union = new Set([...set1, ...set2])
      let difference = new Set([...set1].filter(value => !set2.has(value)))
      
      console.log(intersect)	// Set {2, 3}
      console.log(union)		// Set {1, 2, 3, 4}
      console.log(difference)	// Set {1}
      

2. WeakSet

  • WeakSet 对象允许你将弱引用对象储存在一个集合中
  • WeakSet 与 Set 的区别:
    • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
    • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素
  • 属性:
    • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数
  • 方法:
    • add(value):在WeakSet 对象中添加一个元素value
    • has(value):判断 WeakSet 对象中是否包含value
    • delete(value):删除元素 value
    • clear():清空所有元素,注意该方法已废弃
      var ws = new WeakSet()
      var obj = {}
      var foo = {}
      
      ws.add(window)
      ws.add(obj)
      
      ws.has(window)	// true
      ws.has(foo)	// false
      
      ws.delete(window)	// true
      ws.has(window)	// false
      

3. 字典(Map)

  • 集合 与 字典 的区别:
    • 共同点:集合、字典 可以储存不重复的值
    • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)	// content

m.has(o)	// true
m.delete(o)	// true
m.has(o)	// false
  • 任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,例如:
const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
  • 如果读取一个未知的键,则返回undefined。
new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined
  • 上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

  • 由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

  • 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123
  • Map 的属性
    • constructor:构造函数
    • size:返回字典中所包含的元素个数
const map = new Map([
  ['name', 'An'],
  ['des', 'JS']
]);

map.size // 2
  • Map 的操作方法
    • set(key, value):向字典中添加新元素
    • get(key):通过键查找特定的数值并返回
    • has(key):判断字典中是否存在键key
    • delete(key):通过键 key 从字典中移除对应的数据
    • clear():将这个字典中的所有元素删除
  • Map 的遍历方法
    • Keys():将字典中包含的所有键名以迭代器形式返回
    • values():将字典中包含的所有数值以迭代器形式返回
    • entries():返回所有成员的迭代器
    • forEach():遍历字典的所有成员
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())	// MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}
  • Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
map[Symbol.iterator] === map.entries
// true
  • Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

    对于 forEach ,看一个例子

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

let map = new Map([
    ['name', 'An'],
    ['des', 'JS']
])
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

与其他数据结构的相互转换

  • Map 转 Array
const map = new Map([[1, 1], [2, 2], [3, 3]])
console.log([...map])	// [[1, 1], [2, 2], [3, 3]]
  • Array 转 Map
const map = new Map([[1, 1], [2, 2], [3, 3]])
console.log(map)	// Map {1 => 1, 2 => 2, 3 => 3}
  • Map 转 Object

因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。

function mapToObj(map) {
    let obj = Object.create(null)
    for (let [key, value] of map) {
        obj[key] = value
    }
    return obj
}
const map = new Map().set('name', 'An').set('des', 'JS')
mapToObj(map)  // {name: "An", des: "JS"}
  • Object 转 Map
function objToMap(obj) {
    let map = new Map()
    for (let key of Object.keys(obj)) {
        map.set(key, obj[key])
    }
    return map
}

objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
  • Map 转 JSON
function mapToJson(map) {
    return JSON.stringify([...map])
}

let map = new Map().set('name', 'An').set('des', 'JS')
mapToJson(map)	// [["name","An"],["des","JS"]]
  • JSON 转 Map
function jsonToStrMap(jsonStr) {
  return objToMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}

4. WeakMap

  • WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。
  • 注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
  • WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。
  • 属性:
    • constructor:构造函数
  • 方法:
    • has(key):判断是否有 key 关联对象
    • get(key):返回key关联对象(没有则则返回 undefined)
    • set(key):设置一组key关联对象
    • delete(key):移除 key 的关联对象
      let myElement = document.getElementById('logo');
      let myWeakmap = new WeakMap();
      
      myWeakmap.set(myElement, {timesClicked: 0});
      
      myElement.addEventListener('click', function() {
      	let logoData = myWeakmap.get(myElement);
      	logoData.timesClicked++;
      }, false);
      

5. 总结

  • Set
    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet
    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map
    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap
    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete

6. 扩展:Object与Set、Map

  • Object 与 Set
// Object
const properties1 = {
    'width': 1,
    'height': 1
}
console.log(properties1['width']? true: false) // true

// Set
const properties2 = new Set()
properties2.add('width')
properties2.add('height')
console.log(properties2.has('width')) // true
  • Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应

介绍下深度优先遍历和广度优先遍历,如何实现?

从dom节点的遍历来理解这个问题,我将用深度优先遍历和广度优先遍历对这个dom树进行查找

  • 深度优先遍历
/*深度优先遍历三种方式*/
let deepTraversal1 = (node, nodeList = []) => {
  if (node !== null) {
    nodeList.push(node)
    let children = node.children
    for (let i = 0; i < children.length; i++) {
      deepTraversal1(children[i], nodeList)
    }
  }
  return nodeList
}
let deepTraversal2 = (node) => {
    let nodes = []
    if (node !== null) {
      nodes.push(node)
      let children = node.children
      for (let i = 0; i < children.length; i++) {
        nodes = nodes.concat(deepTraversal2(children[i]))
      }
    }
    return nodes
  }
// 非递归
let deepTraversal3 = (node) => {
  let stack = []
  let nodes = []
  if (node) {
    // 推入当前处理的node
    stack.push(node)
    while (stack.length) {
      let item = stack.pop()
      let children = item.children
      nodes.push(item)
      // node = [] stack = [parent]
      // node = [parent] stack = [child3,child2,child1]
      // node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
      // node = [parent, child1-1] stack = [child3,child2,child1-2]
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push(children[i])
      }
    }
  }
  return nodes
}
  • 广度优先遍历
let widthTraversal2 = (node) => {
  let nodes = []
  let stack = []
  if (node) {
    stack.push(node)
    while (stack.length) {
      let item = stack.shift()
      let children = item.children
      nodes.push(item)
        // 队列,先进先出
        // nodes = [] stack = [parent]
        // nodes = [parent] stack = [child1,child2,child3]
        // nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
        // nodes = [parent,child1,child2]
      for (let i = 0; i < children.length; i++) {
        stack.push(children[i])
      }
    }
  }
  return nodes
}

ES5/ES6 的继承除了写法以外还有什么区别

  • class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
const bar = new Bar(); // it's ok
function Bar() {
  this.bar = 42;
}

const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
  constructor() {
    this.foo = 42;
  }
}
  • class 声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
  baz = 42; // it's ok
}
const bar = new Bar();

class Foo {
  constructor() {
    fol = 42; // ReferenceError: fol is not defined
  }
}
const foo = new Foo();
  • class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
// 引用一个未声明的变量
function Bar() {
  this.bar = 42;
}
Bar.answer = function() {
  return 42;
};
Bar.prototype.print = function() {
  console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']

class Foo {
  constructor() {
    this.foo = 42;
  }
  static answer() {
    return 42;
  }
  print() {
    console.log(this.foo);
  }
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
  • class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function() {
  console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
  • 必须使用 new 调用 class。
function Bar() {
  this.bar = 42;
}
const bar = Bar(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
  • class 内部无法重写类名。
function Bar() {
  Bar = 'Baz'; // it's ok
  this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}  

class Foo {
  constructor() {
    this.foo = 42;
    Foo = 'Fol'; // TypeError: Assignment to constant variable
  }
}
const foo = new Foo();
Foo = 'Fol'; // it's ok

多维数组(扁平 + 去重 + 排序)

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})

(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点

1. 回调函数(callback)

  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return

    回调地狱的根本问题在于: 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转) 嵌套函数过多的多话,很难处理错误

  • 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

2. Promise

Promise就是为了解决callback的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

  • 优点:解决了回调地狱的问题
  • 缺点:无法取消 Promise ,错误需要通过回调函数来捕获

3. Generator

  • 特点:可以控制函数的执行,可以配合 co 函数库使用
    function *fetch() {
    		yield ajax('XXX1', () => {})
    		yield ajax('XXX2', () => {})
    		yield ajax('XXX3', () => {})
    }
    let it = fetch()
    let result1 = it.next()
    let result2 = it.next()
    let result3 = it.next()
    

4. Async/await

async、await 是异步的终极解决方案

  • 优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

情人节福利题,如何实现一个 new

function _new(fn, ...arg) {
    const obj = Object.create(fn.prototype);
    const ret = fn.apply(obj, arg);
    return ret instanceof Object ? ret : obj;
}

(微医)React 中 setState 什么时候是同步的,什么时候是异步的?

【React的更新机制】

  • 生命周期函数和合成事件中:

    • 无论调用多少次setState,都不会立即执行更新。而是将要更新的state存入'_pendingStateQuene',将要更新的组件存入'dirtyComponent';
    • 当根组件didMount后,批处理机制更新为false。此时再取出'_pendingStateQuene'和'dirtyComponent'中的state和组件进行合并更新;
  • 原生事件和异步代码中:

    • 原生事件不会触发react的批处理机制,因而调用setState会直接更新;
    • 异步代码中调用setState,由于js的异步处理机制,异步代码会暂存,等待同步代码执行完毕再执行,此时react的批处理机制已经结束,因而直接更新。

总结: react会表现出同步和异步的现象,但本质上是同步的,是其批处理机制造成了一种异步的假象。(其实完全可以在开发过程中,在合成事件和生命周期函数里,完全可以将其视为异步)

React setState 笔试题,下面的代码输出什么?

<!-- 提问 -->

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};
<!-- 解答 -->

1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。

2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。

3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。

输出: 0 0 2 3

判断数组的方法

  • Object.prototype.toString.call()

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。

const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

<!-- 这种方法对于所有基本的数据类型都能进行判断,即使是 nullundefined 。 -->

Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
Object.prototype.toString.call() 常用于判断浏览器内置对象时。

  • instanceof

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。

[]  instanceof Array; // true
<!-- 但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。 -->

[]  instanceof Object; // true
  • Array.isArray()

功能:用来判断对象是否为数组

当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
Array.isArray() 与 Object.prototype.toString.call()

Array.isArray( )是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

介绍前端模块化发展历程

模块化的概念:主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

  • IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。

    (function(){
    	return {
    	data:[]
    	}
    })()
    
  • AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。

    define('./index.js',function(code){
    	// code 就是index.js 返回的内容
    })
    
  • CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。

    define(function(require, exports, module) {  
    	var indexCode = require('./index.js');
    });
    
  • CommonJS: nodejs 中自带的模块化。

    var fs = require('fs');
    
  • UMD:兼容AMD,CommonJS 模块化语法。

  • webpack(require.ensure):webpack 2.x 版本中的代码分割。

  • ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。

    import a from 'a';
    

全局作用域中,使用 let 和 const 声明的变量不在window下,那存在哪里?怎么获取?

<!-- 全局 -->

let a = 1
const b = 2

<!-- 不像 var 一样,此时使用window.a & window.b 是取不到值的-->
  • 在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是在一个块级作用域(Script)中

  • 怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window

说明一下 token 比 cookie 的优势

  • 可以防止CSRF,CSRF攻击的原因是浏览器会自动带上cookie,而浏览器不会自动带上token

下面代码打印什么?为什么?

<!-- 问题 -->

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();
<!-- 答案 -->

// 【打印结果】
ƒ b(){
    b = 20;
    console.log(b); 
}

// 【原因】
// 在自执行匿名函数里,b函数是一个相当于用const定义的常量,内部无法进行重新赋值,如果在严格模式下,会报错
var b = 10;
(function b() {
  'use strict'
  b = 20;
  console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."

(京东)下面代码中 a 在什么情况下会打印 1?

  • 问题
var a = ?;
if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}
  • 答案
<!-- 因为==会进行隐式类型转换 所以我们重写toString方法就可以了 -->

var a = {
  i: 1,
  toString() {
    return a.i++;
  }
}

if( a == 1 && a == 2 && a == 3 ) {
  console.log(1);
}

实现一个 sleep 函数

  • 问题
比如 sleep(1000) 意味着等待1000毫秒,可从 PromiseGeneratorAsync/Await 等角度实现
  • 答案
<!-- 4种方式 -->

// Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
  console.log(1)
})

// Generator
function* sleepGenerator(time) {
  yield new Promise(function(resolve,reject){
    setTimeout(resolve,time);
  })
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})

// async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

// ES5
function sleep(callback,time) {
  if(typeof callback === 'function')
    setTimeout(callback,time)
}

function output(){
  console.log(1);
}
sleep(output,1000);

使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

  • 答案
[102, 15, 22, 29, 3, 8]
  • 解析

根据MDN上对Array.sort()的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的UTF-16编码顺序来进行排序。所以'102' 会排在 '15' 前面。以下是MDN中的解释转中文:

sort()方法对数组中的元素进行适当排序并返回数组。默认排序顺序建立在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列的基础上。

输出以下代码执行的结果并解释为什么

  • 题目
var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
  • 答案
Object(4[empty × 2, 1, 2, splice: ƒ, push: ƒ]

请解释 双向绑定 和 vuex 是否冲突

当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model 会比较棘手

<input v-model="obj.message">

假设这里的 obj 是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,v-model 会试图直接修改 obj.message。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。

解决方案一:用“Vuex 的思维”去解决这个问题。给 input 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法:

// 页面

<input :value="message" @input="updateMessage">
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

// VUEX

mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

解决方案二:双向绑定的计算属性。必须承认,这样做比简单地使用“v-model + 局部状态”要啰嗦得多,并且也损失了一些 v-model 中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:

<input v-model="message">
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

call 和 apply 的区别是什么,哪个性能更好一些

  • Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
  • 第一个参数都是,指定函数体内this的指向;
  • 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
  • call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式,个人理解少了一次解构数组的操作。

(百度)实现 (5).add(3).minus(2),结果输出6的功能

Number.prototype.add = function(n) {
  return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
  return this.valueOf() - n;
};

// 输出6

(5).add(3).minus(2)

Vue3为什么抛弃Object.defineProperty,有什么缺陷?

  • Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  • Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

代码打印

<!-- 题目 -->
var a = {n: 1};
var b = a;
a.x = a = {n: 2} // 解析为两步:a.x = a; a = {n: 2}
console.log(111,a.x) 	
console.log(222,b.x)

<!-- 打印结果 -->
VM9537:5 111 undefined
VM9537:6 222 {n: 2}

某公司 1 到 12 月份的销售额存在一个对象里面

var obj = {1:222, 2:123, 5:888}
// 将obj转换为: [222, 123, null, null, 888, null, null, null, null, null, null, null]

const result = Array.from({ length: 12 }).map((_, index) => obj[index + 1] || null);
console.log(result)

要求设计 LazyMan 类,实现以下功能

  • 题目
<!-- 要求设计 LazyMan 类,实现以下功能 -->

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.taskList = [];
        this.name = name;
        console.log(`Hi I am ${this.name}`);
        setTimeout(() => {
            this.next();
        }, 0);
    }
    eat (name) {
        var that = this;
        var fn = (function (n) {
            return function () {
                console.log(`I am eating ${n}`)
                that.next();
            }
        })(name);
        this.taskList.push(fn);
        return this;
    }
    sleepFirst (time) {
        var that = this;
        var fn = (function (t) {
            return function () {
                setTimeout(() => {
                    console.log(`等待了${t}秒...`)
                    that.next();
                }, t * 1000);  
            }
        })(time);
        this.taskList.unshift(fn);
        return this;
    }
    sleep (time) {
        var that = this
        var fn = (function (t) {
            return function () {
                setTimeout(() => {
                    console.log(`等待了${t}秒...`)
                    that.next();
                }, t * 1000); 
            }
        })(time);
        this.taskList.push(fn);
        return this;
    }
    next () {
        var fn = this.taskList.shift();
        fn && fn();
    }
}
function LazyMan(name) {
    return new LazyManClass(name);
}
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(4).eat('junk food');

分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景

  • 结构

    • display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击,
    • visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击
    • opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击
  • 继承

    • display: none和opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。
    • visibility: hidden:是继承属性,子孙节点消失由于继承了hidden,通过设置visibility: visible;可以让子孙节点显式。
  • 性能

    • displaynone : 修改元素会造成文档回流,读屏器不会读取display: none元素内容,性能消耗较大
    • visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容
    • opacity: 0 : 修改元素会造成重绘,性能消耗较少
  • 联系

    • 它们都能让元素不可见

给定两个数组,计算它们的交集

var arr1 = [1,2,3,6,7,9]
var arr2 = [1,11,3,33,9,99]

var newarr = arr1.filter(item => arr2.includes(item))
console.log(newarr) // [1,3,9]

已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改

  • 题目
<!-- 已知节点,且不可修改 -->

<img src="1.jpg" style="width:480px!important;”>

  • 解答
<!-- 第一种 -->
max-width: 300px;

<!-- 第二种 -->
transform: scale(0.625,0.625)

redux中的reducer为什么必须(最好)是纯函数

首先你得看看文档怎么说reducer的作用的,接收旧的 state 和 action,返回新的 state,您可得瞧好咯,他就是起一个对数据做简单处理后返回state的作用,为什么只起这个作用,这时用设计这个词回答这个问题才恰当,因为redux把reducer设计成只负责这个作用。很白痴的问答对吧,所以题目的答案也就简单了,reducer的职责不允许有副作用,副作用简单来说就是不确定性,如果reducer有副作用,那么返回的state就不确定,举个例子,你的reducer就做了一个value = value + 1这个逻辑,然后返回state为{value},ok,这个过程太jr纯了,然后你可能觉得要加个请求来取得value后再加1,那么你的逻辑就是value = getValue() + 1, getValue是个请求函数,返回一个值,这种情况,退一万步讲,如果你的网络请求这次出错,那么getValue就返回的不是一个数值,value就不确定了,所以return的state你也不确定了,前端UI拿到的数据也不确定了,所以就是这个环节引入了副作用,他娘的redux设计好的规范就被你破坏了,redux就没卵用了。到此为止这个问题回答完了,我没有说什么上面几个jr说的教科书的理论,甚至还加了些脏话。请原谅,这只是戏剧需要。

最后我回答下如何解决这个副作用,实际上也很白痴的问题,这里的请求可以放在reducer之前,你先请求,该做出错处理的就做出错处理,等拿到实际数据后在发送action来调用reducer。这样通过前移副作用的方式,使reducer变得纯洁

模拟实现一个 Promise.finally

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

a.b.c.d 和 a['b']['c']['d'],哪个性能更高?

应该是 a.b.c.d 比 a['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况,再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点

ES6 代码转成 ES5 代码的实现思路是什么

ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:

  • 解析:解析代码字符串,生成 AST;
  • 转换:按一定的规则转换、修改 AST;
  • 生成:将修改后的 AST 转换成普通代码。

如果不用工具,纯人工的话,就是使用或自己写各种 polyfill 了。

代码输出

<!-- 题目 1 -->
 var a = 123;
 var a = function (){}
 console.log(a)
<!-- 题目 2 -->
 var a = 123;
 function a(){}
 console.log(a)
  • 答案
[Function: a] // 表达式中的两次 var a 提升,两次赋值顺序执行

123	// var a 和 函数声明 都提升,a=123后面执行

如何把一个字符串的大小写取反,例如 ’AbC' 变成 'aBc'

function processString (s) {
    var arr = s.split('');
    var new_arr = arr.map((item) => {
        return item === item.toUpperCase() ? item.toLowerCase() : item.toUpperCase();
    });
    return new_arr.join('');
}
console.log(processString('AbC')); // aBc

什么普通 for 循环的性能远远高于 forEach 的性能?

  • for 循环

    • 没有任何额外的函数调用栈和上下文;
  • forEach函数

    • 实际上是 array.forEach(function(currentValue, index, arr), thisValue)
    • 它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;

使用 JavaScript Proxy 实现简单的数据绑定

<body>
  hello,world
  <input type="text" id="model">
  <p id="word"></p>
</body>

<script>
  const model = document.getElementById("model")
  const word = document.getElementById("word")
  var obj= {};

  const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log('setting',target, key, value, receiver);
        if (key === "text") {
          model.value = value;
          word.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

  model.addEventListener("keyup",function(e){
    newObj.text = e.target.value
  })
</script>

Vue的父组件和子组件生命周期钩子执行顺序是什么

1. 父组建: beforeCreate -> created -> beforeMount
2. 子组件: -> beforeCreate -> created -> beforeMount -> mounted
3. 父组件: -> mounted

总结:从外到内,再从内到外

打印出 1 - 10000 之间的所有对称数 例如 121、1331 等

[...Array(10000).keys()].filter((x) => { 
  return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join('')) 
})

算法题之「移动零」

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

  • 示例:
    • 输入: [0,1,0,3,12]
    • 输出: [1,3,12,0,0]
  • 说明:
    • 必须在原数组上操作,不能拷贝额外的数组。
    • 尽量减少操作次数。
<!-- 解决了有连续的0无法实现功能的问题 -->

function zeroMove(array) {
    let len = array.length;
    let j = 0;
    for(let i=0;i<len-j;i++){
        if(array[i]===0){
            array.push(0);
            array.splice(i,1);
            i --;
            j ++;
        }
    }
    return array;
}

react-router 里的 标签和 标签有什么区别

vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

  • vue本身不做事件代理,每个 span 节点绑定一个 click 事件,并指向同一个事件处理程序
     <div>
      <span 
        v-for="(item,index) of 100000" 
        :key="index" 
        @click="handleClick">
        {{item}}
      </span>
    </div>
    
  • React 中的事件是合成事件,所有事件都委托到 document 了,最后通过addEventListener来监听事件,react-dom 中 合成事件的处理源码:
    // react-dom/src/events/EventListener.js 
    export function addEventBubbleListener(
      element: Document | Element,
      eventType: string,
      listener: Function,
    ): void {
      element.addEventListener(eventType, listener, false);
    }
    
    // 调用处
    addEventBubbleListener(element, getRawEventName(topLevelType)
    

(京东)写出如下代码的打印结果

<!-- 原题 -->

function changeObjProperty(o) {
  o.siteUrl = "http://www.baidu.com"
  o = new Object()
  o.siteUrl = "http://www.google.com"
} 
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
<!-- 解析 -->

// 这里把o改成a
// webSite引用地址的值copy给a了
function changeObjProperty(a) {
  // 改变对应地址内的对象属性值
  a.siteUrl = "http://www.baidu.com"
  // 变量a指向新的地址 以后的变动和旧地址无关
  a = new Object()
  a.siteUrl = "http://www.google.com"
  a.name = 456
} 
var webSite = new Object();
webSite.name = '123'
changeObjProperty(webSite);
console.log(webSite); // {name: 123, siteUrl: 'http://www.baidu.com'}

(京东)请写出如下代码的打印结果

<!-- 原题 -->

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
<!-- 解析 -->

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行

Foo.prototype.a = function() {
    console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3

Foo.a = function() {
    console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4

let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/

obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2

Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1

解释一下这个运算为什么

1/0 // Infinity   

typeof Infinity // 'number'

typeof 1/0 // 为什么等于 NaN
  • 因为typeof优先级高于除法运算,如果是 typeof (1/0) 就会等于 'number' 了