《JavaScript》JavaScript进阶知识点(一)

《JavaScript》JavaScript进阶知识(一)

简介

为了更好的学习JavaScript,本系列旨在整理一些不属于基础知识的知识点,以便于在工作中可以灵活应用,使得代码开始追求质量,追求可靠性;

知识点

立即执行函数

立即执行函数,简单的说,就是定义了一个函数,并且立刻执行了,这么做的目的是什么呢?乍一看好像没什么意义,还多此一举了,既然需要立刻执行,那么干嘛不直接写在JS主体里?

实际上,立即执行函数解决了一个最大的问题,就是变量污染,在ES6之前是没有块级作用域的概念的,那么不可避免的就出现了一些变量污染的问题(项目一大,就有可能出现后定义的变量跟前面的变量重名,后面的变量不小心覆盖了前面的变量的值等等),而JS有的是函数作用域,定义在函数内部的变量,函数,外部是访问不到的,因此就有立即执行函数,将函数,变量定义在一个函数的内部,并且这个函数立刻执行,这样就相当于在主体里写了代码,而且函数内部的变量也不会跟别的变量冲突;

示例

(function(){
   var a = 10
})()
console.log(a) //报错,因为没有定义a

上面这个,相信第一次看到的人都有点懵,大约知道是一个函数,因为有function关键字,实际上立即执行函数是一种最终的简写结果,其本质就是定义了一个函数并且立刻执行了;

var a = 1;
var count = function(num){
	console.log(num);
}
count(a);

相信这一步学到这的你都能看得懂,就是定义了一个变量a,定义了一个函数表达式count,之后调用了count,并且将a作为参数传递进去了

先简写第一步,直接将变量传递到count中

var count = function(num){
	console.log(num);
}
count(1);

这样就去掉了a,之后发现好像还可以简写,可以将count的定义直接去掉,直接改成调用,如下

function(num){
	console.log(num);
}(1)	//报错

这么写之后发现,js不识别这种写法,因为不管在任何时候,关键字function都会被识别成函数定义的开始,因此不能这么写;
然而发现,函数表达式的后面则可以根括号,到这一步的关键就是要将函数声明转成函数表达式,那么如果转换呢,其实也简单,就是加一对括号

(function(num){
	console.log(num);
})(1)

这就是一个立即执行函数,定义了一个匿名函数,并且立即执行了,外界的变量可以通过参数传递进入,比如需要将window对象传递进去,那么可以

//这个window就是全局环境window
(function(window){
	console.log(window);
})(window)

闭包

什么是闭包?《高程》一书中的解释是:指有权访问另外一个函数作用域中的变量函数,这里有几个关键词,通俗点讲,就是首先闭包是一个函数,其次这个函数存在于另外一个函数体内,之后函数体内的函数访问了不属于自己用用于内的变量,可能有点绕口,示例如下

function func(){
	var number = 1;
  function printNum(){
    console.log(number);
  }
}

这就是一个闭包,虽然没有什么用,因为这么写并没有什么实际用处,通常闭包的使用是将函数体内的函数作为返回值返回出去,也就是这样:

function func(){
	var number = 1;
  return function (){
    console.log(number);
  }
}

为什么要这么做?其中最大的目的就是为了解决一个问题,为了保留执行结果且不污染全局环境,在了解这个结论之前,首先得明白,JS因为其垃圾回收机制,会导致变量只要离开环境,其值就会被释放,也就是会被销毁,为了不被销毁,只有想办法不让它释放,这么写可能有点空洞,看个例子,比如:

现在项目中有一个函数,每次执行过后要保存执行的结果,之后根据当前结果判断按哪种方式继续执行,具体的话就是有如下例,函数每次执行的时候都需要在上次的执行结果上+1,之后返回

function count(){/*代码*/}

count()	//1
count()	//2
count()	//3
function count(){
	var i = 0;
  i++;
  return i;
}
count()	//1
count() //1

这个函数执行肯定不行,因为每次count执行的时候,都会重新定义变量i,之后i++,再之后返回i,执行结束以后,count函数就会被释放(销毁)掉,这也就导致了每次count的执行结果都是1;
考虑到函数每次执行会重新定义i,那么只有将变量i提出到函数体外,i的值才能保留,比如:

var i = 0;
function count(){
  i++;
  return i;
}
count()	//1
count() //2

能实现要求,但是又带来了一个新问题,变量i定义到了全局环境重,我们不确定i这个变量会不会在其他部分被使用过,重新定义会不会有什么问题,即使假设这部分没问题,也不确定这个变量是不是在团队其他成员使用过了,如果使用过,合并代码的时候,会出现各种异常,到时候难免又是加班加点排查错误,因此,肯定不能将i定义在全局环境中,因为其风险我们不可控,那怎么办?在立即执行函数中说了,因为作用域的关系,函数体内的变量,不会影响到父级作用域,因此,改变一下,在外层我们在嵌套一个output函数,比如

function output(){
	var i = 0;
  function count(){
    i++;
    return i;
  }
}

这么写是没什么问题,变量i也不在全局环境中了,但是我们这么访问这个count函数呢?这么写访问不了,那就通过return返回出去

function output(){
	var i = 0;
  return function count(){
    i++;
    return i;
  }
}

//因为返回的是一个函数,所以必须先定义一个变量来接收这个函数
var count = output();
//相当于
var count = function count(){
  i++;
  return i;
}

这么一看返回的函数名count没有存在的必要,因为返回的是一个函数,而外界肯定要接收,所以直接返回一个匿名函数就好

function output(){
	var i = 0;
  return function(){
    i++;
    return i;
  }
}

//因为返回的是一个函数,所以必须先定义一个变量来接收这个函数
var count = output();

count()	//1
count()	//2

这下总算可以了,实现了题目中的要求,这种函数也就是我们最常见的闭包,其作用就是帮助我们保留执行结果,并且不污染全局环境

千万不要以为到这就结束了,闭包有一个非常大的缺陷,就是执行过后,这个称作闭包的函数没有被释放(我们也就是借助这一点保留了执行结果),正常情况下,函数执行了,引擎就会将函数释放掉,销毁掉,之后外界也就访问不到当前结果了,可闭包不是,它的值一直存在于当前的环境中,这也就导致了内存泄漏,又或者被“有心人”收集当前的执行结果,为了解决这种问题,等到不用的时候记的手动释放

var count = output();

count()	//1
count()	//2
//释放
count = null;

函数式编程

函数式编程是一种编程的规范,也可以说是一种编码风格,与函数式编程对应的是命令式编程

示例

假设现在有一个题目:有一个数组[1, 2, 3, 4],对数组进行操作,操作后,生成一个新的数组,其值是原数组的每项+1

命令式编程,就是为了达到最终效果,将执行的步骤每一步就详细的描述出来,然后让引擎去按设定好的步骤执行,比如:

//创建一个数组
let arr = [1, 2, 3, 4];
//创建一个新数组
let newArr = [];

//对老数组的每一项进行遍历
arr.forEach((el) => {
  	//将老数组的每一项都+1,然后push到新数组里
    newArr.push(el+1)
})
//打印新数组
console.log(newArr)

//又或者,通过函数返回一个新数组
let newArr = (arr) => {
    let res = []
    arr.forEach((el) => {
        res.push(el+1)
    })
    return res
}
console.log(newArr(arr))

这两种都是命令式编程,让引擎按照自己的意愿执行每一步,达到最终效果,命令式编程有一个最大的问题,就是所有的代码都是写死的,不可复用,假如那天产品经理拿着新需求过来了,他说:按照统计,用户不喜欢将数组的每一项都加1,而是每一项都加10,这个时候代码就复用不了,你必须去新建一段代码或函数,重新写一遍逻辑,实在是费时;

因此,到了这里,就不得不考虑如何提高效率了,基于此,也就有了函数式编程,其旨在尽可能的对函数复用,为了复用,就需要将函数拆解,使得函数的颗粒度达到最小,换句话说,就是一个函数只干一件事,绝不多干;
因此我们可以对上面的需求进行拆解:一个原数组,进行了一些操作,返回了一个新数组,具体如下

