JS基础梳理(一)

838 阅读8分钟

一、类型

1、基础类型和引用类型

  • 基础类型有:number string boolean null undefined symbol bigint
  • 引用类型有:object array function ...

2、类型检测

typeOf 和 instanceOf

  • typeOf可以判断基础类型、判断对象类型时,除了函数 其他都是 object
  • instanceOf 可以判断对象类型,原理是通过原型链查找原型对象

Object.prototype.toString.call()

instanceOf 实现手写

const myInstanceOf = (a, A) {
  if(typeOf a !== 'object') {
      return false
  }
  if(a._proto_ === A.prototype) {
    return true
  }
  return myInstanceOf(a._proto_, A)
} 

3、类型转换

  • == 的转换只有三种:转成数字、转成字符串、转成bealoon

4、对象转原始类型的流程

  1. 先找Symbol.toPrimitive方法, 优先调用
  2. 再找valueOf方法,如果转为原始类型,则返回。
  3. 调用toString(), 如果转为原始类型则返回
  4. 如果都没有则报错

二、闭包

作用域链

什么是闭包

闭包是指有权访问另外一个作用域中变量的函数。

为什么会有闭包

作用域有 全局作用域和函数作用域。内层函数会将拷贝外层的作用域,与自己的作用域一起形成作用域链。作用域链链保证了执行环境对变量的有序访问,当在当前环境内没有找到的变量,就会去父级作用域查找

三、原型链

如何理解原型链

  • 构造函数有一个prototype属性,指向了原型对象。
  • 原型对象中的属性和方法会被构造函数的所有实例共享。
  • 实例对象有一个_proto_属性指向了原型对象
  • 原型对象又有自己的_proto_指向它的原型对象,因此就形成了原型链

四、继承

  • 实现继承的几种方式(逐步趋近完善)

1、借用call

