JavaScript知识点梳理

114 阅读11分钟

一. 数据类型

javaScript数据类型分为原始类型和对象类型,对象对象类型也可称为引用类型

基本类型(类型以小字母开头)

  1. 字符串string
  2. 数字number(不区分整型和浮点型,均用64位浮点格式表示)
  3. 布尔值boolean
  4. null代表空值
  5. undefined表示未定义
  6. symbol 这个类型的值都是唯一的

引用类型

object(function、Array、Date都是引用类型)

判断数据类型

  • typeof
console.log(typeof '13');//string

console.log(typeof 12);//number

console.log(typeof false);//boolean

console.log(typeof null);//object

console.log(typeof undefined);//undefined

console.log(typeof Symbol(123));//symbol

console.log(typeof [1, 2]);//object

console.log(typeof (() => { }));//function

console.log(typeof { name: "huo" });//object

console.log(typeof new Date);//object

typeof判断类型存在不准确的可能

typeof null会返回object

typeof无法分变出数组、日期、正则,而是把他们都归为对象

  • toString

toString() 是 Object 的原型方法,默认返回当前对象的 [[Class]]是字符串 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

Object.prototype.toString.call('13');//[object String]

Object.prototype.toString.call(123);//[object Number]

Object.prototype.toString.call(false);//[object Boolean]

Object.prototype.toString.call(null);//[object Null]

Object.prototype.toString.call(undefined);//[object Undefined]

Object.prototype.toString.call(Symbol(123));//[object Symbol]

Object.prototype.toString.call([1, 2]);//[object Array]

Object.prototype.toString.call((() => { }));//[object Function]

Object.prototype.toString.call({});//[object Object]

Object.prototype.toString.call(new Date());//[object Date] 

如上所示,toString()方法可以辨别出所有类型,我们封装一下

/**
 * 判断类型
 * @param {any} value 
 * @returns {string} 类型(小写)
 */
const decideType = (value) => {
  let str = Object.prototype.toString.call(value).substring(8)
  return str = str.substring(0, str.length - 1).toLowerCase()
}

在遇到要判断类型时直接调用decideType就可以了,这里值的注意的地方,返回的全是小写,小写的才是类型,开头大些的是包装类,比如string和String是不一样的。

字符串的常用方法

  • toLowerCase()把字符串转为小写,返回改变后的字符串,不改变原来字符串
const str="Huo".toLowerCase()//huo
  • toUpperCase()把字符串转为大写,返回改变后的字符串,不改变原来字符串
const str="huo".toLowerCase()//HUO
  • indexOf(): 返回某个指定的子字符串在字符串中第一次出现的位置(经常用)
//第一个参数是目标值,第二个是开始位置
console.log("huo".indexOf("u"));//1,没有的话返回-1
  • slice(): 提取字符串,第一个参数是开始位置,第二个参数选填代表结束位置,不改变原字符串(经常用)
console.log(“Huo.slice(1));//uo
  • substring:和slice()用法一样,但是参数不能为负数。
  • split(): 把字符串分割成字符串数组。参数是什么就以什么分割,(经常用)
let str = "Huo,wo"
//把空字符串 ("")用作分割符,那么字符串的每个字符之间都会被分割,空不是空格
console.log(str.split(""));//[ 'H', 'u', 'o', ',', 'w', 'o' ]
console.log(str.split(","));//[ 'Huo', 'wo' ]
  • replace(): 在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
let str = "Huo"
const reg = /o/ig; //o为要替换的关键字,不能加引号,否则替换不生效,i忽略大小写,g表示全局查找。
console.log(str.replace(reg, "**"));//Hu**
  • match(): 返回所有查找的关键字内容的数组。用于查看一个字符串出现多少次
let str = "Huo,wo,oi"
const reg = /o/ig; //o为要查找的关键字,不能加引号,否则替换不生效,i忽略大小写,g表示全局查找。
console.log(str.match(reg, "**"));//[ 'o', 'o', 'o' ]

其他类型要转换成string类型

//toString()  +号也具有隐式转换的功能
let num = 123
let arr = [1, { name: 123 }]
console.log(num.toString());"123"
console.log(arr + '');//1,[object Object]

数组常用方法

Array.push(),//向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。

Array.unshift(),//向数组的开头添加一个或多个元素,并返回新的数组长度。原数组改变。

Array.concat(arr1,arr2...),//合并两个或多个数组,生成一个新的数组。原数组不变。

