函数

2020/01/16 JavaScript 共 10233 字,约 30 分钟

函数

函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递.记忆前一个计算得到的值,为之后计算节省时,任何类型的缓存都必然会为性能牺牲内存

函数概念

函数可以拥有有属性

Function.prototype 或者 Function.arguments…

函数可以拥有方法

Function.prototype.apply() , Function.prototype.call()Function.prototype.bind()…

函数可以赋值给变量,数组或者其他对象属性给变量

var large=function (){} var obj={ large:function(){} }

函数还能被调用

function large(){},large();

当然函数还享有普通对象所拥有的特性,因为 Function 继承 Object

函数可以作为参数传递给函数,(函数名本身是变量,所以函数也可以作为值来使用;即可以把函数作为参数传递给另一个函数,也可以把函数作为另一函数的结果返回;)

function add(a,b){
    return a+b
}
function sum(fn,c){
    return fn+c
}
sum(add(2,3),4);//9

函数可以作为返回值进行返回

function add(a,b){
    return a+b;
}
add(2,3)//5

所以说函数是第一类型对象,函数是代码执行的主要模块单元化

函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,我们编写的所有的脚本代码都将在一个函数内执行

所谓的编程,就是将一组需求分解成一组函数与数据结构的技能

对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

函数式编程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块化

函数名是指向函数对象的指针。如果没有函数名,我们就称之为匿名函数,匿名函数没有指向函数对象的指针,一般情况下我们会立即执行函数,或者将匿名函数赋值给变量;

函数创建

函数创建的三种方式:函数声明和函数表达式(匿名函数,拉姆达函数),new Function()

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// expected output: 8

new Function()

new Function 是 JavaScript 中的一个构造函数,用于动态地创建一个新的函数(因此使用它也存在安全风险。在使用时需要格外小心,避免因为恶意代码或用户输入造成安全问题。),函数的参数是一个或多个字符串,每个字符串表示一个函数的参数名,多个参数名之间用逗号 , 分隔

把一串字符串变成一个可以执行的函数new Function()

使用场景: html 中 template渲染变量的时候

    // 字符串转模板字符串 https://www.zhangxinxu.com/wordpress/2020/10/es6-html-template-literal/
    String.prototype.interpolate = function (params) {
        const names = Object.keys(params);
        const vals = Object.values(params);
        const str = this.replace(/\{\{([^\}]+)\}\}/g,(all,s)=>`\${${s}}`); //   =>  ${ }
        return new Function(...names, `return \`${this}\`;`)(...vals);
    };
    或者字符串转对象
    let str = '{ opacity: 0.5, color: "black", mode:  "hide" }';
    new Function('return '+str)()

new Function(...names, return `${this}`;) this指当前这个,这里指的是传进来的这个字符串,即template.content,这个是一个函数 (…vals)=>表示这个函数的执行,vals的🈯️一个一个传入函数里面

补充: this指向 string 这个,类似的Array.prototype.xx=()=>console.log(this) => this指向array

// 函数声明
function sum(num1,num2){
  return num1+num2
}

// 函数表达式
const sum = function(num1,num2){
  return num1+num2
}

其中num1 和 num2 是函数的形参,(形参,形式上的参数)当 num1和num2作为具体的数据传递给函数时,就是实参,(实参,实际的参数) 形参和实参

如果形参个数大于实参个数,剩下没有对应的形参将赋值为 undefined

如果形参个数小于实参个数,多余的实参将会自动忽略

函数调用都会传递两个隐式的参数: this 和 arguments;

arguments 传递给函数的所有参数集合,一个类数组结构,如果为arguments[0]赋一个新值,那么,同时也会改变第一个参数的值

为函数的最后一个命名参数前加上省略号(…)前缀,这个参数就变成了一个叫作剩余参数的数组,数组内包含着传入的剩余的参数.剩余参数是真正的Array实例,也就是说你可以在它上面直接使用所有的数组方法