function Parent(){
  this.name = `parent1`;
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

缺点:没办法继承父类的方法

2、借用原型链

function Parent(){
  this.name = `parent1`;
  this.play = [1,2,3]
}

function Child() {
  this.type = 'child'
}

Child.prototype = new Parent()

缺点:继承的属性会被所有实例共享

3、call 和原型链组合

function Parent(){
  this.name = `parent1`;
  this.play = [1,2,3]
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

Child.prototype = new Parent()

缺点: Parent的构造函数会被重复调用。

4、组合方式优化

function Parent(){
  this.name = `parent1`;
  this.play = [1,2,3]
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

Child.prototype = Parent.prototype

缺点: Child.prototype.constructor 指向了Parent

5、寄生组合(完美方案)

function Parent(){
  this.name = `parent1`;
  this.play = [1,2,3]
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

五、函数的arguments、this

arguments

1. arguments 是不是数组

不是数组、是一个类数组对象。它的key从0开始依次递增。并且有length属性,但是没有数组的诸多工具方法

2. 如何转成数组

  1. 通过Array.form
  2. 通过结构赋值 [...arguments]
  3. Array.prototype.slice.call(arguments)
  4. Array.prototype.concat.apply([], arguments)

this 的指向

各种场景下的this指向

  • 全局上下问的this 指向window,严格模式下为undefined
  • 直接调用函数与全局上下文一致
  • 对象.方法的方式,this指向这个对象
  • DOM事件绑定: onclickaddEventerListenerthis指向绑定事件的元素 IE浏览器中的attachEvent,this指向window
  • new + 构造函数,构造函数中的this指向实例对象。
  • 箭头函数没有自己的this,也不能绑定,箭头函数中的this指向构造时上下文环境的this

手动实现new

  • 创建一个空对象obj
  • appy一下构造函数
  • obj_proto_ 指向构造函数的prototype
  • 如果构造函数的执行结果返回一个对象,则返回该对象、否则返回obj
function myNew(cstor, ...args) {
  let obj = {}
  let res = cstor.apply(obj, args)
  if(typeOf res === 'object' || typeOf res === 'function') {
    return res
  }
  obj.__proto__ = Object.create(cstor.prototype)
  return obj

}

实现apply、call

  • ES5的方式
// 以call为例
Function.prototype.call = function(context) {
  
  //  不能用 var args = arguments.slice(1) ,因为arguments 不是数组对象
  var args = []
  for(i = 1; i < arguments.length; i ++) {
    args.push(arguments[i])
  }
  context.fn = this
  // 通过数组转字符串来提取参数
  var res = eval('context.fn(' + arg + ')')
  delete context.fn
  return res
}
  • ES6的方式
// apply

Function.prototype.myApply(obj, args) {
  let fn = this
  obj.fn = fn
  // !数组里面的参数通过解构传进来
  let res = obj.fn(...args)
  delete obj.fn
  return res
}

// call

Function.prototype.myApply(obj, ...args) {
  let fn = this
  obj.fn = fn
  let res = obj.fn(...args)
  delete obj.fn
  return res
}

手动实现bind

  • bind 会返回一个新函数
  • 如果新函数被作为构造函数执行时,忽略this绑定。
Function.prototype.bind = function(context, ...bindArg) {
  let fn = this
 // 闭包
  let resFn = function(...arg) {
      if (this instanceof resFn) { 
          // 作为构造函数被调用
        return fn.call(this)
      } else {
        return fn.call(context, ...bindArg.concat(arg)))
      }
  }
 
 // 原函数的原型对象不能丢
  resFn.prototype = Object.create(this.prototype)
  return resFn
}

六、 数组

1、判断数组中包含某值

  • includes
  • indexOf
  • find
  • findIndexOf
  • some

2、数组扁平化

let arr = [ [1,3,[12]] ,5]
  • flat 方法
arr.flat(Infinity)
  • replace + 正则匹配
let str = JSON.stringify(arr)
str.replace(/(\[ | \])/, '').split(',')
// 除了split, 也可以 在外面加上[] 字符之后通过JSON.parse 转成数组
  • 递归
// 结合reduce
function myFlat(arr) {
  return arr.reduce((res, item) => {
    if(Array.isArray(item)) {
      return res.concat(myFlat(item))
    } else {
      return res.concat(item)
    }
  }, [])
}
// 也可以用forEach
  • some + concat + 扩展运算符号
while(arr.some(item => Array.isArray(item))) {
  arr = [].concat(...arr)
}

延伸:对象扁平化

// 例:有对象如下 
{
  a: 1,
  b: { ba: 2,  bb: 4},
  c: [ 1 , 3],
}
// 扁平化转成
{
  a: 1,
  b.ba: 2,
  b.bb4,
  c.0: 1,
  c.2:  3
}

实现如下:利用递归

const objectFlat = function(originObj) {
  if(!originObj) {
    return originObj
  }

  let result = {}
  function flat(obj, prefixKey, res) {
    for(key in obj) {
      let currentKey = prefixKey ? `${prefixKey}.${key}` : key
      if(typeof obj[key] === 'object' && obj[key] !== null) {
        // 如果是数组或对象
        flat(obj[key], currentKey , res)
      } else {
        res[currentKey] = obj[key]
      }
    }
  }

  flat(originObj, '', result)
  return result
}

3、数组的函数手动实现

  • map
Array.prototype.map = function(callback, thisObj) {
  // 边界处理
  if(this === null || this === undefined) {
    throw new Error("can not read prototype 'map' of null or undefind ")
  }
  let currentArr = this
  let len = currentArr.length
  let res = []
  for(let i = 0; i < len; i ++) {
    res.push(callback.call(thisObj, currentArr[i], currentArr, thisObj))    
  }  
  return res
}
  • reduce

reduce 的特性: 参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组。 不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。

Array.prototype.reduce = function(callback, initVal) {
    //  校验入参,具体同上
    CHACK_ARG(callback, initVal)

  let len = this.length
  let res = initVal || this[0] 
  for(let i = initVal ? 0 : 1 ; i < len ; i++) {
     res = callback(res, this[i], this)
  }
  return res
}

  • filter

一个参数,即回调函数 这个函数接受一个参数,表示当前元素 函数返回一个布尔值决定是否保留。

Array.prototype.fliter = function(callback) {
  // 校验入参
  CHECK_ARG(callback, this)

  let currentArg = this
  let len = this.length
  let res = []
  for(let i = 0; i < len ; i++) {
    if(callback(currentArg[i])) {
       res.push(currentArg[i])
    }
  }
}
  • push、pop

push : 可接收一个或多个参数,将该参数依次加入到数组尾部,并返回新的数组长度

Array.prototype.push = function(...args) {
  let arr = this
  let len = this.length
  let addCount = args.length
  
  // 边界校验:
  if(len + addCount > 2 ** 53 - 1){
    throwTypeError("The number of array is over the max value restricted!")
  } 
   for(let i = 0; i < addCount; i ++) {
      arr[len + i] = args[i]
    }
    return arr.length
}

pop: 删除数组末尾的元素,并返回该元素

Array.prototype.pop = function() {
   let arr = this
    let len = this.length
    if(len === 0) {
      return undefined
    }
    
    result = arr[len - 1]
    delete arr[len - 1]
    arr.length = len - 1
    return result
}
  • splice
  • 接受多个参数:position, count,item1,item2, ...
  • position 必传,表示起始下标,如果是负数,则从数组结尾处倒数;
  • count,必传,表示要删除的元素个数,如果是0则不删除;
  • item1、item2、... 可选,向数组新插入的元素
// 实现 splice方法

Array.prototype.splice = function(position, count, ...items) {

  // 判断数组是否是密封对象或冻结对像
  isSealedOrFrozen(this)

  let arr = this
  let len = this.length
  
  // 处理起始位置,将负数的转成下标
  let startIndex = countStartIndex(position, len)
  
// 处理删除值
  let deleteCount = countDeleteCount(startIndex, arr , count)
  // 删除需要删除的元素,并返回被删除的元素数组
  let deletedArr = deleteArrItems(arr, startIndex, count)

  // 移动数组元素
  moveArrItem(arr, startIndex, deleteCount, items)
  // 插入新元素
  insertNewItems(arr, items, startIndex)

  return deletedArr

}

function  isSealedOrFrozen(arr, deleteCount, addItems) {
  
  if(Object.isSealed(arr) && deleteCount !== addItems.length) {
    // ! 密封对象不可以删除或新增属性,但可以修改已有属性,所以当新增和删除相同时,不会有问题
    throw new TypeError('this array is a sealed Object')
  } else if(Object.isFrozen(arr) && (deleteCount > 0 || addItems.length > 0) ) {
    // ! 冻结对象不可删除、不可新增、不可更改
    throw new TypeError('this array is a frozen object')
  }
}

function countStartIndex(position, arrLen) {
  if(parseInt(position) !== position) {
    throw new Error('参数类型错误')
  }

  if(position < 0) {
    return arrLen + position
  }

  return position
}

function countDeleteCount(startIndex, arr, count) {
  if(parseInt(count) !== count) {
    throw new Error('count 参数类型错误')
  }

  if(count < 0) {
   return 0
  }

  if(count > arr.length - startIndex) {
    return arr.length - startIndex
  }
  return count
 }

function deleteArrItems(arr, startIndex, count) {
  // 删除元素,但暂不挪动位置
  let deleted = []
  if(!count) {
    return deleted
  }
  for(let i = startIndex; i < startIndex + count ; i ++) {
    deleted.push(arr[i])
    delete arr[i]
  }

  return deleted
}

function moveArrItem(arr, startIndex, count, items) {
  let arrLen = arr.length
  let itemsLen = items.length
  if(itemsLen === count) {
    return
  }

  if(itemsLen > count) {
    // 添加数大于删除数整体后移
    let moveLen = itemsLen - count
    for(let i = arrLen - 1; i <= startIndex; i --) {
      arr[i + moveLen] = arr[i]
    }
  }

  if(itemsLen < count) {
    let moveLen = count - itemsLen
    for(let i = startIndex + count ; i < arrLen; i ++) {
      arr[i - moveLen] = arr[i]
    }
  }
}

function insertNewItems(arr, items, startIndex) {
  // 
  let arrLen = arr.length
  let itemsLen = items.length

  for(let i = startIndex; i < startIndex + itemsLen; i ++) {
    arr[i] = items[i - startIndex]
  }
}
  • sort

sort 的使用方法:

  • 接收一个回调函数参数
  • 回调函数接受两个参数,分别代表比较的两个元素 - 当对比函数返回值大于0, 则a 在b 后面,即a 的下标应该比b大; - 反之,则a 在b 的前面,即a 的下标比b小; - 整个过程是完成一次升序的排列。
  • 如果不传比较函数。则会将数字转成字符串,然后按unicode 值升序排列
// 使用范例:
let nums = [2,3,1]
nums.sort(function(a, b) {
  if(a > b) return 1;
  else if(a < b) return -1
  else if(a === b) return 0
})

手写实现:

源码中的sort思路 :

  • n <= 10时,采用插入排序

理论上,插入排序时间复杂度是O(n^2), 快速排序的复杂度是O(nlogn), 但当元素个数足够小的时候,快速排序经过优化后性能是有可能超过快速排序的

  • n > 10时,采用三路快速排序
  • 10 < n <= 1000,采用中位数作为哨兵元素
  • n > 1000 每隔200 ~215个元素挑一个元素,放到新数组,然后对它进行排序,然后找到中间数,作为哨兵元素。

之所以要花费力气去寻找哨兵元素,是因为: 快速排序的性能瓶颈取决于递归深度,递归越深,消耗时间越长,因此,选择的哨兵元素要尽可能地处于中间大小,让比较的两边尽可能平衡,这样递归的层级就不会太深。

Array.prototype.sort = function (compareFn) {
  const len = arr.length

  // 处理参数边界值
  const arr = handleParams(this, compareFn)

  return quickSort(arr, compareFn)
}

function handleParams(arr, fn) {  // 处理参数边界值
  if (typeof fn !== 'function' && fn !== 'undefined') {
    throw new TypeError('fn is not a function!')
  } else if (fn === 'undefined') {
    return arr.map((item) => {
      return `${item}`
    })
  } else {
    return arr
  }
}

function insertSort(arr, compareFn) {
  // 插入排序
  const len = arr.length
  for (let i = 0; i < len; i++) {
    const current = arr[i]
    // 从当前元素往前找,如果与前面的元素比较结果 < 0, 则将当前元素插入到它前面
    for (let j = i; j >= 0; j--) {
      if (compareFn(current, arr[i]) < 0) {
        arr[i + 1] = arr[i]
        arr[i] = current
      }
    }
  }
}

function quickSort(arr, compareFn) {
  const len = arr.length
  if (len <= 10) {
    // 如果数组太短,自动改用插入排序
    return insertSort(arr, compareFn)
  }

  const sentry = getSentry(arr, compareFn)
  // 三路快排
  const low = []
  const high = []
  const eq = [sentry]
  for (let i = 0; i < len; i++) {
    if (compareFn(arr[i], sentry) < 0) {
      low.push(arr[i])
    } else if (compareFn(arr[i], sentry) > 0) {
      high.push(arr[i])
    } else {
      eq.push(arr[i])
    }
  }
  return [...quickSort(low, compareFn), ...eq, ...quickSort(high, compareFn)]
}

function getSentry(arr, compareFn) { // 计算哨兵元素
  const len = arr.length
  if (len <= 10) {
    // 如果长度小于10 不需要哨兵元素
    return 0
  }
  if (len > 10 && len <= 1000) {
    // 直接取中间点
    const sentryIndex = Math.floor(len / 2)
    return arr[sentryIndex]
  }
  // 超过1000 每隔200元素挑一个元素,放到新数组,然后对它进行排序,然后找到中间数,作为哨兵元素

  const sentryArr = [] // 用来存放备选的哨兵
  const sentryArrLen = Math.floor(len / 200) // 计算备选哨兵个数

  for (let i = 0; i < sentryArrLen; i++) {
    sentryArr.push(arr[200 * i])
  }

  const sortedSentryArr = quickSort(sentryArr, compareFn)
  const middleIndex = Math.floor(sortedSentryArr.length / 2)

  return sortedSentryArr[middleIndex]

}

拓展:其他的排序算法

1、冒泡排序


function bubbleSort(arr) {
  let len = arr.length

  for(let i = 0; i < len; i++) {
    for(let j = 0; j < len - 1 - i; j++) {
      if(arr[j] > arr[j + 1]) {
        let temp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = temp
      }
    }
  }

  return arr
}

2、归并排序

function mergeSort(arr) {
  function sort(arr, start, end) {
    const len = end - start
    if (len <= 1) {
      return arr
    }

    const middleIndex = Math.floor(len / 2)
    const left = sort(arr, start, middleIndex)
    const right = sort(arr, middleIndex, end)

    const result = []
    let p1 = 0
    let p2 = 0
    while (p1 < left.length && p2 < right.length) {
      if (p1 < p2) {
        result.push(left[p1])
        p1++
      } else {
        result.push(right[p2])
        p2++
      }
    }

    while (p1 < left.length) {
      result.push(left[p1])
      p1++
    }

    while (p2 < right.length) {
      result.push(right[p2])
      p2++
    }

    return result
  }

  return sort(arr, 0, arr.length)
}

4、如何跳出forEach

  • 使用try-catch,并在需要抛出跳出循环的地方抛出错误
  • 推荐使用every 或 some 替代forEach,every在return false 时中止循环,some 在 return true 时跳出循环。