likes
comments
collection
share

JavaScrip基础 | 一文扫清知识盲点

作者站长头像
站长
· 阅读数 23

数据类型

Js有哪些数据类型?

其中原始数据类型+引用数据类型

  • 六种:undefined、null、number、boolean、string、symbol、bigint
  • 一种引用类型:Object

原始数据类型和引用数据类型的主要区别在于**存储位置不同**:

原始数据类型存在于中的简单数据段,大小固定、占据空间小,适合被频繁使用的数据。

引用数据类型存在于中的对象,占据空间大、大小不固定,引用数据类型在栈中存放了地址指针,该指针指向该对象在堆中的引用值。

栈和堆的理解:

数据结构层面,栈是先进后出的数据结构;而堆是一个优先队列,根据优先级来排序

操作系统层面,内存会被分成栈区和堆区

  • 栈区内存有编译器自动分配和释放,存放函数的参数值、局部变量等
  • 堆内存由开发者分配释放,如果不释放,程序结束可能会造成内存泄漏

ES6中新增的symbol和bigint类

  • symbol类型,symbol类型是为了解决全局对象命名冲突问题,symbol可以创建一个独一无二的键。
  • bigint是为了解决大数问题,存在-2^53 ~2^53(安全数)之外的数

检测数据类型的方法有哪些?

  1. typeof 只可以检测原始数据类型,对于引用数据类型并不适用

typeof的返回值有7种结果

typeof undefined//undefined
typeof null//或[]、{} Object
typeof 'stri'//string
typeof true//boolean
typeof 2//numner
typeof Symbol('2')//Symbol
typeof function(){}//function

为什么typeof null是'object' ,因为js将前三位为0的变量判定为引用数据类型,而null全部都是0

  1. instanecof 只可以检测引用数据类型,对原始数据类型并不适用

instanceof用于检测该对象的原型链上是否存在右边的类型(构造函数的prototype属性)

instanceof只能用于判断引用数据类型,而不能用于判断原始数据类型

{} instanceof Object//true
[] instanceof Array//true
function(){} instanceof Function//true

false instanceof Boolean//false
2 instanceof Number//false
'str' instanceof String//false

instanceof的实现原理(手写):

function instancof_(L,R){
    if(typeof L !=="object"&& L==null){
        return false;
    }
    R = R.prototype;
    L = L.__proto__;//Object.getPrototypeOf(L);
    while(true){
        if(L == null) return false;
        if(L === R.prototype) return true;
        L = L.__proto__;//Object.getPrototypeOf(L);
    }
}
  1. Object.prototype.toString.call() 可以判断所有数据类型
  1. constructor 实例可以通过constructor来访问构造函数

判断数组的方法有哪些?

  1. Object.prototype.toString.call(arr).silce(8,-1) === Array
  1. Array.isArray(arr)
  1. arr.__proto__ === Array.prototype
  1. arr instanceof Array

undefined和null的区别

字面意思:undefined是未定义,而null是空对象。undefined一般用于声明一个变量但未给它赋值,或者函数没有显示返回值时,而null则是人为的赋值为空对象。

isNaN和Numer.isNaN的区别

isNaN存在类型转换,而Number.isNaN不进行类型转换

比如

isNaN('s1')//先将里面的内容转成Number,如果结果是NaN, => true
Number.isNaN('s1')//先判断是不是Number,不是false,不存在类型转换

0.1 + 0.2 !==0.3? 为什么?

0.1转二进制小数时:

0.1 * 2 = 0.2
0.2 * 2 = 0.4 // 注意这里
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4 // 注意这里,循环开始
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
...

有限十进制的0.1转化成了无限二进制小数的0.00011001100...,所以导致了转化发生精度丢失。所以十进制中一位小数0.1~0.9除了0.5之外其他值在转化二进制的过程中都丢失了精度。

精度解决方案

  1. 大数使用bigInt
  1. 使用toFixed 四舍五入
parseFloat((0.1 + 0.2).toFixed(10))//0.3
parseFloat((0.3 / 0.1).toFixed(10))//3
parseFloat((0.1 * 0.2).toFixed(10))//0.02
parseFloat((0.7 * 180).tpFixed(10))//126
parseFloat((1.0 - 0.9).toFixed(10))//0.1
parseFloat((9.7 * 100).toFixed(10))//970
parseFloat((2.22 + 0.1).toFixed(10))//2.32
  1. 小数加法
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));//取两个长度最大的
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}
  1. 确保一个数字的精确
function strip(num,precision=12){
    return +parseFloat(num.toPrecision(precision));
}

==、===和Object.is() 的区别

==的隐式类型转换规则:

  1. 如果两边都是布尔值,则都转成数值,再进行比较
  1. 如果一个是字符串,一个是数值,尝试将字符串转成数值,再进行比较
  1. 如果一个是对象,而另一个不是,则先使用valueOf的方法取到原始值,再进行比较
const str1 = 'abc';
const str2 = new String('abc');
str1 == str2 //true str2调用了valueOf取得原始值
str1 === str2 //false 类型不同
  1. 如果两个都是对象,则先判断两个对象是否属于同一块内存地址,是返回true
  1. undefined和null相等,不进行类型比较
  1. NaN任何值比较都是false,NaN自身比较也是false

比较~~坑的~~比较

'' == '0' // false
0 == '' // true
0 == '0' // true false == 'false' // false
false == '0' // true false == undefined // false
false == null // false

=== 是全等的比较

不进行类型转换,如果类型不相等,直接返回false

undefined === null/false

Object.is(a,b) 的判断

判断规则和===类似,但是对于NaN+0-0的判断规则有不同之处:

Object.is(NaN,NaN)//true
NaN === NaN//false

Object.is(+0,-0)//false
+0 === -0//true

**parseInt和Number**的显示类型转换规则

