深入了解js

122 阅读13分钟

JS基本概念

JS语言特点

单线程:GUI线程和JS线程互斥 动态、弱类型:变量在使用之前无需申明类型;与之对应的是静态语言(强类型语言)-编译时变量的数据类型就需要确定。 面向对象、函数式:参数->结果;副作用;纯函数;原型、继承、封装; 解释类语言、JIT 安全、性能差

JS数据类型

JS数据类型.jpg

JS作用域

变量的可访问性和可见性

静态作用域,也叫词法作用域,通过它就能够预测代码在执行过程中如何查找标识符。

变量提升

var有变量提升; let、const没有变量提升,提前访问会报错; function函数可以先调用再定义; 赋值给变量的函数无法提前调用;

JS执行

JS是怎么执行的

JS执行过程.jpg

字节码的代码量比机器码少得多,可以节省内存开销。

优化代码部分:JIT;相同的代码只要出现两次及以上,V8就认为是热代码(多次出现的代码),就将其直接转成机器码,再存储起来,下次再遇到就直接执行,就不需要再转为字节码了。

JS执行上下文

当JS引擎解析到可执行代码片段(通常是函数调用) 的时候,就会先做一些执行前的准备工作,这个准备工 作,就叫做“执行上下文(execution context简称 EC)",也叫执行环境。

JS执行上下文.jpg

在JS中,执行上下文包括全局执行上下文、函数执行上下文、Eval执行上下文。

·全局执行上下文:代码开始执行时就会创建,将他压执行栈的栈底,每个生命周期内只有一份。 ·函数执行上下文:当执行一个函数时,这个函数内的代码会被编译,生成变量环境、词法环境等,当 函数执行结束的时候该执行环境从栈顶弹出。

JS的调用栈(存放各种执行上下文的一个栈)是有最大上限空间的。所以注意代码不要出现过大的循环或递归。

JS调用栈.jpg

JS一开始执行代码,首先就创建一个全局执行上下文放到调用栈的栈底,遇到函数调用再创建函数的执行上下文,并依次入栈。

创建执行上下文的时候做了什么?绑定This、创建词法环境、创建变量环境。

词法环境:放函数。基于ECMAScript代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法 环境由环境记录器和一个可能的引用外部词法环境的空值组成。 变量环境:放变量。变量环境和词法环境的一个不同就是前者被用来存储函数声明和变量(let和const)绑定, 而后者只用来存储var变量绑定 Outer:指向外部变量环境的一个指针,找上一级的。

JS变量环境.jpg

基础数据类型存变量里边,复杂数据类型存一个内存地址,该地址指向堆空间的一个内存空间。

ESP:执行当前函数时,ESP会执行当前函数的执行上下文;等到该函数执行完毕后,该函数的执行上下文会被它上面/上一级的执行上下文覆盖掉(该函数成为无效内存,回收),此时ESP会指向它的上一级的执行上下文。

ESP.jpg

闭包

闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。

闭包.jpg

showName返回一个函数,该函数调用外层函数showName作用域里的变量,按理说showName执行完成,函数showName的执行上下文会被showName返回的函数的执行上下文覆盖掉,showName里边的变量就不能被访问,但返回的函数访问了showName里的变量,此时就形成了一个闭包;闭包让返回的函数能访问它的外部作用域(showName的作用域)。

是闭包的话,就会创建闭包,创建的闭包就是一个地址,指向堆上的一块内存空间,里边保存了返回函数需要的变量内容。因为这个闭包(内存空间)一直跟随getName这个变量的生命周期结束,所以即使showName执行完成,但这块内存空间也不会被回收。闭包本质就是一块没有被回收的对象。

作用域链

作用域链.jpg

this

this.jpg

JS垃圾回收

堆回收、栈回收。

栈回收:无效的执行上下文被它的上一级的执行上下文覆盖掉/回收后,ESP指针就指向上一级的执行上下文。

堆回收:新生代 和 老生代

JS垃圾回收.jpg

新生代:放一些小内存的变量,如果变量的空间非常大就会放到老 生代空间里。