//splice并以数组形式返回被修改的内容。此方法会改变原数组。(增、删、改都可以)

Array.splice(start,0,items)//start开始位置,<=0代表添加,第三个参数是要添加的元素可以多个

Array.pop(),删除并返回数组的最后一个元素,若该数组为空,则返回undefined。原数组改变。

Array.shift(),删除数组的第一项,并返回第一个元素的值。若该数组为空,则返回undefined。原数组改变。

delete Array[index] 返回true 被删除的元素位置以undefined代替,数组长度不会改变

Array.splice(start,index)//start开始位置,index要删数的个数index>0

Array[index]=item//直接根据下标改变

Array.splice(start,index,items)//start开始位置,index要修改的个数,items要插入的值可多个
  • for(迭代速度最快)没有返回值,可以终止迭代
let arr = [1, 2, 3, 4, 5, 4, 3, 2]
let num
for (let i = 0; i < arr.length; i++) {
  if (i === 5) {
    num = i
    return
  }
}
  • for of 不能接受返回值(break、continue和return都可以用),可以终止迭代
for (let i of arr) {
  if (i === 5) {
    num = i
    break
  }
}
  • Array.map 接受参数(value, index, array),项目值、索引、和项目本身 可以接受返回值是数组, 迭代过程不可终止。
const data = arr.map((item, index) => {
  if (item === 5) {
       num = 5
      }
  return index
})
data//[0, 1, 2, 3,4, 5, 6, 7]
  • Array.forEach 接受参数(value, index, array),项目值、索引、和项目本身 不能接受返回值,迭代过程不可终止。forEach速度要比map快
arr.forEach((item, index) => {
  if (item === 5) {
     num = 5
  }
})
  • Array.filter 接受参数(value, index, array),项目值、索引、和项目本身,返回符合条件的元素,迭代过程不可终止.
const data = arr.filter((item, index) => {
  if (item === 5) {
    num = 5
  }
  return item === 5
})
data//[5]
  • Array.reduce 接受参数(total, value, index, array),每次循环的返回值,项目值、索引、和项目本身,可以接受返回值,返回值是total,迭代过程不可终止.
const data = arr.reduce((total, value, index, array) => {
  if (value === 5) {
    num = 5
  }
  return total+value
})
data//24 累加操作
  • Array.every 接受参数(value, index, array),项目值、索引、和项目本身,返回值是布尔值,第一次返回false就终止
const data = arr.every((value, index, array) => {
    if (value === 5) {
       num = 5
       return false
    }
    return true
})
data//false
  • Array.some 接受参数(value, index, array),项目值、索引、和项目本身,返回值是布尔值,第一次返回true就终止
const data = arr.some((value, index, array) => {
    if (value === 5) {
       num = 5
       return true
    }
    return false
})
data//true
  • Array.indexOf 和查找字符串一样 返回元素第一次出现的下标
arr.indexOf(5) > 0 ? num = 5 : null

数组其他方法

  • Array.join(),将数组转为字符串并返回改变后的字符串,不改变原数组,和字符串splic()方法很像
const data = arr.join(',')//"1,2,3,4,5,4,3,2"
  • Array.reverse(),将数组倒序。原数组改变。
const data = arr.reverse()//[2, 3, 4, 5,4, 3, 2, 1]
  • Array.sort(),对数组元素进行排序。按照字符串UniCode码排序,原数组改变,可接受函数(经常用)
let arr = [1, 2, 3, 4, 5, 4, 3, 2]

let data = arr.sort()//data[ 1, 2, 2, 3,3, 4, 4, 5]按照字符串UniCode码排序

data = data = arr.sort((a, b) => a - b)///data[ 1, 2, 2, 3,3, 4, 4, 5] 按照数值大小,从小到大

data = arr.sort((a, b) => b - a)//[5, 4, 4, 3,3, 2, 2, 1]从大到小

arr = [{ name: "hong", age: 18 }, { name: "zhang", age: 16 }, { name: "gao", age: 19 }]

const compare = (value) => (a, b) => a[value] - b[value]

data = arr.sort(compare("age"))//按照age从小到大 

const compare = (value) => (a, b) => b[value]-a[value]

data = arr.sort(compare("age"))//按照age 从大到小
  • 生成指定长度数组
let arr = [...new Array(10).keys()]

对象常用方法

const obj = {}
obj.name = "huo"
obj["age"] = 19
console.log(obj);//{ name: 'huo', age: 19 }
delete obj.name
obj//{ age: 19 }
obj.name = "张"
obj["age"] = 20