parseInt()

  • boolean = >NaN
  • null、undefined => NaN
  • 空字符串 => NaN
  • 数字开头的字符串:-12x=>-12、非数字开头的=>NaN

Number()

  • boolean => 1/0
  • null=>0 、undefined=>NaN
  • 空字符串 => 0
  • 含非数字的=>NaN

['1', '2', '3'].map(parseInt) 的结果是什么?1 NaN NaN

Array.prototype.parseInt((curVal,index,arr),this) , ['1', '2', '3'].map(parseInt)被分解之后就是parseInt('1',0) 、 parseInt('2',1) 、 parseInt('3',2)

  • parseInt第二个参数表示进制
    • parseInt('1',0)参数为0,默认10进制
    • parseInt('2',1) 参数为1,不符合NaN
    • parseInt('3',2) 二进制没有3,返回NaN

Boolean()显示类型转换

将任意类型的值转为布尔值

Boolean(undefined)//false
Boolean(null)//false
Boolean(NaN)//false
Boolean('')//false
Boolean({})//true
Boolean([])//true
Boolean(new Boolean(false))//true

赋值、深浅拷贝

深浅拷贝是对于Object或Array这样的引用类型而言

  • 赋值是将该对象在栈中的地址赋值给另一个对象,而不是堆中的数据,两个对象指向的是同一个存储空间,哪个对象改变都会改变存储空间中的内容,对象联动。
  • 浅拷贝按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址,所以其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝是开辟一个内存空间,复制一个一模一样的对象
和原数据是否指向同一对象第一层是原始值类型原数据含子对象
赋值改变会一同改变改变会使数据一同改变
浅拷贝改变不会使原数据一同发生改变改变会使原数据发生改变
深拷贝改变不会使原数据一同发生改变改变不会使原数据一同发生改变

浅拷贝

  1. Object.assign() 第一层不联动,第二层开始联动
var obj = {name:'2',a:{b:"code",c:"code2"}}
var obj2 = Object.assign({},obj)
obj2.name = '3';
obj2.a.b = 'wode'
console.log(obj.name)//'2' 拷贝了对象属性的引用,而不是本身,所以第一层原始数据不变
console.log(obj.a.b)//'wode'
  1. Array.prototype.concat() && Array.prototype.slice()
let arr = [1,3,{username:'cobe'}]
let arr2 = arr.slice()
arr2[2].username = 'wode'//第二层开始联动
console.log(arr[2].username)//wode
  1. 扩展运算符 ...[1,2,3]

深拷贝

  1. 使用JSON.parse()JSON.stringify()
let arr = [1,3,{username:'cobe'}]
let arr2 = JSON.parse(JSON.stringkify(arr))
arr2[2].username = 'wobe'
console.log(arr[2].username)//code 还是原值

这种方法虽然可以实现对象或数组的深拷贝,但是不能处理函数、Date和Regexp内置对象

  1. 手写递归
function deepClone(obj,hash = new WeakMap()){
    if(obj === null) return obj;
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj);
    //可能是对象或普通值 如果是函数的话是不需要深拷贝
    if(typeof obj !=="object") return obj;
    //是对象的话进行深拷贝
    if(hash.get(obj)) return hash.get(obj)
    let cloneObj = new obj.constructor();
    //原型上的constructor指向当前类本身
    hash.set(obj,cloneObj);
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            cloneObj[key] = deepClone(obj[key],hash)
        }
    }
    return cloneObj
}

数组的常用方法

  1. 可以改变数组的方法: pop()、push()、shift()、unshift()、splice(index, num,value)、sort()、reverse()
  1. 不能改变数组的方法:slice()、 concat()
  1. 查找数组方法find 返回查找的对象 findIndex返回查找对象的下标
  1. 数组的遍历方法

    1. forEach(cb(val,index,arr),this) 遍历操作每个成员,没有返回值
    2. map(cb(val,index,arr),this) 遍历操作每个成员,返回值构成数组
    3. some(cb(val,index,arr),this) 全部成员执行回调,一个满足就返回true
    4. every(cb(val,index,arr),this) 全部成员执行回调并都返回true, 结果才返回true
    5. filter(cb(val,index,arr),this) 过滤得到满足条件的数组
    6. reduce(cb(val,index,arr),this) 累积
    7. reduceRight(cb(val,index,arr),this)从右到左累积

手写forEach方法

 Array.prototype.forEach = function(callback,thisArgs){
     if(this === null){
         throw new Error('this is null or not defined ')
     }
     if(typeof callback!== 'function'){
         throw new Error('callback is not a function')
     }
     const O = Object(this);
     const len = O.length >>> 0;
     while(k < len){
         if(k in O){
             callback.call(thisArgs,O[k],k,O);
         }
         k++;
     }
 }

手写map方法

Array.prototype.map = function(callback,thisArgs){
    if(this === null){
         throw new Error('this is null or not defined ')
     }
     if(typeof callback!== 'function'){
         throw new Error('callback is not a function')
     }
     const result = []
     const O = Object(this);
     const len = O.length >>> 0
     while(k < len){
         if(k in O){
             result[k].push(callback.call(thisArgs,O[k],k,O))
         }
         k++;
     }
     return result;
}

手写some方法

Array.prototype.map = function(callback,thisArgs){
    if(this === null){
         throw new Error('this is null or not defined ')
     }
     if(typeof callback!== 'function'){
         throw new Error('callback is not a function')
     }
     let k=0,res = false;
     let O = Object(this);
     let len = O.length;
     while(k < len){
         if(k in O){
             if(callback.call(thisArgs,O[k],k,O)){
                 return true;
             }
         }
     }
}

手写every方法