新生代垃圾回收机制:把新生代划分为两个空间:对象区域(放活跃的变量)空闲区域;对象区域满了之后会对垃圾进行一次标记,然后把活跃的变量放到空闲区域里,最后把这两个区域进行反转,之后再把反转后的空闲区域里的变量清除掉即可。

老生代里边的数据来源->数据太大,新生代放不下就放到了老生代;新生代里的变量经历了两轮还没被回收的就放到老生代里。

老生代垃圾回收机制(主垃圾回收器):标记清除、内存整理。

先对垃圾进行一次标记;再执行清除操作,清除完成后会有不连续的碎片空间;再对碎片进行整理,变成连续的内存空间。

老生代空间很大,标记清除很费时,所以标记清除时JS引擎会停顿(避免清除时又引用了这些数据)。为了减少JS引擎停顿时间过长,所以主垃圾回收器就把整个的标记任务分成了一个个小的碎片任务,一个个去执行标记操作(标记一个JS执行一会),全标记完成后最后再执行清除操作。

总结

1.JS是单线程的,但是Render进程里面有多个线程 2.JS线程和GUI线程互斥,执行大的计算任务会导致页面卡顿 3.基础数据类型存在栈上,复杂数据类型存在堆上 4.const、let没有变量提升,提前使用会报错 5.JS也有编译的过程,执行之前会生成执行上下文 6.一个执行上下文包括变量环境、词法环境、this 7.变量环境里面有一个指向外部函数执行上下文的指针,形成了作用域链 8.全局执行上下文只有一份 9.this和执行上下文绑定

JS编码原则之组件封装

组件封装设计原则.jpg

行为:JS 控制流

JS行为(控制流).jpg

重构-插件化(解耦):

插件化(解耦).jpg

重构-HTML的模板化:

模板化.jpg

重构-抽象化:

抽象.jpg

JS编码原则之过程抽象

过程抽象:

过程抽象.jpg

普通高阶函数Once:

Once.jpg