对象的属性有自身的属性和原型链上的属性,有可枚举属性和不可枚举属性之分

// 声明一个构造函数
function Person() {
  this.name = "huo"
  this.age = 18
  this.getName =()=>{}
};
Person.prototype.hobby = "run"//在原型上设置属性

const person = new Person()//实例化一个person对象

// 在person实例上设置age 为不可枚举属性
Object.defineProperty(obj, "age", {
  //是否为枚举属性
  enumerable: false
});
  • for in 循环会得到对象原型链上的属性,不包含不可枚举属性
for (let i in person) {
  console.log(i)//name getName hobby  得不到age
}
  • Object.keys() 只包含自身属性,不包含原型链和不可枚举属性
console.log(Object.keys(person))//[ 'name', 'getName' ]
  • Object.getOwnPropertyNames() 返回自身属性和不可枚举属性,不包含原型链属性
console.log(Object.getOwnPropertyNames(person))//[ 'name', 'age', 'getName' ]
  • 其他常用方法
  • Object.assign 将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。用于对象的浅拷贝
let person2 = {}
Object.assign(person2, person)
console.log(person2);//{ name: 'huo', getName: [Function (anonymous)] }
  • 对象深拷贝(有缺陷,到递归时会讲)
let person2 = JSON.parse(JSON.stringify(person))
  • Object.is判断两个值是否是相同的值。
let obj1 = { name: 2 }

let obj2 = { name: 2 }

let obj3 = Object.assign({}, obj1)

let obj4 = obj1

let obj5 = Object.assign(obj1, { age: 1 })

console.log(Object.is(obj1, obj2));//false

console.log(Object.is(obj1, obj3));//false

console.log(Object.is(obj1, obj4));//true

console.log(Object.is(obj1, obj5));//true

数字常用方法

  • Number.toString() 指定进制数 返回字符串 默认是十进制\
let num = 10
console.log(num.toString(2));//1010
  • Number.toFixed() 指定小数位
let num = 10.2518
console.log(num.toFixed(2));//10.25 四舍五入
  • parseInt(string,radix)解析一个字符串,并返回一个整数
console.log(parseInt("1010", 2));//10
  • 生成随机数
let num=Math.floor(Math.random()*10+1)//生成1到10之间的随机数

二. 函数

函数是可重复执行的代码块,可用作表达式,也可用作构造函数。

函数的声明

  • 直接声明
function getName() {
  console.log(1);
}
  • 函数表达式
var getName = function () {
  console.log(2);
}

函数的调用

  • 作为函数调用
function getName() {
  console.log(1);
}
getName()
  • 作为方法调用
//作为方法调用的前提必须生命在对象里面
const person = {
  name: "开林",
  age: "18",
  add() {
    console.log(this.name);
  }
}
person.add()
  • 作为构造函数使用
//先声明一个构造函数
function Person(name) {
  this.name = name
}
//通过new操作符号来赋值 得到的是对象
const peson1 = new Person("开林")
  • 间接调用(通过call和apply)

构造函数

通过 new 函数名 来实例化对象的函数叫构造函数。

构造函数简而言之就是克隆对象,javaScript生成对象的是基于原型继承的方式。

原型的基本规则:

  1. 所有的数据都是对象
  2. 要得到一个对象不是通过实例化类,而是找到一个对象作为原型并且继承
  3. 对象会记住自己的原型
  4. 如果对象无法响应某个请求会通过请求委托给自己的原型

这里有两个属性

  • prototype 返回对象类型原型的引用(只有构造函数有)
  • proto ,指向它构造器的原型对象 (任何对象都有)
  • constructor属性返回对创建此对象的数组函数的引用(任何对象都有)

通过new操作符号来创建对象

// new 操作符
function Person(name) {
  this.name = name
  this.getNmae = () => {
    console.log(this.name);
  }
}
const obj = new Person("huo")

console.log(obj.constructor === Person);//true
console.log(obj.__proto__ === Person.prototype);//true
console.log(Person.constructor.prototype === Person.__proto__);//true

我画了张思维导图

image.png

函数也是对象,对象最后都指向null

new 操作符运算的过程

function Person(name) {
  this.name = name
  this.getNmae = () => {
    console.log(this.name);
  }
}
const New = function () {
  const obj = {}//先创建一个空对象
  const Constructor = [].shift.call(arguments)//取得外部传入的构造器 Person
  obj.__proto__ = Constructor.prototype//指向正确的原型  Person.prototype  Person{}
  const ref = Constructor.apply(obj, arguments)//给obj设置属性
  return obj
}
console.log(person1);//Person { name: 'zhang', getNmae: [Function (anonymous)] }
console.log(Object.getPrototypeOf(person1));//Person {}