Array.prototype.every = function(){
    if(this === null){
         throw new Error('this is null or not defined ')
     }
     if(typeof callback!== 'function'){
         throw new Error('callback is not a function')
     }
     let k=0;res = false;
     let O = Object(this);
     let len = O.length
     while(k < len){
         if(k in len){
             if(!callback.call(thisArgs,O[k],k,O)){
                 return false
             }
             if(k == len){
                 return true;
             }
         }
         k++;
     }
}

手写filter方法

Array.prototype.every = function(){
    if(this === null){
         throw new Error('this is null or not defined ')
     }
     if(typeof callback!== 'function'){
         throw new Error('callback is not a function')
     }
     let k=0;res = [];
     let O = Object(this);
     let len = O.length
     while(k < len){
         if(k in len){
             if(callback.call(thisArgs,O[k],k,O)){
                 res.push(O[k])
             }
         }
         k++;
     }
     return res;
}

手写reduce方法 简易版

Array.prototype.reduce = function(reducer,initialValue){
    const hasInitial = arguments.length > 1;
    let res = hasgInitial?initialValue:this[0];//判断是否指定初始值
    for(let i=hasInitial?0:1;i<this.length;i++){
        ret = reducer.call(undefined,ret,this[i],i,this)
    }
    return ret;
}

字符串常用的方法

增:str.concat()

删:str.slice()、str.substr()、str.substring()

改: trim()、trimLeft()、trimRight()、repeat() 、replace('a','b')、padStart(len,'fill')、padEnd(len,'0') 、toLowerCase()、toUpperCase()、search(Regexc)

查: chatAt(index)、indexOf(val)、startWith('dd')、includes(str)

分割:split('+') 分割成数组

作用域和作用域链

作用域在代码运行时某些变量、函数和对象的可访问性。作用域可以隔离变量,不同的作用域可以有相同的变量名,不会发生冲突。

作用域有:

  • 全局作用域: 可以在整个js脚本任意位置访问,一般生命在函数和花括号之外的地方
  • 函数作用域:在函数内部声明的,只能在函数内部访问,在函数以外访问会报错。
  • 块级作用域:ES6中新增letconst声明的变量,在花括号内声明的,只能在花括号内使用。

词法作用域,又称静态作用域,即变量的作用域访问在其创建的时候已经确定,而非在运行时确定,js遵循的就是词法作用。

作用域链:当js中访问一个自由变量时,js引擎尝试在当前作用域在查找该变量,如果没找到,再到它的上一层级去寻找,以此类推直到查找全局作用域,如果全局作用域都没有,抛出错误。

**闭包**的理解及使用场景:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。即闭包可以让开发者从内部函数访问外部函数的作用域。再JavaScript中,闭包随函数的创建同时被同时创建。

使用场景:

  • 创建私有变量
(function(){
var private Counter = 0;//私有变量
function changeBy(val){//私有函数(公共函数)
    privateCounter += val;
}
return :{//闭包使用私有变量
    increment:function(){
        changeBy(1);
    }
    decrement:function(){
        changeBy(-1)
    }
    value:function(){
        return privateCounter;
    }
}
})();
  • 延长变量的生命周期

一般函数的词法环境在函数执行完成后被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建词法环境依然存在,达到延长生命周期的目的。

比如柯里化函数、延迟调用函数、计时器

注意:处理一些特定情况下下需要使用到闭包,在其他函数中创建函数(闭包)是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能有负面影响。

执行上下文和执行栈

执行上下文是一种对JavaScript代码执行环境的抽象概念,也就是说js代码运行一定是在执行上下文中。

执行上下文分为:

  1. 全局执行上下文:只有一个,全局对象this
  1. 函数执行上下文:函数在执行前会创建新的执行上下文
  1. Eval函数执行上下文:运行在eval函数中的代码

生命周期:创建阶段 => 执行阶段 =>回收阶段

创建阶段:在函数被调用,但执行之前

  • 确定this的值,即This Binding
  • LexicalEnviroment(词法环境)组件被创建
  • 词法环境有两个组成部分:环境记录器[EnvironmentRecord]外部引用[outer]
    • 环境记录器存储变量和函数声明的实际位置
    • 外部环境引用意味着可以访问全局执行上下文
  • VariableEnvironment(变量环境)组件被创建

变量环境同样有两部分组成:环境记录器和外部引用

在ES6中,词法环境和变量环境的区别在于前者用于存储函数声明和变量(letconst)绑定,后者用于存储变量var绑定

比如:

let a = 20;
const b = 30;
var c;
function multiply(e,f){
    var g = 20;
    return e * f *g
}
c = multiply(20,30)

执行上下文

GlobalExectionContext = {//全局执行上下文
    ThisBinding:<Global Object>,
    LexicalEnvironment:{ //词法环境
        EnvironmentRecord:{//环境记录器
            Type:"Object",
            a:< uninitialized>,
            b:< uninitialized>,
            multiply:<func>
        }
        outer:<null>//外部指针
    }
    VariableEnvironment: {// 变量环境
        EnvironmentRecord: {//环境记录器
          Type: "Object",  
          // 标识符(变量名)绑定在这里  
          c: undefined,  
        }  
        outer: <null> //外部指针
      }  
}
FunctionExectionContext = {//函数执行上下文
  ThisBinding: <Global Object>,

  LexicalEnvironment: {//词法环境
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},//arguments
    },  
    outer: <GlobalLexicalEnvironment>//外部指针指向全局词法环境
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

所以letconst在执行上下文创建阶段并没有被赋值,而是< uninitialized>,但是var声明的变量在创建时已经被赋值为undefined, 所以var声明的变量存在变量提升,而letconst没有变量提升。

执行阶段

执行变量赋值、代码执行,如果引擎找不到变量的实际值,分配undefined

回收阶段

虚拟机回收执行上下文

执行栈

执行栈,也称为调用栈,具有后进先出结构,用于存储在代码执行阶段创建的所有上下文。

过程描述:js引擎执行js脚本时,会创建一个全局上下文,并将它压进执行栈,每当引擎碰到一个函数,它就会创建一个函数执行上下文并压入执行栈;引擎会先执行栈顶的上下文, 当该函数执行完后,对应的执行上下文就会被弹出,然后再执行下一个函数,以此类推。

this关键字

函数运行时自动生成的一个内部对象,只能在函数内部使用,总是指向调用它的对象。

  • 函数全局调用:指向全局对象
  • 函数实例调用:指向对象
  • new构造函数调用:指向新创建的实例对象
  • 箭头函数:this是编译时确定,总是指向自己的上一层作用中的this

显式绑定this: apply, call, bind

相同点:都可以用来显示改变运行时函数this的指向

不同点:

  • apply接受两个参数,第一参数是this的指向,第二个参数是函数接受的参数,以数组方式传入。当第一个参数是nullundefined,默认指向window
  • call第一个参数也是this的指向,后面传入的是一个参数列表
  • bind方法和call方法相似,但是bind方法在改变this指向之后不会立即执行,而是返回一个绑定好this指向的函数,而call和apply是立即执行。

手写call 多个参数

Function.prototype.Call = function(thisArgs,...args){
    // this是undefined和null的情况
     const context = thisArgs?Object(thisArgs):window;
     let result;
     const symbol = Symbol();
     context[symbol] = this;//fn
     let arr = args?args:[];
     let symbols = Object.getOwnPropertySymbols(context);
     for(let key of symbols){
         result = context[key](...arr);//执行函数
         delete context[key]
     }
     return result;
}
let o = {name:'lisi'}
function b(){
    console.log(this.name)
}
b.Call(o,'1','2')//lisi

手写apply 一个数组

Function.prototype.Apply = function(thisArgs,args){
    const context = thisArgs?Object(thisArgs):window;
    let result;
    const symbol = Symbol();
    context[symbol] = this;
    let arr = args?args:[];
    const symbols = Object.getOwnPropertySymbols(context);
    for(let key of symbols){
        result = context[key](...arr);
        delete context[key];
    }
    return result;
}

let o = {name:'lisi'}
function b(){
    console.log(this.name)
}
b.Apply(o,['1','2'])//lisi

手写bind 返回一个函数,传参类似call

Function.prototype.Bind = function(thisArgs,...args){
    const context = thisArgs?Object(thisArgs):window;
    const symbol = Symbol();
    context[symbol] = this;
    let arr = args?args:[];
    return function(...args1){
        let symbols = Object.getOwnPropertySymbols(context);
        let result;
        for(let key of symbols){
          console.log(key)
           result = context[key](...arr,...args1)
           delete context[key]
        }
        return result;
    }
}
let o = {name:'lisi'}
function b(){
    console.log(this.name)
}
b.Bind(o,'1','2')()//lisi

原型和原型链

原型:规范定义,是给其他对象提供共享属性和方法的对象。 理解:Js是基于原型的语言,而不像Java是基于类的。JavaScript中的每个对象都有一个原型属性__proto__,这个原型属性指向它的构造函数的原型对象prototype

原型链:每个实例对象都有一个私有的__proto__属性指向它构造函数的原型对象prototype。该原型对象也有一个自己的原型属性__proto__,层层向上直到一个对象的原型属性为null,即原型链的最顶层null, 没有原型。

let f = new Function();
f.__proto__ == Function.prototype//true
f.constructor  === Function//true 每个实例都有一个construtor属性,指向它的构造函数
Function.prototype.__proto__ == Object.prototype;//true
  • 一切对象都继承自Object对象,Object对象直接继承根源对象null Object.prototype.__proto__ === null //true
  • 对象有普通对象和函数对象。 一切函数对象(含Object对象),都继承自Function对象

Object.__proto__ == Function.prototype

  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象· Function.prototype === Function.__proto__
  • 每个对象都有__proto__属性,但只要函数对象才有prototype属性
  • 每个prototype对象都有一个constructor属性指向自身构造函数。

继承

继承可以使得子类具有父类的各种属性和方法,不需要再次编写相同的代码,子类可以重写父类方法和属性,可以定义定的方法和属性。

  1. 原型链继承

特点:继承父类方法和属性,多个子类实例共用一个原型,无法实现多继承

function F(){
    this.color = 'yellow'
}
function C(){
    this.type = 'child'
}
Child.prototype = new Father();
console.log(new Child())

JavaScrip基础 | 一文扫清知识盲点

原型式继承Object.create()方法

  1. 构造函数继承

特点:继承父类构造函数上的属性和方法,无法继承父类原型上属性和方法

function F(){
    this.color = 'yellow'
}
F.prototype.say = function(){
    console.log(this.color)
}
function C(){
    F.call(this);
    this.type = 'child'
}
Child.prototype = new F();
console.log(new C())

JavaScrip基础 | 一文扫清知识盲点

  1. 组合继承

特点:可以继承父类原型和构造函数的属性和方法,但是需要消耗两次父类,原型上生成了多个属性和方法

function F(){
    this.color = 'yellow'
}
F.prototype.say = function(){
    console.log(this.color)
}
function C(){
    F.call(this);
    this.type = 'child'
}
C.prototype = new F();
Child.prototype = new F();
console.log(new C())

JavaScrip基础 | 一文扫清知识盲点

  1. 寄生组合继承

特点:利用原型式继承方法解决new操作符消耗一次父类函数问题。

function F(){
    this.color = 'yellow'
}
F.prototype.say = function(){
    console.log(this.color)
}
function C(){
    F.call(this);
    this.type = 'child'
}
function inheritClone(C,F){
    let prototype = Object.create(F.prototype);
    prototype .constructor = C.constructor;
    Child.prototype = prototype;
}
inheritClone(C,F)
Child.prototype = new F();
console.log(new C())

