“能做事就可以了”一直是自己找的借口,这样只会让自己在技术的道路上越走越偏。。。

昨天受了点打击,17年毕业的已经远远把自己甩在了后面,发现社区里面活跃的前端大多入行一两年而已。

《前端面试之道》整理一下,自己不是很熟悉,或者是一知半解的,方便以后自己查阅。

微信图片_20190116101648.jpg

微信图片_20190219165004.png

js基础知识点及常考面试题

一直都知道js基本类型是6种,但是不知道还分为原始类型和对象类型,以前也听说过,但是从来都模模糊糊

原始类型

boolean、null、number、undefined、string、symbol
确切的说是原始值,原始类型储存的都是值,是没有函数调用的。

对象类型

在js中,除了原始类型都是对象类型。对象类型和原始类型不同的是,原始类型储存的是值,对象类型储存的是指针。

  1. function test(person) {
  2. person.age = 26
  3. person = {
  4. name: 'yyy',
  5. age: 30
  6. }
  7. return person
  8. }
  9. const p1 = {
  10. name: 'yck',
  11. age: 25
  12. }
  13. console.log(p1)

如果仅仅是这样,大家肯定知道结果,直接打印出来么({name: “yck”, age: 25}),那么大家看在上面加一个

  1. function test(person) {
  2. person.age = 26
  3. person = {
  4. name: 'yyy',
  5. age: 30
  6. }
  7. return person
  8. }
  9. const p1 = {
  10. name: 'yck',
  11. age: 25
  12. }
  13. const p2 = test(p1)
  14. console.log(p1)
  15. console.log(p2)

这一次test函数运行了,p1的地址#001已经传递进函数了,person.age = 26 修改的就是#001,但是下面的person是直接又开了一个地址#002,所以他修改的不生效,但是这个函数返回的是后来的person,就是#002,所以现在的p1是{name: “yck”, age: 26},p2是返回的#002,结果就是{name: “yyy”, age: 30}

typeof vs instanceof

typeof对于原始类型来说,除了null都是可以显示正确的类型
typeof对于对象来说,除了函数都会显示object,所以typeof并不能准确判断变量到底是什么类型
这时候我们考虑instanceof

const Person = function () {}
const p1 = new Person()
p1 instanceof Person // true

const str = 'hello world'
str instanceof String // false

const str = new String('hello world')
str instanceof String // true

对于原始类型来说,你想直接通过instanceof来判断类型是不行的

class PrimitiveString {
    static [symbol.hasInstance](x) {
      return typeof x === 'string'
  }
}
console.log('hello world' instanceof PrimitiveString) // true

类型转换

转Boolean

在条件判断时,除了undefined、null、false、NaN、’’、0、-0、其他所有值都转为true,包括所有对象

对象转原始类型

  • 如果已经是原始类型了,就不需要转换
  • 调用x.valueof(),如果转换为了基础类型,就返回基础类型的值
  • 调用x.toString(),如果转换为了基础类型,就返回基础类型的值
  • 如果没有返回基础类型的值,就会报错

    四则运算符

    加法:

  • 运算符中其中一方为字符砖,那么会把另外一方也转化为字符串

  • 如果一方不是字符串或者数字,那么会将她黄钻换位数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // '41,2,3'
'a' + + 'b' // 'aNaN'

不是加法的,只要一方是数字,另一方就会变成数字

4 * '3' // 12
4 * [] // 0
4 * [1,2] // NaN

this

function foo() {
  console.log(this.a)
}
var a = 1
foo()

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()
  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo函数中的 this 就是 obj 对象
  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

箭头函数

箭头函数是没有this的,bind只取决于第一个参数

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)()

所以这里只能是window
QQ截图20190116114012.jpg

== vs ===

对于==来说,如果对比双方的类型不一样的话,就会进行类型转换

  1. 首先会判断两者类型是否相同,相同的话就是比大小了
  2. 类型不相同的话,那么就会进行类型转换
  3. 会先判断是否存在null和undefined,是的话就会返回true
  4. 判断两者类型是否为string和number,是的话就会将字符串转换为number
  5. 判断其中一方是否为boolean,是的话就会把Boolean转换为number
  6. 判断其中一方是否为object且另一方为string、number、或者symbol,是的话就会把object转为原始类型在进行判断
1 == '1'
      ↓
1 ==  1


'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1


 '1' == { name: 'yck' }
        ↓
'1' == '[object Object]'


[] == ![];
↓
[] == false;    //首先处理布尔值转换, ![] => !true => false
↓
[] == 0;   //右边的布尔值转化为数字0
↓
0 == 0; // 右边的空数组转换为数字0,结果true

QQ截图20190116160354.jpg

闭包

函数A内部有一个函数B,函数B可以访问导函数A中的变量,那么函数B就是闭包

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1
for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

三种方法:

for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}

第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

深浅拷贝

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

浅拷贝

首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我们还可以通过展开运算符 ... 来实现浅拷贝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

深拷贝

JSON.parse(JSON.stringify(object))

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()

当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数

function deepClone(obj) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非对象')
  }

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [...obj] : { ...obj }
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj
}

let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2

ES6 知识点及常考面试题

var let const

这里需要提及的就是let、const并不会变量提示,还有暂时性死区

var a = 1
let b = 1
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined

function test(){
  console.log(a)
  let a
}
test()
VM542:8 Uncaught ReferenceError: a is not defined
    at test (<anonymous>:8:15)
    at <anonymous>:11:1

首先报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 letconst 优于 var 的一点。然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  • var 存在提升,我们能在声明之前使用。letconst 因为暂时性死区的原因,不能在声明前使用
  • var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
  • letconst 作用基本一致,但是后者声明的变量不能再次赋值

    原型继承和Class继承

    首先先来讲下 class,其实在 JS 中并不存在类,class 只是语法糖,本质还是函数
class Person {}
Person instanceof Function // true

组合继承

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

Class继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)

代理Proxy

Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式

let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数
用Proxy来实现一个数据响应式

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}


let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

在上述代码中,我们通过自定义 setget 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

map filter reduce

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中

[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]

另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

['1','2','3'].map(parseInt)
  • 第一轮遍历 parseInt('1', 0) -> 1
  • 第二轮遍历 parseInt('2', 1) -> NaN
  • 第三轮遍历 parseInt('3', 2) -> NaN

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素

let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]

map 一样,filter 的回调函数也接受三个参数,用处也相同

reduce 可以将数组中的元素通过回调函数最终转换为一个值。

如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码

const arr = [1, 2, 3]
let total = 0
for (let i = 0; i < arr.length; i++) {
  total += arr[i]
}
console.log(total) //6

但是如果我们使用 reduce 的话就可以将遍历部分的代码优化为一行代码

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)

对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程

  • 首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
  • 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
  • 在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
  • 所以在第二次执行回调函数时,相加的值就分别是 12,以此类推,循环结束后得到结果 6

想必通过以上的解析大家应该明白 reduce 是如何通过回调函数将所有元素最终转换为一个值的,当然 reduce 还可以实现很多功能,接下来我们就通过 reduce 来实现 map 函数

const arr = [1, 2, 3]
const mapArray = arr.map(value => value * 2)
const reduceArray = arr.reduce((acc, current) => {
  acc.push(current * 2)
  return acc
}, [])
console.log(mapArray, reduceArray) // [2, 4, 6]