\

js 原型链

js 原型链就是单项链表,只不过在结构上多一个指向自身的constructor构造函数。

js 继承方式

1.工厂模式 缺点无法解决对象识别的问题

function createPerson(name, age, job) {
  const o = {}
  o.name = name
  o.age = age
  o.job = job
  o.getMas = function () {
    console.log(`我的名字叫${name}`);
  }
  return o
}
let person1 = createPerson("开林", 24, "富二代")
console.log(person1.constructor);//Object
console.log(person1 instanceof createPerson);//false
person1.getMas()

2.构造函数模式

主要问题 每个方法都要在每个实例从新创建一遍,重新创建的方法的实例并不相同,创建多个实例,会创建多个不同的方法,占用空间

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.getMas = function () {
    console.log(`我的名字叫${name}`);
  }
}
let wang = new Person("开林", 24, "富二代")
let dinng = new Person("麦客", 24, "富二代")
console.log(wang.constructor);//Person
console.log(wang instanceof Object);//true
console.log(wang instanceof Person);//true
console.log(wang.getMas == dinng.getMas);//false
// 改进 
function Person2(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.getMas = getMas
}
function getMas() {
  console.log(`我的名字叫${name}`);
}
let wang2 = new Person2("开林", 24, "富二代")
let dinng2 = new Person2("麦客", 24, "富二代")
console.log(wang2.getMas == dinng2.getMas);//true
  1. 原型模型
function Person3() {}
Person3.prototype.name = "开林"
Person3.prototype.age = 25
Person3.prototype.getMas = function () {
  console.log(this.name);
}
let wang3 = new Person3()
let dinng3 = new Person3()
console.log(wang3.getMas == dinng3.getMas);//true
console.log(Person3.prototype.isPrototypeOf(wang3));//true
// 查看原型链的属性
console.log(Object.getPrototypeOf(wang3));

// hasOwnProperty 判断属性是原型上的还是实例上的 in 操作符 无论属性在原型还是实例上都返回true
console.log(wang3.hasOwnProperty("name"));//false
console.log("name" in wang3);

console.log(wang3.hasOwnProperty("name"));//true
console.log("name" in wang3);//true
// 在for in 中 无论是原型上的属性 还是实例上的可枚举的属性 都可以被访问到
for (item in dinng3) {
  console.log(item);
}

this指向问题

  • 作为对象的方法调用this指向该对象
// 1 如果函数在对象中定义 this 指向对该象
const person = {
  name: "王凯",
  age: "18",
  add() {
    console.log(this.name);//name
  }
}
  • 作为普通函数调用,this 指向全局对象
  • 作为构造函数使用this 指向隐式生成的那个对象

闭包

闭包可以让函数外面可以访问函数里面的变量,一般用来生成私有变量,来做数据缓存,闭包占用的内存和直接声明变量的方式差不多,不会存在性能问题。前端常见的防抖、节流都是利用闭包来实现的。

\

  • 防抖,就是在规定时间内取最后一次操作为准。
function debounce(callBack, time) {
  let timer = null; // 定时器的返回值 初始为null
  return (...ags) => {
    clearTimeout(timer); // 如果防抖函数一直触发那么callBack一直不执行
    timer = setTimeout(() => {
      callBack.apply(this, ags);
    }, time * 1000);
  };
}
  • 节流,规定时间内只执行一次
function throttle(callBack, time) {
  let open = true; // open 是缓存变量 来控制函数执行还是不执行 
  return (...ags) => {
    if (!open) return;// 如果节流函数已经执行了 直接return
    open = false; // 设为关
    callBack.apply(this, ags)
    setTimeout(() => {
      open = true; // 节流周期执行完设为开
    }, time * 1000);
  };
}

递归

//求10的阶乘
function recursion(n) {
  console.log(n);
  if (n < 0) return -1
  if (n === 0 || n === 1) return 1
  return n * recursion(n - 1)
}

递归的问题:堆栈溢出

  • 函数的堆栈概念:js中,每次函数调用会在内存形成一个“调用记录”, 保存着调用位置和内部变量等信息。如果函数中调用了其他函数,则js引擎将其运行过程暂停,去执行新调用的函数,新调用函数成功后继续返回当前函数的执行位置,继续往后执行。执行新调用的函数过程,也是将其推入到堆栈的最顶部并为其开辟一块内容。新函数执行完毕后将其推出堆栈,并回收内存。由此便形成了函数的堆栈。

在写递归时我们可以写成尾递归:在函数执行的最后一步返回一个函数调用

function tailRecursion(n, total) {
  console.log(n);
  if (n === 1) return total;
  return tailRecursion(n - 1, n * total);
}

在js在V8引擎中并不支持尾递归优化,也就是说尾递归和递归性能是一样的。

上面讲深拷贝,用JSON的方法有缺陷,JSON实现的深拷贝,对value为函数或是undefined和正则,则不会保留。举个例子:

const params = { 
        name: "huo",
        time: new Date,
        getAge: (age) => { console.log(`我的年龄是${age}`) }, 
        children: [{ book: "123" }], 
        girlFriend: undefinedsetMany: /g/
      }
console.log(JSON.parse(JSON.stringify(params)));//{name: 'huo',time: '2020-12-01T13:34:18.588Z',children: [ { book: '123' } }

可以看到getAge 和girlFriend、setMany都没有拷贝出来,直接就去掉这个属性了,在某些场景这种拷贝不适用。

下面我们用递归来实现一下深拷贝

讲一下思路,对于基本类型可以直接赋值,函数、正则、时间虽然属于对象但是也可直接赋值,对于object,array我们要创建一个新对象或者数组来赋值它,我们之前写的判断类型的函数现在可以用了

/**
 * 判断类型
 * @param {any} value 
 * @returns {string} 类型(小写)
 */
const decideType = (value) => {
  let str = Object.prototype.toString.call(value).substring(8)
  return str = str.substring(0, str.length - 1).toLowerCase()
}
/**
 * 深拷贝
 * @param {any} value 
 * @returns {any} value 
 */
function DeepCopy(value) {
  if (decideType(value) === "object") {//是对象拷贝对象
    const obj = {}
    for (let i in value) {
      obj[i] = DeepCopy(value[i])
    }
    return obj
  } else if (decideType(value) === "array") {//是数组拷贝数组
    let ary = []
    value.forEach((item, index) => {
      ary[index] = DeepCopy(item)
    })
    return ary
  } else {//基本类型 时间  函数  正则全部返回
    return value
  }
}

高阶函数

  1. 函数可以作为参数被传递
  2. 函数可以作为返回值输出

满足以上条件一个条件我们都可以称之为高阶函数

常用的高阶函数 回调函数、函数柯里化、防抖节流、缓存函数。

回调函数与异步

\

event loop(事件循环)

javascript是单线程的非阻塞的,同步任务(主任务)的执行会放在执行栈中,遇到异步任务并不会阻塞同步任务的执行,而是将异步任务放到事件队列中待主任务执行完毕再去执行异步任务,这里的异步分为宏任务和微任务,宏任务如(setInterval(),setTimeout()),微任务(new Promise()、new MutaionObserver()),在同一事件队列中微任务总是排在宏任务前面。如下代码:

setTimeout(() => {
  console.log("异步,宏任务");
}, 0);
new Promise((resolve, reject) => {
  console.log("同步主任务");
  resolve("异步,微任务")
}).then(res => {
   setTimeout(() => {
     console.log("第二个事件队列");
  }, 0);
  console.log(res);
})

以上代码:同步主任务、异步微任务、异步,宏任务、第二个事件队列

在同一事件队列中宏任务会等待微任务执行完毕再执行,这样会导致宏任务造成阻塞、看下面代码

console.time("几秒后执行")
setTimeout(() => {
  console.timeEnd("几秒后执行");
}, 0);
let index = 0
console.time("async时间")
async function recursionAsync() {
  await undefined
  index += 1
  // console.log(index)
  if (index === 3000000) {
    console.timeEnd("async时间");
    return
  }
  return await recursionAsync()
}
recursionAsync()

async时间: 3.616s

几秒后执行: 3.828s

这里换一下定时时间

console.time("几秒后执行")
setTimeout(() => {
  console.timeEnd("几秒后执行");
}, 4000);
let index = 0
console.time("async时间")
async function recursionAsync() {
  await undefined
  index += 1
  // console.log(index)
  if (index === 3000000) {
    console.timeEnd("async时间");
    return
  }
  return await recursionAsync()
}
recursionAsync()

async时间: 3.586s

几秒后执行: 4.005s