let arr = [1, 2, 3, 4];

let newArr = (arr,fn) => {
    let res = [];
    arr.forEach((el) => {
        res.push(fn(el))
    })
    return res
}
let add_1 = el => el+1;

console.log(newArr(arr,add_1))

和上面的区别,将对数组的运算独立了出来,将运算方式作为参数传递进去,这样,如果需求变更成+10那么只需要新建一个+10的函数,比如

let arr = [1, 2, 3, 4];

let newArr = (arr,fn) => {
    let res = [];
    arr.forEach((el) => {
        res.push(fn(el))
    })
    return res
}
let add_1 = el => el+1;
let add_10 = el => el+10;

console.log(newArr(arr,add_10))

甚至,同为加法运算,加的具体数字也可以作为参数传递进去

let add = (el,num) => el+num;

从例子可以看出,函数式编程就是将一个函数的执行过程,尽可能的细化,尽量写成是一系列函数的嵌套过程,这样,如果又其中一部分因为需求变更,那么只需要将变更的这部分函数重新设计编写,剩下的绝大部分逻辑都可以复用,以便达到提高效率(减少加班)的目的;

纯函数

如果函数的调用参数相同,则永远返回相同的结果,它不依赖于程序执行期间函数外部任何状态或数据的变化必须只依赖于其输入参数
简单的说,相同的输入,永远有相同的输出,为什么要这样?如果函数的执行结果取决于当前的外部变量结果,那么这种不可控不是一件很可怕的事情吗!

示例

let a = 10;
let add = b = b + a;
add(10);	//20

a = 1;
add(10);	//11

这就不是纯函数,因为明明执行了两次相同的代码,结果确实不一样的,试想一下,你执行了一个函数,结果是20,并用其结果进行了某些逻辑操作,之后再另外的地方又需要这个函数的结果作为参数了,待代码运行的时候,发现结果于预期不符合,这个时候要查错误,就比较麻烦了,因为没有报错,但结果就是不对;

因此为了保证程序的稳定性,应该尽可能的使用纯函数避免出现意料之外的情况,当然这种是相对的,具体情况还需要更具项目具体分析,只是说,能用纯函数的时候千万别弄别的幺蛾子;

高阶函数

顾名思义,高阶函数也是一种函数,与普通函数不同的是:高阶函数接收函数作为参数,或者返回的是一个函数;

let arr = [1, 2, 3, 4];

let newArr = (arr,fn) => {
    let res = [];
    arr.forEach((el) => {
        res.push(fn(el))
    })
    return res
}
let add_1 = el => el+1;

console.log(newArr(arr,add_1))

函数式编程中的函数newArr,这个就是一个高阶函数,它接收了一个函数作为参数,因此它就是一个高阶函数,同样,闭包也是一个高阶函数,因为它把一个函数作为返回值返回出去,也符合高阶函数的描述;

JS中有很多内置的高阶函数,比如数组方法中的map,reduce等等,下面有几个示例

//使用reduce实现数组去重
let arr = [1, 2, 3, 4, 5, 6, 6, 7, 7, 7]

let newArr = arr.reduce((prev, cur) => {
  	prev.indexof(cur) === -1 && prev.push(cur);
},[])

//实现数组拍平
const arr = [1, [2], [3, [4, [5]]]];
//给Array扩展一个flat
Array.prototype.flat = function () {
    let arr = function (curarr) {
       return curarr.reduce((tol, cur)=>{
         		//判断当前元素是否是数组
            //如果是数组,对其进行递归后再合并,如果不是数组,直接使用扩展预算父合并
            return Array.isArray(cur) ? [...tol, ...arr(cur)] : [...tol, cur]
        },[])
    }
    return arr(this)
}

console.log(arr.flat())

递归

递归函数在项目中认为是比较常见的函数了,其过程就是执行的过程中调用自身,形成一层层函数嵌套;请直接看示例:

function count(num){
    if(num <= 1){
        return 1;
    }
    else {
        return num * count(num-1)
    }
}
console.log(count(4))

