JS知识点汇总

700 阅读8分钟

原型和原型链

  • 原型
    • 每个JS函数都有一个 prototype 属性,改属性指向的对象被称为原型对象。同时这个对象是这个函数的实例的原型对象。所有由这个函数构造出来的实例都有一个 __proto__ 属性指向原型对象,在原型对象上,有一个属性 constructor 指向实例的构造函数
  • 原型链
    • 定义在原型对象上的所有属性和方法都可以被实例共享。多个原型组成的就是一条原型链,在JS中,访问一个对象中的属性和方法,首先在对象自身中查找,如果找到则返回,否则去这个对象的原型中查找,如果没找到,就去原型的原型中查找,一直找到 object 的原型为止。如果没有找到,则返回undefined。 注: object 实例的原型没有原型对象,为null

数据类型、简单数据类型和复杂数据类型的区别

  • 简单数据类型:ES5中有Number,String,Null,Undefined,Boolean,ES6新增了symbol(表示 独一无二的值,是一种类似于字符串的数据类型), BigInt(安全存储,操作大整数)

  • 复杂数据类型:object (其中包括 function, array 等)

  • 简单数据类型的值直接保存在中,而复杂数据类型的值保存在中,栈中保存的只是复杂数据类型的堆内存地址。

  • NaN

    • NaN 是 Number 中的一种,非Number
    • isNaN() 检测是否是非数值型
    • js 规定的 NaN 不等于 NaN (NaN==NaN false)
  • 例子

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

    JS函数传参都是传值(无论是基础类型还是引用类型),可以看这篇:JS函数传参
    分析: a是数组,b是对象,都是引用类型,传的都是值(引用的地址值)。假设a的地址是0x001,d的地址是0x002,函数 send(c,d) 传入参数后,c = [] 开创了一个新的对象,假设c此时的地址是0x003,然后 d.b = 2,此时 d 的地址是 d 的地址,是0x002。但注意 d = {a:1} 时 d 被赋值了新的对象,是新的地址了,假设 d 新地址为0x004。console.log(a) 输出的是 a 的地址 0x001 中的值,是[ 1, 2, 3 ],console.log(b)输出的是 b 的地址 0x002 中的值,是{ b: 2 }

null和undefined

  • null表示“没有对象”,即该处不该有值
    • 作为函数的参数,表示函数的参数不是对象
    • 作为原型链的终点
  • undefined“缺少值”,此处应该有一个值,但没有定义
    • 变量声明未赋值
    • 调用函数时,参数未提供
    • 对象没有赋值的属性
    • 函数没有返回值

if语句中,nullundefined被自动转成false;

Number(null)=0;         Number(undefined)=NaN;
typeof(null)=object;    typeof(undefined)=undefined;

判断JS数据类型的方法

  • typeof
    • 特殊: typeof(null)=object ; 引用类型均返回object,除typeof(function)-->function
  • A instanceof B
    • instanceof 运算符 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
    • A是否是B的实例,检测的是原型;只能判断两对象是否属于实例关系,不能判断对象实例具体属于哪种;例如:
      let arr = [];
      arr instanceof Array;  // true
      arr instanceof Object; // true
      原型链查找:[]---Array.prototype---Object.prototype---null
      
  • Object原型上的toString方法
    • Object.prototype.toString()通过apply/call绑定调用
      console.log(Object.prototype.toString.call([]));         // "[Object Array]"  字符串
      console.log(Object.prototype.toString.call(null));       // "[object Null]"
      console.log(Object.prototype.toString.call(undefined));  // "[object Undefined]"
      
  • === 严格相等

判断数组、数组去重

判断数组的方法

  • Array.isArray() 用于确定传递的值是否是一个数组,返回一个布尔值
  • Object.prototype.toString.call(a) === '[object Array]';