JavaScrip基础 | 一文扫清知识盲点

原型式继承Object.create()原理

function create(o){
      function F(){}
      F.prototype = o.prototype;
      return new F();
}

new操作符

new操作符用于创建一个给定构造函数的实例对象

过程:

  1. 创建一个空对象
  1. 将这个空对象的原型属性_proto__指向构造函数的原型对象prototype
  1. 将构造函数的this指向该对象
  1. 执行构造函数,根据构造函数返回类型判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。

手写new

function _new(){
    //类数组转成数组
    const args = [].slice.call(arguments);
    //获得构造函数
    const constructor = args.shift();
    //创建一个空对象,将构造函数的prototype指向空对象的__proto__
    const context = Object.create(constructor.prototype)
    //执行构造函数
    const result = constructor.apply(content,args);
    //判断构造函数返回类型
    return (typeof result ==='object' && result !==null)?result:context;
}

**事件模型**有哪些?

js中的事件可以理解是在HTML文档或浏览器中发生的一种交互操作,使得网页具备互动性,常见事件有鼠标事件、点击事件、拖拽事件等。

DOM是一个树结构,如果父子节点绑定事件时,当触发子节点时,就存在一个执行顺序问题,这就涉及事件流。

事件流一般经历三个阶段:

  • 事件捕获(capture): 从上往下的传播方式,由点击父节点向子节点逐层触发事件
  • 目标阶段(target)
  • 冒泡阶段(bubbling): 从下往上的传播方式,由点击的子节点向父节点逐层冒泡

事件模型:

  • 原始事件模型

    • HTML中直接绑定
    • <input type="button" onclick="fun()">
      
    • JS代码绑定
    • var btn = document.getElementById('.btn');
      btn.onclick = fun;
      

特点:

  1. 绑定速度快:太快以至于页面有可能未加载出来
  1. 只支持冒泡,不支持捕获
  1. 一个节点只能绑定一次,再次绑定会覆盖之前的绑定
  • 标准事件模型

该模型中有三个阶段:事件捕获、事件处理、事件冒泡

绑定方式

var btn = document.getElementById('.btn');

btn.addEventListener(‘click’, showMessage, false);
btn.removeEventListener(‘click’, showMessage, false);

特点:

  1. 可以绑定多个事件,不会冲突
  1. 第三个参数true表示捕获,false表示冒泡,默认false
  • IE事件模型

该模型有两个过程:事件处理、事件冒泡

var btn = document.getElementById('.btn');
//绑定监听
btn.attachEvent(‘onclick’, showMessage);
//移除监听
btn.detachEvent(‘onclick’, showMessage);

特点:

  1. 该模型旨在IE浏览器有效,其他浏览器不兼容

事件代理(事件委托):把一个或一组元素委托到它的父层或更外层元素上。真正绑定事件的是外层元素,而不是目标元素。通过事件冒泡机制触发外层元素的绑定事件,然后在外层元素上执行函数。

应用场景:

如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件。

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

遍历每个列表项,给列表项绑定一个函数,十分消耗内存,这时可以把绑定事件绑定在父级元素ul上,然后去匹配元素。适合事件委托的事件有:clickmousedownmouseupkeydownkeyupkeypress

优点:

  • 减少整个页面所需的内存,提升整体性能
  • 动态绑定,减少重复工作

局限:

  • 对于focusblur这样的事件没有冒泡机制,无法进行委托
  • mousemovemouseout需要不断触发,对性能消耗极高,因此也不适合使用事件委托

AJAX的原理及实现

AJAX: 异步的JavaScript和XML,是一种创建交互网页应用的网页开发技术,可以不加载整个网页的情况下,与服务器交换数据,并且更新部分网页

原理:通过XMLHttpRequest对象来向服务器发送异步请求,从服务器获得数据。然后用JavaScript来操作DOM而更新页面。

实现过程

  • 创建XMLHttpRequest对象
  • 通过XMLHttpRequest对象的open()方法与服务器建立连接
  • 构建请求需要的数据内容,并通过send()发送给服务端
  • 通过onreadystatechange事件监听服务器返回的结果
  • 接受并处理服务端的响应数据
  • 使用JavaScript将结果更新到HTML

简单封装一个Ajax请求

ajax({
    type:'POST',
    dataType:'json',
    data:{},
    url:'https://xx.com',
    sucess:function(res){
        
    },
    fail:function(err){
    
     }
})

//此处封装
function ajax(options){
    const xhr = new XMLHttpRequest();
    //初始化参数
    options = options || {};
    //默认GET请求
    options.type = (options.type || 'GET').toUpperCase();
    options.dataType = options.dataType || 'json';
    const params = options.data;
    if(options.type === 'GET'){
        xhr.open('GET',options.url + '?'+params,true)
        xhr.send(null)
    }else{
        xhr.open('POST',options.url,true)
        xhr.send(params)
    }
    
    xhr.onreadstatechange = function(){
        if(xhr.readyState === 4){
            let status = xhr.status;
            if(status>= 200 && statis <300){
                options.success(xhr.responseText)
            }else{
                options.fail && options.fail(status)
            }
        }
    }
}

正则表达式用来匹配字符串的工具,也是一个对象。两种方式创建

  1. 字面量创建,在两个斜杠之间
const re = /\d+/g
  1. 条用RegExp对象的构造函数
const re = new RegExp("\d+","g");

校验规则(部分)

规则描述
^开始
$结束
*匹配前表达式0次或多次
+匹配前表达式1次或多次
?匹配前表达式0次或一次
.默认匹配除换行符之外的任何单个字符
\d匹配一个数字
\D匹配一个非数字
\s匹配空白字符,含空格、制表符、换页、换行符
\w匹配单个字符(字母、数字或下划线)
以下是标记
g全局
i忽略大小写
m多行
s.可以匹配换行符