这是一个简单的递归函数,看下来之后发现,递归其实也是一个循环既然是循环那么必定存在终止循环的条件,比如上例的return 1,就是终止循环的条件,简单分析一下这个递归:

执行函数后:

  • 第一个阶段:发现参数num的值是4,不符合if条件,因此执行else语句,发现里面是一个return,但是执行到4* 后面的时候发现又是一个函数,因此4*这个表达式就暂缓,搁置了,得先执行函数count;
  • 第二个阶段:此时count的参数因执行num-1,就变成了3,但3仍然不符合if语句,还是得执行else,因此在执行else的时候3*这个表达式依旧被暂缓执行,还是得先执行函数count;
  • 第三个阶段:这次执行的count参数的值是2,发现不符合if,因此执行else,执行的时候2*被搁置暂缓了,依旧得先执行count;
  • 第四个阶段:这次count的参数值是1,符合if条件,因此返回1,到这里就没有嵌套函数了;

第四阶段执行完毕后,有了一个明确的返回值,不再有嵌套函数,因此就要开始执行前面被暂缓的表达式了,最终第三阶段后面的count函数的值是1,因此第三阶段的返回的表达式是21,第三阶段执行完毕返回,那么第二阶段的count函数也有了计算结果,最终就是32,第二阶段执行完毕,有了返回值,那么第一阶段的count的值就是6,因此执行的就是4*6,因此整个count执行的结果就是24

柯里化

是把接收多个参数的函数转成接收单一参数的函数,并且返回接收剩余参数的函数;
我的理解是,原本有一个函数,它能接收多个参数,现在将函数改成链式的调用,每次只接收一个参数,具体示例

//普通函数
function boy(name, age, single){
	return `我是${name},今年${age}岁,我${single}单身`
}
boy("张三", 18, "是");

//柯里化之后
function boy(name){
	return function (age){
  	return function (single){
    	return `我是${name},今年${age}岁,我${single}单身`
    }
  }
}
boy("张三")(18)("是")

后面这个就是柯里化的函数,可以明确,这是一个高阶函数,因为返回的是函数,那么柯里化有什么优点呢?上面的例子好像没什么区别,实际上柯里化在现实开发项目中使用的是不大多,我本人使用的最多的是参数的固定,比如表单的验证上要确认每项输入不能有空格,具体请看示例

//正常函数
let macthInput = (reg,str) => reg.test(str);

macthInput(/\s+/g,"hello world");
macthInput(/\s+/g,"helloworld");

上例有一个验证函数matchInput,接收两个参数,第一个是一个正则,第二个是待检验的字符串,而注册表单等等表单上往往就十多项输入,每次检验都是需要输入正则,因此可以使用柯里化将正则参数固定下来,之后每次只需要输入待检验字符串就可以了

let macthInpuit = (reg) => {
	return (str) => {
  	return reg.test(str)
  }
}

let macthing = macthInpuit(/\s+/g);
macthing("hello world");
macthing("helloworld");

现在很多验证都已经内置在了UI框架中了,这种技巧平时使用的也不太多,不过思想得了解,当工作中用到多次使用同一个固定参数的时候,就可以考虑用柯里化的技术将参数固定下来;

防抖和节流

在前端中,resize,scroll,mousemove,mousehover等等事件,会不断的被触发,甚至一秒内会被触发几十几百次,如此高频的被触发,不仅造成计算机资源的浪费,还会降低程序的运行速度,造成浏览器卡死,奔溃;

防抖

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时;

let deBounce = (fn,delay) => {
    let timer = null;
    //返回一个函数,...arg,将argument数组化
    return (...arg) => {
      	//如果存在timer,就清除
        if(timer){
            clearTimeout(timer);
        }
      	//到这里说明timer是null,所以就设定了一个延时,延时之后触发函数,并且将作用域this传递过去
        timer = setTimeout(()=>{
            fn(...arg)
        },delay)
    }
}
//因为返回的是函数,所以相当于onmousemove绑定的就是一个函数
oDiv.onmousemove = deBounce(changeNum,200)