数组去重

  • 双重 for 循环,splice 去重
  • 利用 indexOf / includes 去重
  • 利用 filter 滤掉重复元素
  • 利用 sort 排序后,遍历比较相邻元素来去重
  • 利用 ES6 中的 Set 去重(原理是通过两个函数__hash__和__eq__结合实现的)
    • 1、当两个变量的哈希值不相同时,就认为这两个变量是不同的
    • 2、当两个变量哈希值一样时,调用__eq__方法,当返回值为True时认为这两个变量是同一个,应该去除一个。返回FALSE时,不去重

伪数组和数组的区别

  • 数组
    • 数组具有一个最基本的特征:索引
    • 数组有Array.prototype,属性继承
    • 真实数组长度是可变的
  • 伪数组
    • 具有length 属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理,这里你可以当做是个非负整数串来理解)
    • 不具有数组所具有的一些方法
    • 伪数组长度是不可变的 常见的伪数组有:
  • 函数内部的 arguments
  • DOM 对象列表(比如通过 document.getElementsByTags 得到的列表)
  • jQuery 对象(比如 $("div") )

伪数组是一个 Object,而真实的数组是一个 Array。 可以看这篇:JavaScript伪数组和数组的使用与区别

闭包

说一下你对闭包的理解?闭包是什么?

  • 闭包就是有权访问另一个函数作用域变量的函数,形成闭包的原因是存在对上级作用域变量的引用.

    var a = [];
    for (let i = 0; i < 10; i++) {
        a[i] = function () {
            console.log(i);
        };
    }
    a[6](); // 6  // 手写一个简单的闭包
    
  • 闭包底层原理:JS代码要经过浏览器进行预编译后才能真正被执行,js代码运行需要一个运行环境,那这个环境就是执行上下文

  • 执行上下文分三种:

    • 全局执行上下文: 代码开始执行时首先进入的环境。
    • 函数执行上下文:函数调用时,会开始执行函数中的代码。
    • eval执行上下文:不建议使用,可忽略。
  • 执行上下文的周期

    • 创建阶段:创建词法环境 -- 生成变量对象 -- 建立作用域链 -- 确定this指向

    • 执行阶段:进行变量赋值 -- 函数引用及执行代码

闭包的作用?

  • 使用闭包可以保护函数的私有变量不受外部干扰
  • 也可以将上级作用域的引用保存下来,实现方法或属性的私有化

用闭包有什么弊端?

  • 容易导致内存泄漏,因为存在其他作用域的引用,过渡使用闭包会使内存占用过多

经典使用场景

  • for 循环的事件赋值引用:使用闭包来改善;使用 let 来改善

怎么检查内存泄漏

  • performance面板memory面板 可以找到泄露的现象和位置

可以看一下这篇:闭包

垃圾回收机制有了解吗?

当我们创建一个基本类型、对象或者函数时,引擎会自动分配内存。JS的引用数据类型是保存在堆内存中,栈内存中保存的是对堆内存地址的引用。当引用关系没有了,就需要被清理(回收),释放内存。 最常见的垃圾回收策略:标记清除算法 和 引用计数算法。

  • 引用计数算法
    • 策略就是跟踪记录每个变量值被使用的次数,判断该对象的引用数,引用数为0就回收,引用数大于0就不回收
    • 好处:当引用数为0时就可以立即回收
    • 缺点:需要计数器来每隔一段时间进行一次计数,计数器占位置,且会存在循环引用无法回收的问题
  • 标记清除算法
    • 将可达的对象标记起来,不可达的对象当成垃圾回收。
    • 好处:实现简单,标记和未标记两种情况,用二进制位(0和1)就能做标记
    • 缺点:清除后剩余对象内存位置不连续,内存分配问题