this 调用上下文,在哪调用,this 就指向谁,而不是取决于声明的时候。(有两个特殊的匿名函数和定时器的 this 指向 window

当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。这也是JavaScript实现面向对象编程的主要方式之一

函数声明和函数表达式的区别:

对于函数声明来说,函数名是强制性的,而对于函数表达式来说,函数名则完全是可选的。函数声明必须具有函数名是因为它们是独立语句。一个函数的基本要求是它应该能够被调用,所以它必须具有一种被引用方式,于是唯一的方式就是通过它的名字。

我们可以将表达式视为一个匿名函数,然后将其赋值给变量

解析器会率先读取函数声明,并在执行任何代码之前可以访问;函数表达式必须等到解析器执行到他所造的代码才会真正被解析(函数声明会提前(会打印出这个函数;函数表达式不会(会打印出undifined,和平常的变量一样);

函数声明后面不能跟圆括号;表达式可以(表达式后加圆括号表示函数调用);

在函数体内,变量声明的作用域开始于声明的地方,结束于所在函数的结尾,与代码嵌套无关;(即函数的作用域以及所有的变量都会在函数执行完以后立即被销毁)

命名函数的作用域是指声明该函数的整个函数范围,与代码嵌套无关

inner 函数能够访问到 outer 里面的变量,此时就形成了闭包,稍后会对闭包进行进一步了解

JavaScript解析器必须能够轻易区分函数声明和函数表达式之间的区别。如果去掉包裹函数表达式的括号,把立即调用作为一个独立语句function() {}(3),JavaScript开始解析时便会结束,因为这个独立语句以function开头,那么解析器就会认为它在处理一个函数声明。每个函数声明必须有一个名字(然而这里并没有指定名字),所以程序执行到这里会报错。为了避免错误,函数表达式要放在括号内,为JavaScript解析器指明它正在处理一个函数表达式而不是语句。

● 还有一种相对简单的替代方案(function(){}(3))也能达到相同目标(然而这种方案有些奇怪,故不常使用)。把立即函数的定义和调用都放在括号内,同样可以为JavaScript解析器指明它正在处理函数表达式

+function{}()
-function{}()
!function{}()
~function{}()

不同于用加括号的方式区分函数表达式和函数声明,这里我们使用一元操作符+、-、!和~。这种做法也是用于向JavaScript引擎指明它处理的是表达式,而不是语句。从计算机的角度来讲,注意应用一元操作符得到的结果没有存储到任何地方并不重要,只有调用IIFE才重要

匿名函数

没有名字的函数都称匿名函数,所有的函数表达式都属于匿名函数,立即调用函数也是匿名函数

(function(c){
    return console.log(c);
})(10)

JavaScript没有块级作用域,我们常用匿名函数模仿块级作用域;

for (var i=0;i<10;i++){ (function(j){ console.log(j) })(i) } 匿名函数在实际项目中用的也算比较多

递归函数

函数自己调用自己(引用自身),并且有终止条件

普通命名函数递归

function num(n){ return num>1?(num-1)*num:1; }

方法中的递归 var ninja={ chirp:function(n){ return n>1?ninja.chirp(n-1)*n:1 } }

当我们在方法中递归采用了匿名函数的时候,会引来另外一个问题,引用丢失;

var ninja={ chirp:function(n){ return n>1?ninja.chirp(n-1)*n:1; } } var sarural={chirp:ninja.chirp}; var ninja={}; console.log(sarural.chirp(4));//报错 为什么会报错,原因如下:

那么如何解决呢?

  var ninja1={
    chirp:function signal(n){
      return n>1?signal(n-1)*n:1;
    }
  }
  var sarural1={chirp:ninja1.chirp};
  console.log(sarural1.chirp(4));
  var ninja1={};
  console.log(sarural1.chirp(4));//24

我们在函数内部不适用匿名函数就能解决问题啦!

一个直接或者间接的调用自身的一种函数;他把一个问题分解为一组相似的子问题,每个都用一个寻常解去解决;(调用自身去解决她的子问题);递归函数可以非常高效的操作树形结构;

回调函数

处理回调函数是另一种常见的使用闭包的情景。回调函数指的是需要在将来不确定的某一时刻异步调用的函数。

回调函数:回调函数就是先定义一个函数稍后执行,不管是在浏览器还是其他地方执行,我们都称之为回调函数;也有种说法:回调函数是一个函数在另一个函数中调用

有没有发现回调函数在我们写代码的时候处处可见,回调已经成为 JavaScript 中必不可少的一部分了,我们广泛使用回调函数作为事件处理程序

function add(a,b){ return a+b } function sum(fn,c){ return fn+c } sum(add(2,3),4);//9 我们首先定义了一个 add 函数,然后在 sum 中调用了他,虽然这个例子不实用,但是很好的解释了回调函数的概念

闭包

闭包是纯函数式编程语言的特性之一

一句话概括就是:一个函数能够访问该函数以外的变量就形成了闭包;

闭包记住的是变量的引用,而不是闭包创建时刻该变量的值

简单点的闭包,看完之后有没有发现我们经常用到

// 自动执行,形成闭包
let x = 5
!function next(index){
if(x>0){
  console.log(index)
  next(--x)
}else{
  console.log('函数读取完了')
}
}(x)

<script>
    var num=1;
    function outerFunction(){
        return num;
    }
    outerFunction()
</script>

复杂点的闭包,一个函数内创建另一个函数

<script>
    var outerValue='ninja';
    var later;
    function outerFunction(){
        var innerValue='samurai';
        function innerFunction(){
            console.log(innerValue);
            console.log(outerValue);
        }
        later=innerFunction;
    }
    outerFunction()
    later();
</script>

正如保护气泡一样,只要内部函数一直存在,内部函数的闭包就一直保存着该函数的作用域中的变量

在外部函数 outerFunction 执行以后 ,内部函数 innerFunction的引用复制到全局引用later,因为内部函数 innerFunction引用复制到全局变量later,所以内部函数一直存在,形成了闭包;

如果直接去调用 later 则会报错,因为内部函数 innerFunction的还没有引用复制到全局变量 later上

只要内部函数 innerFunction一直存在,就形成了闭包,该函数引用的变量(innerValue,outerValue)就一直存在,没有被 javaScript 的回收机制给回收,闭包就想保护罩一样把她们保护起来,不允许外部访问,也不能被回收机制回收

问题:闭包保存的是整个变量对象,而不是某个特殊的变量;因为闭包必须维护额外的作用域,因此会比其他函数占用更多的内存,对性能有一定的影响,因此慎重使用闭包;

私有变量:任何在函数中定义的变量,都可以认为是私有变量;因为函数的外部不能访问这些变量,私有变量包括函数的参数,局部变量,函数内部定义的其他函数

function Private(){
  var num=1;
  this.getnum=function(){
    return num;
  };
  this.setnum=function(){
    num++;
    console.log(num);
  }
}
var private=new Private();
console.log(private.num);//报错,闭包形成私有变量,访问不到
console.log(private.getnum());//能够存取方法来获取私有变量,但是不能直接访问私有变量
console.log(private.setnum());

通过闭包模拟私有变量,通过回调函数使得代码更加优雅。

在JavaScript中没有真正的私有对象属性,但是可以通过闭包实现一种可接受的“私有”变量的方案。尽管如此,虽然不是真正的私有变量,但是许多开发者发现这是一种隐藏信息的有用方式

return语句可用来使函数提前返回,当return被执行时,函数立即返回而不再执行余下的语句;

谨记每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息,这一点非常重要。因此,虽然闭包是非常有用的,但不能过度使用。使用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这些信息

利用闭包的特性矫正 setInterval 时间计算有误差的情况

var startTime = new Date().getTime();
var count = 0;
setInterval(function(){
    var i = 0;
    while(i++ < 100000000);
}, 0);
function fixed() {
    count++;
    var offset = new Date().getTime() - (startTime + count * 1000);
    var nextTime = 1000 - offset;
    if (nextTime < 0) nextTime = 0;
    setTimeout(fixed, nextTime); // 形成闭包,下一次调用的时间根据上一次计算的时间来执行
     
    console.log(new Date().getTime() - (startTime + count * 1000));
}
setTimeout(fixed, 1000);

箭头函数

箭头函数没有自己的 this

也没有 prototype

也没有 arguments

无法创建箭头函数的实例

let fn = () => {
    console.log(this);
    console.log(arguments);//Uncaught ReferenceError: arguments is not defined
}
console.log(fn.prototype);//undefined
fn();
new fn();

箭头函数没有单独的this值。箭头函数的this与声明所在的上下文的相同

在全局代码中定义对象字面量,在字面量中定义箭头函数,那么箭头函数内的this指向全局window对象

执行上下文

在JavaScript中,代码执行的基础单元是函数。我们时刻使用函数,使用函数进行计算,使用函数更新UI,使用函数达到复用代码的目的,使用函数让我们的代码更易于理解。为了达到这个目标,第一个函数可以调用第二个函数,第二个函数可以调用第三个函数,以此类推。当发生函数调用时,程序会回到函数调用的位置。你想知道JavaScript引擎是如何跟踪函数的执行并回到函数的位置的呢?

全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的执行上下文

一旦发生函数调用,当前的执行上下文必须停止执行,并创建新的函数执行上下文来执行函数。当函数执行完成后,将函数执行上下文销毁,并重新回到发生调用时的执行上下文中。所以需要跟踪执行上下文——正在执行的上下文以及正在等待的上下文。最简单的跟踪方法是使用执行上下文栈(或称为调用栈)

[[Environment]]的内部属性上(也就是说无法直接访问或操作)。两个中括号用于标志内部属性。

定义函数时的环境与调用函数的环境往往是不同的

词法环境是JavaScript作用域的内部实现机制,人们通常称为作用域(scopes)

变量和函数的声明并没有实际发生移动。只是在代码执行之前,先在词法环境中进行注册。虽然描述为提升了,并且进行了定义,这样更容易理解JavaScript的作用域的工作原理,但是,我们可以通过词法环境对整个处理过程进行更深入地理解,了解真正的原理。

函数的使用

深度优先遍历

举一个不巧当的例子,循环一个数组,打印出出数组对象里面每一项的的键和值;在对象的项数和深度不确定的时候,使用循环一层一层找。虽然能够实现树形结构从上往下一层一层打印,但是有一个很不好的地方就是循环,时间空间复杂度增长

反正大概思路就是这样的

let list = [{name:'sunseeker',info:{age:18,like:{people:'secret'}}},{add:'hunan'}]

const isObject = obj=>Object.prototype.toString.call(obj)==='[object Object]'

function depth(list){
  list.forEach(item=>{
    if(isObject(item)){
      next(item,'')
    }
  })
}
 function next(item,val){
   if(!isObject(item)) {
      console.log(item);
      return
    }
    for(let i in item){
      if(isObject(item)){
        console.log(i);
        next(item[i],i)
      }
    }
 }

广度优先

先把第一层的第一个拿出来,遍历的时候,只要发现是数组,就往数组后面加,不做遍历。大概思路就是这样子

function breadth(list){
  let arr = list
  while(arr.length>0){
     let current = arr.shift()
     console.log(current)
     if(Array.isArray(current)){
     current.forEach(item=>{
       arr.push(item)
     })
     }
  }
}
const list = [[1],[2,[3,[4]]],[5],[6]]
breadth(list)


const fs = require('fs')
const path = require('path')

function wide(dir) {
 let arr = [dir]
 while (arr.length > 0) {
   let current = arr.shift()
   console.log(current)
   let stat = fs.statSync(current)
   if (stat.isDirectory()) {
     let files = fs.readdirSync(current)
     files.forEach(item => {
       arr.push(path.join(current, item))
     })
   }
 }
}
wide('src')

回调函数的作用

function preorder(dir, callback) {
  console.log(dir)
  fs.readdir(dir, (err, files) => {
    !function next(i) {
      console.log(i, 'i 有哪些值');
      if (i >= files.length) return callback()//这里有两个作用,第一是下一次遍历和全部遍历完,两种情况
      let child = path.join(dir, files[i])
      fs.stat(child, (err, item) => {
        if (item.isDirectory()) {
          preorder(child, () => next(i + 1))
        } else {
          console.log(child);
          next(i + 1)
        }
      })
    }(0)
  })
}
preorder('src', (data) => {
  console.log('裙摆')
})

为什么在调用这个函数时,代码中的b会变成一个全局变量?

function myFunc() { let a = b = 0; }

myFunc(); 复制代码原因是赋值运算符是从右到左的求值的。这意味着当多个赋值运算符出现在一个表达式中时,它们是从右向左求值的。所以上面代码变成了这样: function myFunc() { let a = (b = 0); }

myFunc(); 复制代码首先,表达式b = 0求值,在本例中b没有声明。因此,JS引擎在这个函数外创建了一个全局变量b,之后表达式b = 0的返回值为0,并赋给新的局部变量a。

柯里化

本质上是降低通用性,提高适用性,柯里化就是层层包裹原函数,直到参数符合我们需要的为止,在执行,否者一直不执行

第一版

  function sub_curry(fn) {
    const argus = [].slice.call(arguments, 1)
    return function() {  // 不能用箭头函数,因为箭头函数没有arguments
      debugger
      const newArgus = argus.concat([].slice.call(arguments))
      return fn.apply(this,newArgus)
    }
  }

第二版,在第一版的基础上,只有函数的参数满足需要传入的参数,才会执行函数,否者就会一直等待

function curry(fn,length){
  length = length||fn.length// 计算函数参数的个数,知道参数个数满足条件,才会执行函数
  let slice = Array.prototype.slice
  return function(){
    if(arguments.length < length){
      let combined = [fn].concat(slice.call(arguments))// 把当前函数和函数的个数作为参数继续传递
      curry(sub_curry.apply(this.combined),length - arguments.length)
    } else {
      return fn.apply(this, arguments); // 这里是为了,把传给原函数的参数解构,也可以使用下面的一种实现方式,这里并没有this的问题,单单只是为了参数解构
      <!-- return fn(...arguments); -->
    }
  }
}

函数组合,柯里化其实本质都是把函数拆分为最小的颗粒,互相之间独立,一个函数做一个简单的事情

函数组合,各种继承,是为了颗粒化一件函数只做一件事,react这方面做的很好,本质上是降低通用性,提高适用性

递归是因为 要计算太多次,可以考虑用缓存,尾递归

递归调用是调用自身去解决他的子问题,深度递归调用容易出现堆栈溢出,使用的时候我们要注意

jquery 之所以能实现链式调用,关键就在于通过 return this

工具库之所以强大,健壮性,各种平台的兼容,各种异常处理都有考虑,在平时的业务代码中可以考虑这么实现

有时候可以根据工具库,定义自己的方法,因为我们并不需要工具库那么强大的功能

JavaScript 专题之函数柯里化


在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents