JavaScript中的函数

概念#

函数就是封装了一段可被重复调用执行的代码块

函数的使用分为两步:声明函数和调用函数。

  1. 函数声明
Copy
function fn() { console.log("hi") }

注意:

  • function 声明函数的关键字全部小写;
  • 函数不调用自己不会执行;
  1. 调用函数
Copy
fn()

注意:调用的时候一定要加小括号。


函数的五种声明方式#

具名函数#

Copy
function f(x, y) { return x + y } console.log(f(1, 2)) // 3

匿名函数#

Copy
var fn fn = function(x, y) { return x + y } console.log(fn(1, 2)) // 3

具名函数赋值#

Copy
var x = function y(a, b) { return a + b } console.log(x(1, 2)) // 3 console.log(y) // y is not defined

window.Function#

Copy
var fn = new Function('x', 'y', 'return x+y') console.log(fn(1, 2)) // 3

箭头函数#

Copy
var fn1 = x => n * n var fn2 = (x, y) => x + y var fn3 = (x, y) => { return x + y }

name属性#

function.name 属性返回函数实例的名称。

我们来看看下面这几种情况:

Copy
function fn() {} console.log(fn.name) // fn

Copy
let fn1 = function fn2() {} console.log(fn1.name) // fn2

Copy
let fn = new Function('x', 'y', 'return x+y') console.log(fn.name) // anonymous

Copy
console.log((() => {}).name) // "" let fn = () => {} console.log(fn.name) // fn

函数的本质#

函数就是一段可以反复调用的代码块。函数是一个对象,这个对象可以执行一段代码,可以执行代码的对象就是函数。

那为什么函数是一个对象呢?

Copy
var f = {} f.name = 'f' f.params = ['x', 'y'] f.functionBody = 'console.log("1")' f.call = function() { return window.eval(f.functionBody) } console.log(f) // {name: "f", params: Array(2), functionBody: "window.runnerWindow.proxyConsole.log("1")", call: ƒ} f.call() // 1

函数的封装#

函数的封装是把一个或多个功能通过函数的方法封装起来,对外只提供一个简单的函数接口。

下面我们来看看几个简单的例子:

Copy
// 计算 1 ~ 100 之间的累加和 function getNum() { var sum = 0 for (let i = 1; i <= 100; i++) { sum += i } console.log(sum) } getNum() // 5050

Copy
// 求任意两个数的和 function getSum(num1, num2) { console.log(num1 + num2) } getSum(1, 2) // 3

Copy
// 求任意两个数之间的和 function getNum(start, end) { let sum = 0 for (let i = start; i <= end; i++) { sum += i } console.log(sum) } getNum(0, 10) // 55

this与arguments#

this 就是 call 的第一个参数,可以用 this 得到。

arguments 就是 call 除了第一个以外的参数,可以用 arguments 得到。arguments 对象中存储了传递的所有实参。
arguments 展示形式是一个伪数组。
伪数组具有以下这几个特点:

  1. 具有 length 属性;
  2. 按索引方式储存数据;
  3. 不具有数组的 push、pop 等等方法;

在普通模式下,如果 this 是 undefined,浏览器会自动把 this 变为 window。

在普通模式下:

Copy
let fn = function() { console.log(this) // window console.log(this === window) // true } fn.call(undefined)

在严格模式下:

Copy
let fn = function() { 'use strict' console.log(this) // undefined console.log(this === window) // false } fn.call(undefined)

arguments:

Copy
let fn = function() { console.log(arguments) } fn.call(undefined, 1, 2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

Copy
// 求任意个数中的最大值 function fn() { let max = arguments[0] for (let i = 1; i < arguments.length; i++) { if (max < arguments[i]) { max = arguments[i] } } return max } console.log(fn(1, 2, 3, 4, 661, 434)) // 661

call stack调用栈#

先进后出

查看调用栈过程

普通调用

嵌套调用

递归调用


柯里化#

柯里化(Currying),又称部分求值(Partial Evaluation),是一种关于函数的高阶技术。它不会调用函数,它只是对函数进行转换。将 fn(a,b,c) 转换为可以被以 fn(a)(b)(c) 的形式进行调用。它是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

我们先来看一个例子:

Copy
function add(a, b, c) { return a + b + c } console.log(add(1, 2, 3)) // 6

现在我们把上面代码修改成柯里化版本:

Copy
function addCurry(a) { return function(b) { return function(c) { return a + b + c } } } console.log(addCurry(1)(2)(3)) // 6

我们来把 addCurry(1)(2)(3) 换一个形式来表示:

Copy
let a = addCurry(1) // ƒ (b) { return function(c) { return a + b + c } } let b = a(2) // ƒ (c) { return a + b + c } let c = b(3) console.log(c) // 6

下面我们再来看一个例子:

Copy
let handleBar = function(template, data) { return template.replace('{{name}}', data.name) } handleBar('<p>Hello,{{name}}</p>', { name: 'zww' }) // <p>Hello,zww</p> handleBar('<p>Hello,{{name}}</p>', { name: 'lq' }) // <p>Hello,lq</p>

上面这段代码导致的问题就是,如果我们经常要使用 template 模板,那么每次像上面这样写将会导致十分繁琐。我们可以将代码修改为柯里化版本:

Copy
function handleBarCurry(template) { return function(data) { return template.replace('{{name}}', data.name) } } let h = handleBarCurry('<p>Hello,{{name}}</p>') h({ name: 'zww' }) // <p>Hello,zww</p> h({ name: 'lq' }) // <p>Hello,lq</p>

这样就实现了 template 模板参数复用的效果了。

张鑫旭 - JS中的柯里化(currying)
现代JavaScript 教程 - 柯里化(Currying)
Currying 的局限性
JavaScript函数柯里化


高阶函数#

高阶函数是至少满足下面一个条件的函数:

  1. 接受一个或多个函数作为输入;
  2. 输出一个函数;
  3. 同时满足上面两个条件;

例如下面这些就是 JS 原生的高阶函数:

  1. Array.prototype.sort()
  2. Array.prototype.forEach()
  3. Array.prototype.map()
  4. Array.prototype.filter()
  5. Array.prototype.reduce()

我们来实现找出数组中所有的偶数并相加:

Copy
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9] let sum = 0 for (let i = 0; i < array.length; i++) { if (array[i] % 2 === 0) { sum += array[i] } } console.log(sum) // 20

下面我们用高阶函数来实现上面的功能:

Copy
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9] let sum = array.filter(function(x) { return x % 2 === 0 }).reduce(function(p, n) { return p + n }, 0) console.log(sum) // 20

廖雪峰 - 高阶函数


回调函数#

MDN 所描述的:被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。

简单的说就是被当作参数的函数就是回调。

就像 array.sort(function() {})array.forEach(function() {})这些都是回调函数。

Copy
function putMsg(msg, callback) { setTimeout(() => { console.log(msg) callback() }, 1000) } putMsg('hi', function() { console.log('msg') })

上面代码将在 1 秒后打印 hi、msg。


构造函数#

简单的说就是返回对象的函数就是构造函数,构造函数名字首字母一般大写。

构造函数有两个特点:

  1. 函数体内部使用了 this 关键字,代表了所要生成的对象实例;
  2. 生成对象的时候,必须使用 new 命令;
Copy
function Person(name, age) { this.name = name this.age = age } let person = new Person('zww', 18) console.log(person) // Person {name: "zww", age: 18}

作用域#

作用域指的是您有权访问的变量集合。

作用域决定了这些变量的可访问性(可见性)。

函数内部定义的变量从函数外部是不可访问的(不可见的)。

作用域分为全局作用域、局部作用域。变量也可以分为全局变量与局部变量。
从执行效率来看全局变量与局部变量:

  1. 全局变量只有浏览器关闭的时候才会销毁,比较占内存资源;
  2. 局部变量当我们程序执行完毕就会销毁,比较节约内存资源;

作用域链:内部函数访问外部函数的变量,采取的是链式查找的方式来决定取哪个值,这种结构称为作用域链。也就是所谓的就近原则。

我们来看看几个例子:

question one:

Copy
var a = 1 function f1() { var a = 2 f2.call() console.log(a) // 2 function f2() { var a = 3 console.log(a) // 3 } } f1.call() console.log(a) // 1

question two:

Copy
var a = 1 function f1() { f2.call() console.log(a) // undefined var a = 2 // 变量提升!!! function f2() { var a = 3 console.log(a) // 3 } } f1.call() console.log(a) // 1

question three:

Copy
var a = 1 function f1() { console.log(a) // undefined var a = 2 f2.call() } function f2() { console.log(a) // 1 } f1.call() console.log(a) // 1

question four:

Copy
var liTags = document.querySelectorAll('li') for (var i = 0; i < liTags.length; i++) { liTags[i].onclick = function() { console.log(i) // 点击第二个li时,打印6 } }

以上代码变量提升后可等价如下:

Copy
var liTags var i liTags = document.querySelectorAll('li') for (i = 0; i < liTags.length; i++) { liTags[i].onclick = function() { console.log(i) } }

闭包#

闭包指有权访问另一个函数作用域中变量的函数,简单的说就是,一个作用域可以访问另外一个函数内部的局部变量。

闭包的主要作用:延伸了变量的作用范围。

Copy
var a = 1 function fn() { console.log(a) }

这个函数 fn 与变量 a 就形成一个闭包。

Copy
function f1() { var num = 10 function f2() { console.log(num); } return f2 } var f = f1() f() // 10

箭头函数#

ES6 中新增的定义函数的方式。

箭头函数不绑定 this,箭头函数中的 this,指向的是函数定义位置的上下文 this。

箭头函数有以下这些特点:

  1. 有更加简洁的语法;
  2. 不会绑定 this;
Copy
let fn = () => { console.log(this) } fn() // window // 如果使用 call,还是不会改变 this 指向。 fn.call({ name: 'zww' }) // window

我们来看看下面这段代码的 this 是什么:

Copy
function fn() {} setTimeout(function(a) { console.log(this) // window }, 1000)

很明显,上面代码执行 1s 后将会打印 window,那么我们应该怎样把 this 指向 fn 呢?

可以使用 bind 来更改 this 的执行,代码如下:

Copy
setTimeout(function(a) { console.log(this) // ƒ fn() {} }.bind(fn), 1000)

这样 this 就指向了 fn 了,下面我们再在 setTimeout 里面添加个 setTimeout:

Copy
setTimeout(function(a) { console.log(this) // ƒ fn() {} setTimeout(function(a) { console.log(this) // window }, 1000) }.bind(fn), 1000)

里面这个 setTimeout 所打印的 this 还是指向的 window,那应该怎么指向 fn 呢?

没错,还是使用 bind,只不过里面直接传 this 即可:

Copy
setTimeout(function(a) { console.log(this) // ƒ fn() {} setTimeout(function(a) { console.log(this) // ƒ fn() {} }.bind(fn), 1000) }.bind(fn), 1000)

上面代码,我们还可以使用箭头函数来简化:

Copy
setTimeout(function(a) { console.log(this) // ƒ fn() {} setTimeout((a) => { return console.log(this) // ƒ fn() {} }, 1000) }.bind(fn), 1000)
posted @   LqZww  阅读(221)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示
CONTENTS