V8的垃圾回收算法

  • 分代回收:

    • V8将堆分为两个空间,一个叫新生代,存放存活周期短的对象;一个叫老生代,存放存活周期长的对象
    •  Scavenge算法:主要负责新生代的垃圾回收
    • Mark-Sweep && Mark-Compact算法:主要负责老生代的垃圾回收
  • 全停顿(Stop-The-World)

    • JS代码和垃圾回收都会用到JS引擎,如果两者同时进行,发生冲突,垃圾回收优先于代码执行,会先停止代码的执行,等到垃圾回收完毕,再执行JS代码。这个过程,称为全停顿
  • Orinoco优化

    • 增量标记:针对标记阶段,当垃圾达到一定数量时,增量标记就会开启,垃圾标记一点,JS代码运行一段,从而提高效率
    • 惰性标记:针对清除阶段,在增量标记后,要进行清理时,垃圾回收器发现了其实就算是不清理,剩余的空间也足以让JS代码跑起来,所以就延迟了清理,让JS代码先执行,或者只清理部分垃圾,而不清理全部。这个优化就叫做惰性清理。缺点就是不及时清理,可能造成对象引用改变,标记错误的现象。
    • 并行回收:垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作,但还是会阻塞主线程
    • 并发回收:在垃圾回收的同时不需要将主线程挂起,两者可以同时进行

更多可以看这篇:垃圾回收

for..in 和 for..of 、forEach 和 map

for..in 和 for..of

  • for..in 遍历的是数组的索引(即键名),for..of 遍历的是数组元素值(键值)
  • for..in会遍历数组所有的可枚举属性,包括原型,更适合遍历对象
  • for..of适用遍历数/数组对象/字符串/map/set等 拥有迭代器对象的集合。但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句
    let arr=[{name:'张三'},{name:'李四'}];
    let obj={name:'张三'};
    for (let i in arr){  // for..in遍历的是数组的索引(即键名)
        console.log(i)   // 0 1
    }
    for (let i of arr){  // for..of遍历的是数组元素值
        console.log(i)  // { name: '张三' }    { name: '李四' }
    }
    for (let i in obj){
        console.log(i)  // name -- 对象的键名 
        // for in会遍历数组所有的可枚举属性,包括原型,更适合遍历对象
    }
    for (let i of obj){   // **for...of不能对象用**
        console.log(i)  // TypeError: obj is not iterable
    }
    
  • 获取对象上属性的方法:
    • for in 可以遍历到myObject对象的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部用以下两种方式判断一下
    • hasOwnPropery方法可以判断某属性是否是该对象的实例属性,返回一个数组
      for (var key in myObject) {
        if(myObject.hasOwnProperty(key)){
          console.log(key);
        }
      }
      
    • 同样可以通过 ES5的Object.keys(myObject) 获取对象的实例属性组成的数组,不包括原型方法和属性

forEach 和 map

  • foreEach() 方法: 针对每一个元素执行提供的函数

    • 不会返回执行结果,结果是undefined,会对每个元素执行所提供的的函数,但不一定改变原数组,除非直接对数组元素进行操作
      let arr =[1,2,3,4];
      let arr1 = arr.forEach((item) => {
          return item * item; // 针对每一个元素执行提供的函数,没有返回值,不一定改变数组的值
      });
      console.log(arr)   // [ 1, 2, 3, 4 ]
      console.log(arr1)  // undefined --- 没有返回值
      arr.forEach((value, key) => {
          return arr[key] = value * value;  // 这里是直接对数组元素的值进行了修改
      });
      console.log(arr) // [ 1, 4, 9, 16 ]   // 数组元素被改变
      
  • map() 方法: 创建一个新的数组,用来储存对数组每个元素执行函数得到的结果

    let arr =[1,2,3,4];
    let res = arr.map((item) => {    // 返回一个新数组
        return item * item;
    });
    console.log(arr)   // [ 1, 2, 3, 4 ] -- 原数组不变
    console.log(res)   // [ 1, 4, 9, 16 ]
    