字符串匹配方式:

  • match 返回一个数组
  • matchAll 返回一个迭代器
  • search 返回匹配到的索引
  • replace匹配字符替换目标字符
  • split按匹配字符替换指定内容

正则对象方法:

  • test匹配到,返回true/false
  • exec匹配到,返回数组

常见的**DOM操作方法**

DOM,即文档对象模型,是HTMLXML文档的编程接口,它提供了对文档的结构化描述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构、样式和内容。节点类型有很多,下面是常用的三种:

<div>
    <p title="title">
        content
    </p >
</div>

divp都是元素节点;title是属性节点;content是文本节点

操作:

  • 创建节点

    • createElement,接受一个标签名的参数,创建一个新元素节点
    • createTextNode 接受一个任意内容字符串,创建一个文本节点
    • createDocumentFragment 创建文档碎片,一种轻量级文档,用来存储临时节点
    • createAttribute 创建一个属性节点
  • 获取节点

    • querySelector 传入一个css选择器,返回一个元素节点
    • querySelectorAll 传入一个css选择器,返回一个节点列表
    • getElementById
    • getElementsByName
    • getElementsByTagName
    • getElementsByClassName
  • 更新节点

    • innerHTML 可以解析文本节点,也可以解析HTML判断
    • innerText、textContent html元素会进行编码,不能解析HTML。两者主要区别在于读取属性时,innerText不返回隐藏的文本,而textContent返回所有文本。
    • style 可以获取设置属性所有css document.style.fontSize = '20px'
  • 添加节点

    • innerHTML
    • appendChild 将一个子节点添加到父节点的最后一个子节点
    • insertBefore(newEle,referEle)将新节点添加到referEle之前
    • setAttributer('class','white')添加一个属性节点
  • 删除节点

    • removeChild 删除子节点

BOM的理解及常见的BOM对象

BOM,即浏览器对象模型,提供独立于内容与浏览器窗口进行交互的对象,比如页面后退、刷新、前进、窗口变化、滚动、屏幕分辨率等。

顶级对象是window,window即是浏览器窗口的一个接口、又是全局对象。

window属性方法

  • window.name 全局对象属性name
  • window.moveTo(x,y) 浏览器窗口相对于屏幕移动像素(px)
  • window.resizeTo(w,h) 窗体宽度调整
  • window.scrollTo(x,y) 滑动到x,y的位置

location

  • hash url中#后面字符
  • host域名+端口
  • hostname 域名,没带端口
  • href 完整url
  • pathname 路径
  • port 端口
  • protocol 协议
  • search ?后面的内容

navigator 对象用来获取浏览器的属性的,区分浏览器类型,属性较多,不一一列举

screen 客户端显示器的一些信息

  • screen.height 屏幕像素高度
  • screen.witdth 屏幕像素高度
  • screen.colorDepth 屏幕颜色位数

history主要用来操作浏览器URL的历史记录,可以通过参数向前、向后,或者向指定URL跳转

  • history.go() 接受整数或者字符串参数
history.go('maixaofei.com')
history.go(3) //向前跳转三个记录
history.go(-1) //向后跳转一个记录
  • history.forward():向前跳转一个页面
  • history.back(): 向后跳转一个页面
  • history.length: 获取历史记录数

本地**存储方式**有哪些?应用场景

  • cookie

Cookie是为了解决HTTP无状态的问题,用于在客户端保存在服务端验证用户身份的数据。

一般大小不超过4KB的小型文本数据。

  • localStorage

H5新增,IE8以上的浏览器都兼容

  • 特点:
    • 生命周期:持久化的本地存储,永久有效,除非手动删除
    • 存在跨域限制,但存储的信息可以在同一个域中是共享的
    • 大小5M , (浏览厂商)
    • localStorage本质是对字符串的读取,存储的内容越多,消耗内存越多,页面变卡
  • sessionStorage

sessionStorage和localStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage就会删除数据

  • indexedDB:用于客户端存储大量结构化数据(文件等)

    • 存储大小没有上限
    • 所有操作是异步的,向localStoage同步操作性能更高
    • 原生支持JS对象

应用场景:

  • 标记用户或跟踪用户行为,使用cooke
  • 适合长期保存在本地的数据(token) 使用localStorage
  • 敏感账号一次性登录,使用sessionStorage
  • 存储大量数据,在线文档保存编辑历史,使用indexDB

**Cookie、localStorage和sessionStorage**的区别

  • 存储大小:cookie数据大小不超过4k , 而localStorage存储的数据大小可达5MB
  • 生命周期:cookie可以设置到期事件,到期前一直有效;sessionStorage数据在当前浏览器窗口关闭后自动删除;localStorage永久有效,除非主动删除数据
  • 与服务器之间的交互方式cookie会被自动上传到服务器,服务器可以通过Set-Cookie改变cookie; sessionStorage和localStorage不会把数据发送给服务器,仅在本地保存。

事件循环

为什么要设计?JavaScript设计之初是单线程的,就是程序运行时,只有一个线程存在,同一时间只能做一件事,为了防止一些请求或高精度计算任务阻塞程序运行,于是有了事件循环

JavaScript在运行时,所有任务可分为

  • 同步任务:同步任务是立即执行的,一般直接在主线程中执行
  • 异步任务:异步任务,比如ajax请求, setTimeout计时器任务,是会被放到任务队列中,等待主线程任务执行完毕,才从任务队列中拿出异步任务放到主线程中执行。

这个主线程执行完同步任务,不断轮询任务队列的过程,就称为事件循环。

异步任务一般分为宏任务和微任务:

宏任务的执行事件粒度比较大,执行的时间不能精确控制

常见的宏任务有:script(外层同步代码)、setTimeout/setIntervalpostMessagerequestFrameAnimation、setImmediate、I/O

微任务执行时机是主线程同步代码执行之后,宏任务执行之前。

常见的微任务有:Promise.thenasync/await、MutationObserverprocess.nextTick(Nodejs)

执行机制

  • 主线先执行同步代码,如果遇到微任务将它放到微任务队列,遇到宏任务放到宏任务队列
  • 当主线程执行完同步代码。会查看微任务队列,然后将队列中的微任务依次执行完成,然后查看宏任务队列,依次执行完成。

**async/await**提供了一种异步解决方案,可以通过它实现同步编程代码,异步请求的的代码风格,await等待的是异步方法执行。

尾递归

尾调用是函数在最后调用另一个函数

函数在函数尾位置调用自身(或一个尾调用本身的其他函数等);尾递归优化:执行栈可以无需保留当前函数执行上下文,所以永远不会发生栈溢出的情况.

尾递归优化: 函数在内部调用一个函数会形成一个调用记录,保存了调用位置和内部变量等信息.如果函数在内部调用函数B,那么A的调用记录上方会形成一个对B的调用.以此类推,形成一个调用栈.

尾递归是函数在最后一步调用自身,无需保留外层函数的调用记录,因为调用位置和内部变量信息都不会再用到,只要直接使用内层函数的调用记录,取代外层函数的调用记录就可以了

应用场景:

数组求和

function sum(arr,total=0){
    if(arr.length === 0) return total;
    return sum(arr,total + arr.pop())
}

斐波那契数列求和

function factorial(n,start=1,total){
    if(n <=1 ) return total;
    return factorial(n-1,total,total + start)
}

数组扁平化

function flat(arr=[],result=[]){
    arr.forEach(v=>{
        if(Array.isArray(v)){
            result = result.concat(flat(v,[]))
        }else{
            result.push(v)
        }
    })
    return result;
}

对象格式化

let obj = {
    a: '1',
    b: {
        c: '2',
        D: {
            E: '3'
        }
    }
}
// 转化为如下:
let obj = {
    a: '1',
    b: {
        c: '2',
        d: {
            e: '3'
        }
    }
}
//代码实现
function keysLower(obj){
    let reg = new RegExp("([A-Z]+)","g")
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            let temp = obj[key];
            //有大写字母
            if(reg.test(key.toString())){
                //替换
                temp = obj[key.replace(reg,function(result){                    return result.toLowerCase();                })] = obj[key];
                delete obj[key];
            }
            if(typeof temp === 'object' || Array.isArray(temp)){
                keysLower(temp)
            }
        }
    }
}

Object.defiinePropery(obj,name,descriptor)

用来添加修改一个对象属性的方法

描述性对象含有两种描述符: 数据描述符和存储描述符

数据描述符:

  • value 设置对象属性的值
  • writable 设置对象属性是否可以重新赋值
  • Enumerable 属性是否可枚举,是否可以使用Object.keys或for..in遍历
  • configurable是否可以删除目标函数或者修改属性特性.

存取描述符

  • get getter
  • set settter

内存泄漏与垃圾回收

内存泄露是指由于开发者疏忽造成程序未能释放已经不再使用的内存

垃圾回收机制: js具有自动垃圾回收机制(Garbage Collection),即执行环境会负责管理代码执行过程中的内存.

原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存

两种垃圾回收方式

  • 标记清除

当变量进入执行环境时,就标记这个变量为"进入环境"进入环境的变量所占用的内存不能被释放掉,当变量离开缓存时,则将其标记"离开环境",垃圾回收机制会将其再上下文中的引用变量删除, 回收内存.

  • 引用计数

js用一张"引用表"保存所有变量的引用次数, 如果这个值得引用次数是0, 那垃圾回收机制可以回收该变量占用得内存里. 但有可能存在循环引用 , 造成内存泄露 .

常见的内存泄漏

  1. 全局变量

使用严格模式,可以避免意外得全局变量

function foo(arg) {
    bar = "foot";
}
//或
function foo() {
    this.variable = "this";
}
  1. 定时器
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

定时器依旧存在,而且回调函数中包含对someResource引用, 内存不会被释放

  1. 闭包

维持函数内局部变量

function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}
  1. 事件监听

比如使用addEventListener监听时,在不监听的情况下使用removeEventListener取消对事件监听,否则会造成内存泄漏.

**函数式编程**理解及优缺点

函数式编程是一种编程范式, 一种编写程序的方法论。

主要的编程范式有三种: 命令式编程、声明式编程、函数式编程

函数式编程强调程序执行的结果而非执行过程, 倡导使用若干简单的执行单元让计算结果不断渐进,逐层推到复杂的运算, 而非设计一个复杂的执行过程.

特点: 把过程逻辑写成一个函数,定义好输入参数,只关心它输出结果.

核心概念:

数据不可变:强调数据是不变的,这意味着如果你想修改一个对象,你必须创建一个新的对象修改它。

无状态:表示给定一个函数,无论何时运行,它都和第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

思想应用

  1. 纯函数:对给定的输入返回相同的输出的函数, 并要求你所有的数据都是不可逆的, 即纯函数 = 无状态 + 数据不可变
  1. 高阶函数:以函数作为输入或输出的函数称为高阶函数 (传入一个函数,返回一个被增强后的函数)
  1. 柯里化函数:把一个参数函数转化成一个嵌套的一元函数的过程(松散解耦)
  1. 组合与管道: 将多个函数组合成一个函数. compose执行是从右到左的.而管道函数执行顺序从左到右的

优点:

  • 复用代码,没有其他外部变量影响,无副作用
  • 更好的状态管理:无状态,更少的状态,能最大化的减少这些未知、优化代码、减少出错情况

缺点:

  • 函数式编程相对于命令式编程的性能开销更大
  • 资源占用
  • 递归陷阱:在函数式编程中,为了迭代,通常会采用递归操作

函数缓存

就是使用函数缓存过的结果进行缓存,就是用空间(缓存存储)换事件(计算过程) 实际上就是使用闭包实现的

  • 闭包
  • 柯里化:将接受多个参数的函数换成接受一个单一参数的函数
  • 高阶函数

适合使用缓存

  • 对于昂贵的函数调用,执行复杂计算的函数
  • 对于具有有限且高度重复输入范围的函数
  • 对于具有重复输入值的递归函数
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数

防抖和节流

浏览器的resizescrollkeypressmousemove等事件在触发时,会不断地触发回调函数,极大浪费资源,消耗性能。

防抖:n秒后在执行该事件,若在n秒内被重复触发,则重新计时(最后一次触发生效)

节流:n秒内只运行一次,若在n秒内重复触发,只有一次生效

节流:高频触发事件,只在一段时间内执行一次

function throttled(fn,delay=500){
    let timer= null;
    return function(...args){
        if(!timer){
            timer = setTimeout(()=>{
                fn.apply(this,args)
                timer =null;
            },delay);
        }
    }
}

防抖:一定时间内连续触发的事件,只是在最后一次执行,而函数节流一段事件内只执行一次

function debounce(fn,delay){
    let timer = null;
    return function(...args){
        if(time) clearTimeout(timer)
        time = setTimeout(()=>{
            fn.apply(this,args);
            timer = null;
        },delay)
    }
}

防抖:在连续事件,只需触发一次回调

  • 搜索框输入,用户最后一次输入完成,才发送请求
  • 手机号、邮箱验证
  • 窗口大小resize。当窗口调整完成后,再计算窗口大小

节流:在间隔一段事件执行一次回调的场景

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

如何判断一个元素出现在可视域内?(图片懒加载

  1. offsetTop、scrollTop
function isInView(el){
    const viewPortHeight = window.innerHeight ||
        document.documentElement.clientHeight|| 
        document.body.clientHeight;
    const offsetTop = el.offsetTop;//当前元素距离最上面的元素
    const scrollTop = document.documentElement.scrollTop;
    return top <= viewPortHeight;
}
  1. getBoundingClientRect
function isInView(el){
    const viewWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    const {
        top,
        left,
        bottom,
        right
    } = el.getBoundingClientRect();
    return (
        top>=0 && 
        left>=0&&
        right <=viewWidth&&
        bottom <= viewHeight
    )
}
const el = document.querySelect('p')
window.addEvemtListener('scroll',function(e){
    if(isInView(el)){
        ...
    }
})
  1. Intersection Observer 判断两个元素是否重叠,性能比getBoundingClientRect
const observer = new IntersectionObserver(function(changes){
   changes.forEah(function(ele,index){
       if(ele.intersectionRation > 0 && ele.intersectionRatio <=0){
           ele.target.src = ele.target.dataset.src;
       }
   }) 
});

(function addObserver(){
    var listItems = document.querySelectorAll('[data-src]');
    listItems.forEach(function(item){
        observer.observe(item);//观察该节点
    })
})()

应用

  • 图片懒加载
  • 列表的无线滚动
  • 计算广告元素的曝光情况
  • 可点击链接的预加载

下拉刷新和上拉加载

上拉加载

JavaScrip基础 | 一文扫清知识盲点

触底时,满足scrollTop + clientHeight >= scrollHeight,clientHeight是定值,变得是scrollTop;而scrollHeight表示body所有元素的长度。

//浏览器高度
let clientHeight = document.documentElement.clientHeight;
//body所有元素的长度
let scrollHeight = document.body.scrollHeight;
//body顶部距离window顶部的长度
let scrollTop = document.documentElement.scrollTop;
let distance = 50;//距离视窗50时,开始触发

if((scrollTop + clientHeight) >= (scrollHeight - distance)){
    console.log('开始加载数据')
}

\

下拉刷新: 页面本身置于顶部,用户下拉时触发动作

实现步骤:

  • 监听原生touchStart事件,记录其初始位置的值e.touches[0].pageY
  • 监听touchmove事件,记录并计算当前滑动的位置值与初始位置的差值,大于0表示下拉动作,同时设置一个允许滑动的最大值
  • 监听touchend事件,若此时元素达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置
<main>
    <p class="refreshText"></p>
    <ul id="refreshContainer">
        <li>111</li>
        <li>222</li>
        <li>333</li>
        <li>444</li>
        <li>555</li>
        ...
    </ul>
</main>
<script>
    var _element = document.getElementById('refreshContainer'),
        _refreshText = document.querySelector('.refreshText'),
        _startPos = 0,//初始位置
        _transitionHeight = 0;//移动的距离
    _element.addEventListener('touchstart', function (e) {
        //记录初始位置
        _startPos = e.touches[0].pageY;
        _element.style.position = 'relative';
        _element.style.transition = 'transform 0s';//元素开始移动
    }, false)
    _element.addEventListener('touchmove', function (e) {
        //e.touches[0].pageY 当前滑动到的位置
        _transitionHeight = e.touches[0].pageY - _startPos //记录当前滑动距离
        if (_transitionHeight > 0 && _transitionHeight < 60) {
            _refreshText.innerText = '下拉刷新';
            //元素跟着偏移
            _element.style.transform = 'translateY(' + _transitionHeight + 'px)';
            if (_transitionHeight > 55) {
                _refreshText.innerText = '释放更新'
            }
        }
    }, false)
    _element.addEventListener('touchend', function (e) {
        _element.style.transition = 'transform 0.5s ease 1s';
        _element.style.transform = 'translateY(0px)';
        _refreshText.innerText = '更新中...'
    })
</script>

使用第三方库better-Scroll实现,具体看官网。