总的来说,防抖就是通过setTimeout,设定了一个延时,判断在这个事件内是否是被不断的触发,如果不断的被触发,则不断的清空之前设定的延时,直到最后一次触发,那么在延时之后,执行这个函数,确保函数只执行一次;

节流

在持续触发事件时,保证一个时间段内只调用一次事件处理函数;

let throttle = (fn,delay) => { 
    let flag = true;
    return (...arg) => {
        if(!flag) return
        flag = false;
        setTimeout(()=>{
            fn(...arg);
            flag = true;
        },delay)
    }
}

总的来说,就是通过设置一个开关变量,来控制当前是否可以触发函数,之后通过setTimeout,来控制时间段内执行一次函数;

应用场景

简单的说个应用场景,就是用户输入账号和密码后自动登录账号,如果没有使用防抖函数,那么每次触发input事件都将向服务器发送请求,这样就会很浪费资源,并且不断的发送请求也会增加服务器负担,因此,通过使用防抖函数,只有当输入停止满一定时间后,才会想服务器发送一次请求,即使用户是中断输入思考账号密码,那请求的次数也是大大的减少了;

深拷贝和浅拷贝

深拷贝和浅拷贝,都是一种拷贝,是对原数据进行一次复制,因为在实际开发中,很多时候往往不能对原数据进行修改,需要将原数据进行复制保存,再对复制的数据进行筛选,操作;

在了解深拷贝,浅拷贝之前,首先得明确,原始数据类型和引用数据类型,原始数据类型存储在内存的栈中,引用数据类型存储在数据的堆中,原始数据的复制只需要赋值就可以了

let a = 1;
let b = a;
b = 2;
console.log(b,a)	//2,1

对原始数据类型的操作并不会影响其值的来源,这也就是按值传递,也就是例子中即使对b进行了修改操作,也不会影响到变量a的值,但是引用类型就不是了

let a = {a:1};
let b = a;
b.a = 2;
console.log(b.a,a.a)	//2,2

对引用类型类型的修改,会追溯到其数据存储的地址中,也就会导致所有引用该地址的变量的值同时被修改,这也就是按址传递;
**
所以,深拷贝浅拷贝都是针对于引用类型而言的;通常对于对象的复制,是通过对对象的遍历然后复制,最后返回一个新对象,新对象的属性的修改于老对象没有关系;

浅拷贝

我的理解是:如果复制的新对象中存在与老对象相互影响的部分,那么本次复制就是浅拷贝;下例是一个简单的浅拷贝示例

let a = {
	a:1,
  b:2,
  c:{
  	d:10
  }
}

let copy = obj => {
	let rst = {}
  for(let key in obj){
  	rst[key] =  obj[key]
  }
  return rst
}
let newObj = copy(a);

通过遍历,将对象a的属性复制到了对象newObj,其中,属性a,属性b,修改相互不影响,但是属性c的部分,却又相互影响,那么本次就是浅拷贝;

深拷贝

和浅拷贝相反,深拷贝就是:复制的新对象与老对象任何部分都不相互影响,那么本次复制就是深拷贝
通过浅拷贝例子中的遍历方式,可以实现深拷贝,那么如果属性的属性值是对象,那么毫无疑问就要使用到递归了

let deepClone = obj => {
  let newObj = Array.isArray(obj)?[]:{};
  if(obj && typeof obj === "object"){
      for(let key in obj){
          if(obj.hasOwnProperty(key)){
              if(obj[key]&&typeof obj[key] === "object"){
                  newObj[key] = deepClone(obj[key])
              }
              else{
                  newObj[key] = obj[key]
              }
          }
      }
  }
  return newObj;
}

小结

不知道各位看到本文的大佬如何看待这些知识点,其实我个人认为,其实这些知识点都在阐述一个行为,为了减少工作量,提高工作效率,代码的复用,参数的复用,代码的稳定设计,都是为了在保证程序上线之后稳定可靠后,换句话说就是为了不再加班,享受生活, = =!,因此能复用的复用,能部分复用的就部分复用;

猜你喜欢

转载自blog.csdn.net/zy21131437/article/details/106709916