事件捕获、事件冒泡、事件代理

  • 事件捕获

    • 当一个事件触发后, 从Window对象触发, 不断经过下级节点, 直到目标节点。在事件到达目标节点之前的过程就是捕获阶段。所有经过的节点, 都会触发对应的事件
  • 事件冒泡

    • 当事件到达目标节点后,会沿着捕获阶段的路线原路返回。同样,所有经过的节点,都会触发对应的事件 image.png
  • 事件委托(代理)的作用

      1. 支持为同一个DOM元素注册多个同类型事件
      1. 可将事件分成事件捕获和事件冒泡机制
  • 用addEventListener(type,listener,useCapture)实现事件监听:

    • type: 必须,String类型,事件类型
    • listener: 必须,函数体或者JS方法
    • useCapture: 可选,boolean类型。指定事件是否发生在捕获阶段。默认为false,事件发生在冒泡阶段
  • event.target 和 event.currentTarget 的区别

    • event.target指向引起触发事件的元素

    • event.currentTarget则是当前触发事件的元素

    • 只有被点击的那个目标元素的event.target才会等于event.currentTarget

      image.png

      捕获阶段                            冒泡阶段
      target:d & currentTarget:a         target:d & currentTarget:d 
      target:d & currentTarget:b         target:d & currentTarget:c
      target:d & currentTarget:c         target:d & currentTarget:b
      target:d & currentTarget:d         target:d & currentTarget:a
      
  • 阻止事件冒泡和阻止默认事件的区别

    • event.stopPropagation()方法:只阻止事件往上冒泡,不阻止事件本身,默认事件任然会执行,例如你点击一个连接,这个连接仍然会打开。

    • event.preventDefault()方法:阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素

    • return false: 即阻止事件冒泡也会阻止默认事件,连接不会被打开,事件也不会传递到上一层

      不是所有的事件都能冒泡。以下事件不冒泡:blur、focus、load、unload。

      扩展:stoppropagation、cancelBubble区别(阻止冒泡)

JS获取DOM节点的方法

  • 通过 id 获取
    • document.getElementById('id名'): 返回的是对应Id的DOM对象
  • 通过 class 获取:
    • document.getElementsByClassName('class名'):返回的是对应class的DOM节点对象集合,是一个伪数组,注意是所有节点
  • 通过 标签名 获取
    • document.getElementsByTagName('标签名'):返回的是对应标签名的DOM节点对象集合,是一个伪数组,注意是所有节点
  • 通过 css选择器 获取
    • document.querySelector('..'): 参数:css选择器,#id,.class,tag等,返回的是对应css选择器对应的第一个标签
    • document.querySelectorAll():参数同上,返回的是对应css选择器对应的DOM属性节点对象集合,是一个伪数组,注意是元素节点

JSON.stringify()转换

var a = function(){}
var b = [1,a]
console.log(JSON.stringify(a))       // undefined
console.log(JSON.stringify({a}))     // {}
console.log(JSON.stringify(b))       // [1,null]

JSON.stringify(a) 是 undefined , 当一个函数当做对象去用的时候,如果是字符串,会调用该函数的toString()方法,该方法没有返回值,那么这个函数就是 undefined ; 同理如果加上 {fn} 的话,返回 {}。
JSON.stringify()转换详细看这里:MDN---JSON.stringify()

Object.defineProperty的属性

Object.defineProperty(obj:对象, prop:key, descriptor:属性和方法)
let obj = {}
Object.defineProperty(obj,'a',{
    value:undefined      //1.就是value值
    configurable: true    //2.是否允许配置对象,删除属性
    writable:true        //3.是否允许赋值,修改对象
    enumerable:true      //4.是否允许枚举对象  
    
    // get、set设置时不能设置1.3,他们代替了二者,是互斥的
    get() {}    //获取的return的值,需要return 否则会回去到 undefined
    set(newVal) {}    //修改的时候执行  newVal就是最新的值,可以修改数据、执行函数、dom操作等一些方法
})


可以看这篇:详解JavaScript之神奇的Object.defineProperty

高阶函数

高阶函数是一个可以接收函数作为参数,甚至返回一个函数函数。 它就像常规函数一样,只是多了接收和返回其他函数的附加能力,即参数和输出。常用的高阶函数:

  • map: 对输入数组中每个元素调用回调函数来创建一个新数组
  • filter(): 会创建一个新数组,其中包含所有通过回调函数测试的元素
  • reduce : 对调用数组的每个元素执行回调函数,最后生成一个单一的值并返回
  • 函数柯里化--高阶函数