function once(fn){
    //fn为传递进来的函数
  return function(...args){
    if(fn){
      const ret = fn.apply(this, args); 
      fn = null; 
      return ret;
     }
  }
const foo=once(()=>{
    console.log("打印一次")
})
foo()
foo()
foo();//不管调用几次均只打印一次,这个需求被剥离出来了,称为过程抽象

高阶函数HOF

HOF.jpg

function HOF0(fn){//这里HOF0可以认为是等价函数,fn和HOF0返回的函数是等价的;一般的高价函数都是在HOF0的基础上做了一些修改的
  return function(...args){ 
    return fn.apply(this, args);
   }
}

常用高阶函数

**HOF : **

​ Once Throttle : 节流函数 Debounce : 防抖函数 Consumer/2 Iterative

function debounce(fn,dur){
   dur=dur||100
   let timer
   return function(){
     clearTimeout(timer)
       timer=setTimeout(()=>{
           fn.apply(this,arguments)
       },dur)
   }
}
//这样写,iterative函数可以实现操作多个dom
const isIterable = obj => obj != null && typeof obj[Symbol.iterator]==='function';

function iterative(fn){
  return function (subject, ...rest){
    if(isIterable(subject)){//如果可迭代
      const ret =[];
      for(let obj of subject){
        ret.push(fn.apply(this, [obj, ...rest]))
      }
      return ret;
    }
  return fn.apply(this, [subject, ..rest]);
}
const setColor =iterative((el,color)=>{
    el.style.color = color;
});
const els = document.querySelectorAll('li:nth-child(2n+1)'); setColor(els, 'red');

纯函数

输入确定->输出确定

一般高阶函数都是一个纯函数

纯函数.jpg

//add就是一个纯函数
function add(x,y){
    return x+y;
}
add(3,4);//7

//count就不是纯函数,是有副作用的;因此,一个系统中非纯函数越多,系统维护难度越大
let index=0
function count(){
    return index++
}
const result=count();//这里每调用一次index都会改变

编程范式

编程范式2.jpg

//声明式:
//点击按钮switcher,改变按钮的颜色和文字(.off  .on的类名css属性不同)
function toggle(...actions){
    return function(...args){
        let action = actions.shift(); 
        actions.push(action);
        return action.apply(this, args);
    }
}
switcher.onclick = toggle(
evt => evt.target.className = 'off',
evt => evt.target.className = 'on'
);

//命令式:
switcher.onclick = function(evt){
   if(evt.target.classNane === 'on'){
     evt.target.classNane = 'off';}
   else{
     evt.target.className = 'on';
   }
}

repeat函数

快速幂实现的

function repeat(string, count)(
 var n = count;
 // Account for out-of-bounds indices
 if (n<0||n==Infinity)(//先判断n是否正确
    //报错
  throw RangeError('String.prototype. repeat argument must be     greater than or equal to  an')
    }
    
  var result = '';
  while (n){
   if(n%2==1){
     result+= string;//*
   }
   if(n>1){
     string+=string;
   }
    n>>=1;//n向右位移1位并赋值给n
)
return result;
}
console.log(repeat('*',5));
//n -> 101->5    str-> **    result-> *
//n -> 10 ->2    str> ****   result-> *
//n -> 1  ->1    str->****   result-> *****

JS代码质量优化

判断是否是4的幂

//方法 1
function isPowerOfFour(num) {
    num= parseInt(num);
    while(num>1){//判断num是否能被4整除
      if(num%4) return false;//如果不能被4整除返回false
      num /= 4; //如果能被4整除,就整除一次再循环判断直到最后num=1
    }
    return  num===1;
}

//方法2:对方法1的优化
function isPowerOfFour(num){
    num = parseInt(num);
    while(num>1){
    //num和二进制11进行&与运算(遇0为0,全1为1)来判断最后两位是否是00;如果不是00表示num不能被4整除返回false退出退出循环;如果是00表示num能被4整除,再使num右移两位再循环判断
        if(num & 0b11) return false; //0b11表示二进制的11;这里的return false表示整个函数直接返回false
        num >>>=2;
    }
    return num===1;
}

//方法三:最终优化 时间复杂度为O(1)
function isPowerOfFour(num){
   num = parseInt(num);
   return num > 0 && (num &(num -1))===0 &&
   (num & 0xAAAAAAAAAAAAA) ===0;
}

//方法4:正则表达式匹配
function isPowerOfFour(num) {
    num = parseInt(num).toString(2);//直接转为二进制字符串
    return /^1(?:00)+s/.test(num);//匹配1后边偶数个0(表示4^n)
}

二进制&运算:

a &(a-1)

一个二进制数 & 该二进制数 - 1 ->使得该二进制数的1减少一个

x.......1{k个0} & x.......0{k个1} -> x........ {k+1个0}

2的幂的数4的幂的数的二进制均只有一个1

因此,如果num是4^n ,那和4^n 进行&运算的话,一定会把4^n的二进制唯一的最高位1减去,所以结果一定为二进制的全0;反之num如果不是4^n,结果就不为0

前两个判断条件:因为4^n一定是2^n,但2^n不一定是4^n,所以有这两个条件的话:num > 0 && (num &(num -1))===0 可以判断成num一定是2^n的数。

第三个判断条件:二进制数num如果是4^n,则它的最高位1后边一定有偶数个01......{2n个0} ;即偶数位上不能为1。因此,可以让num & 0101010....10(即偶数位上为1的数,十六进制表示为0xAAAA...AA)===0来判断。这里多少个A看js的整数精度为几位(js:64位浮点数,其中有 1位为符号位;11位为小数精度;52位为整数精度),需要13个A

js浮点数.jpg

洗牌算法

错误写法:这里用sort方法,但sort方法中洗牌后的越靠前边的索引值的数被换到后边的概率比留在当前位置的概率要小。

const cards = [0, 1, 2, 3, 4, 5, 6,7, 8,9];
function shuffle(cards){
	return [...cards].sort(()=>Math.randon()>0.5? -1: 1):
}
console.log(shuffle(cards));

//这里试着将洗牌后的每一位的数据相加,如果概率大概相等的话,他们的总数据和也大概相等。
const result = Array(10).fill(0);
for(let i= 0;i <1000000;i++){ 
    const c= shuffle(cards); 
    for(let j= 0;j<10;j++){
        result[j] += c[j];
    }
}
console.table(result);

洗牌错误写法.jpg

正确写法:O(n)

算法:在cards里随机抽取一张牌,将其放到cards末尾,再在剩下的牌里再抽取 一张放到cards末尾,直到每一张都抽取过一次。

const cards =[0,1,2,3,4,5,6,7,8,9];
//如果只有两种牌: [a, b] ;抽到b的概率:[b, a] = 50;抽到a的概率:[a, b] = 50
//k 1/k
/ [a1, a2.. ak]/ (1.k-1) P=(k-1)/k*1/(k-1)
function shuffle(cards)( const c= [...cards]; forllet i= c.length; i>0; i-)( const pIdx Math.floor(Hath.randoal)*i); lc[pIdx], c[i-1]= [cli-1], c[pIdx]l;
)
return c;
)
console.log(shuffle(cards));
const result = Array(10).fill(0);
for(let i =0;i 1ee0e;i++){ const c = shuffle(cards); forllet j=:j 10:j++)( result[j] += cljl;
)
)
console.table(result);

进一步的改进,适应需求,比如抽奖,只在100个里随机抽取5个

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8,9];
//创建一个生成器函数
function * draw(cards){
    const c= [...cards]; 
    for(let i= c.length; i>0; i--){
       const pIdx= Math.floor(Math.randon()*i); 
        [c[pIdx], c[i- 1]]= [c[i-1], c[pIdx]];
        yield c[i - 1];
)
)
const result = draw(cards);
//console.log([...result]);//洗完整个cards
//console.log(result.next().value);//只取一张牌
console.log(result.next().value,result.next().value,result.next().value);    //取三张牌确定前三名

洗牌的正确方式.jpg

红包生成器-随机红包金额

不能完全随机(金额之间差距不能过大、不能不够分)

切西瓜法

先切一刀成两个,取大的那一个再切;一直切大的那一块

该算法分的相对均匀

O(m*n)

//amount:初始数据;count:分成的总份数
function generate(amount, count){
   let ret = [amount];//保存所切分的内容
   while(count>1){
     let cake = Math.max(..ret),//先挑出最大一块
     idx = ret.index0f(cake), //记录大块的索引值
     part = 1+ Math.floor((cake / 2) * Math.random()),//把大块的切成两块,并记录其中的一块
     rest = cake - part;//记录剩下的那块
     ret.splice(idx,1,part,rest);//替换旧内容,保存切分的新内容
     count--;
   return ret;
)

抽牌法

分的不均匀,相互之间差距大

如果cards是一个长度为10000的数组,其索引值为0~9999;

对0~9999之间随机插入分隔符,插入多少取决于分的次数

比如:需要分成10份,就需要插入9次(这里可以使用抽牌算法来抽取9张牌),再对这9个数进行排序来作为分隔符

//amount:初始数据;count:分的次数=取的牌数
function generate(amount, count){
    //这里以索引为0~9999的数组为例
  if(count <1)return [amount];
  //cards:生成随机数列,索引为0~9999,值为1~10000
  const cards = Array(amount -1).fill(0).map((_, i)=>i+1);
  //这里draw为取牌算法 的生成器函数
  const pick = draw(cards);//从数列中去取一张牌作为分隔符
  const result =[];//保存分成的金额数
  for(let i=0; i<count; i++){//取的牌数为count个,分成count份
     result.push(pick.next().value);//将取的牌/分隔符的值放到result数组里
  }
  result.sort((a, b)=>a-b);//对分隔符/牌 进行排序
  result.push(amount);
  for(let i = result.length - 1; i>0; 1--){
      //分割成的金额数=当前分隔符的值-上一个分隔符的值
     result[i] = result[i] - result[i-1];
  }
  return result;
}

这里pick.next():Generator生成器 实例的 next() 方法返回一个包含属性 donevalue 的对象。你也可以通过向 next 方法传入一个参数来向生成器传一个值。