前言
你是否对函数的理解有些似懂非懂,那么现在就让我们一起来消灭它吧
定义
匿名函数
function (){
return 1
}
上面的代码直接运行的话会报错,声明了一个函数却没有引用,就会报错
Function statements require a function name函数语句需要一个函数名
// 它是匿名函数但是它有 name
let fn = function (){
return 1
}
以上代码,表示函数在堆里面申请了一块内存,将地址给了栈,fn 记录了这个栈中函数的地址
let fn2 = fn
以上代码表示的是,在栈中把函数的地址复制了一份,fn2 记录了这个栈中复制的那个地址
那么 fn 和 fn2 都指向了那个函数,故下图打印出来的都是 fn ,匿名函数的 name 它默认和你变量的名字是一样的
具名函数
具有名字的函数
// fn3 这个名字,它是一个变量, 它的作用域是所有地方都可以访问到 fn3
function fn3(){
return 3
}
// 举例
// 这个的作用域就变了**function fn4(){}**这个就是它的作用域了
let fn5 = function fn4(){}
// console.log(fn4) 超过这个部分就访问不到 fn4 了
箭头函数
// 只有一个参数
const $ = s => document.querySelector(s)
$('div')
// 两个参数
let fn6 = (x, y) => x+y
// 如果后面有多句,就要用花括号把它们阔起来,同时把 return 的值说清楚
let fn7 = (x, y) => {
console.log(1)
return x+y // 只有一句话情况,就不需要写 return 了
}
词法作用域(静态作用域)
var global1 = 1
function fn1(param1){
var local1 = 'local1'
var local2 = 'local2')
function fn2(param2){
var local2 = 'inner local2' // 第六行,这个是 local2
console.log(local1)
console.log(local2)
}
function fn3(){
var local2 = 'fn3 local2'
fn2(local2)
}
}
上面代码分析
当我们在 fn2 里面去打印 local1, 它打印的 local1 是根据 词法树来确定的
fn2 里面没有 local1,那我就从 fn1 里去找,于是就找到 local1
通过词法树分析就能知道 local2 是我上面的第六行的 local2 ,与执行循序无关
注意⚠️:词法树只是用来分析这个打印出来的变量 local2 是不是这个作用域内部的那个变量 local2 ,它与值无关,分析的是语义,它们的值是否相等不是我所关心的
面试题
var a = 1
function b(){
console.log(a) // 这个 a 是不是我外面的 a? 通过词法树分析 肯定是的,但是 a 的值在任何情况下一定打印出来的是 1 吗?
}
假如我改成以下的代码
var a = 1
function b(){
console.log(a)
}
a = 2
b() // 打印出来的是 2
词法作用域,确定的是两个变量的关系,它们的值并不关心
如需深入了解请阅读以下文章
Call Stack
嵌套调用
递归
以上两个例子 后进先出 ,完整的展示了函数调用栈的执行时机
- JS引擎在调用一个函数前
- 需要把函数所在的环境 push 到一个数组里
- 这个数组叫做调用栈
- 等函数执行完了,就会把环境 pop 出来
- 然后 return 到之前的环境,继续执行后续代码
this & arguments
this & arguments,每个函数都有除了箭头函数,arguments 是包含所有参数的伪数组
- this 是隐藏的的第一参数,且必须是对象
function f(){
console.log(this)
console.log(arguments)
}
// 如果你传的是 null 或 undefined,那么默认打印 window(严格模式下默认是 undefined)
f.call() // window
f.call({name:'hone'}) // {name: 'hone'}, []
f.call({name:'hone'},1) // {name: 'hone'}, [1]
f.call({name:'hone'},1,2) // {name: 'hone'}, [1,2]
- 这篇文章解释了为啥第一个参数是隐藏参数
- 为何必须是对象
当我写成
f.call(10, 1)的时候,它其实相当于new Number(10)
那为何这样,因为 this 就是函数与对象之间的羁绊
假如没有 this
let persion = {
name: 'hone',
'sayHi':sayHi
}
let sayHI = function (){
console.log('你好,我叫' + person.name)
}
分析
- 如果 persion 改名, sayHi 函数就失效了
- sayHi 函数甚至有可能在另一个文件里面
- 它具有强耦合性,所以我们不希望 sayHi 函数里出现 persion 引用
class Persion {
constructor (name){
this.name = name // 这里的 this 是 new 强制指定的
}
sayHi(){
console.log('???')
}
}
分析
- 这里只有类,还没有创建对象,故不可能获取对象的引用
- 那么如何拿到对象的 name?,需要一种办法拿到,用参数
以下为假设
对象
let persion = {
name: 'hone',
sayHi: function(persion){
console.log('Hi, I am ' + persion.name)
}
}
persion.sayHi(persion)
类
class Persion{
constructor(name){
this.name = name
}
sayHi(p){
console.log('你好,我叫'+ p.name)
}
}
Python 采用了这种方法
this 是为了解决不用传值,我也知道当前对象传的是啥,那个 this 就是函数前面 . 那个东西,没有 . 那就是 window
// 以下两者等价
persion.sayHi()
person.sayHi.call(person) // 易于告诉代码的阅读者,这个 this 是啥
aplay
let array = [1, 2, 3, 4, 5]
function sum(){
let n = 0
for(let i = 0; n < arguments.length; i++){
n += arguments[i]
}
}
// 如果用 call 传值的话 当 arguments 不确定多少时,无法写出来
// 于是就发明了 apply
sum.apply(undefined, array)
call 和 apply 几乎一模一样,当不确定参数个数时就用 apply,就算知道长度也可以用 apply
bind
一句话介绍bind
call 和 apply 是直接调用这个函数,而 bind 则是返回一个新函数(并不是调用原来的函数),这个新函数会 call 原来的函数, call 的参数由你指定
模拟
// 伪代码1
var view = {
element: ${'#div'),
bindEvents: function(){
// 在调用这个绑定事件的时候 view.bindEvevts,一般来说这个 this 肯定是这个 view,但是也不能肯定,因为用户可以用 call 调用,所以无法确定
this.element.onclick = function(){
this.onClick() // 这个函数是如何被调用的?不知道
}
},
onClick: function(){
}
}
这个 element 被点击的时候,这个函数理应被调用,被浏览器调用,浏览器用的是 call ,那浏览器 call 的第一个参数是什么? 看文档
文档告诉我,我的 call 的第一个参数,是触发事件的元素
那么浏览器在调用
function(){ this.onClick() } ,它会 call 一下,那 call 给我们的第一个参数是被点击的那个元素,也就是上面这个伪代码的 div,this.onClick()这个 this 就是 div,那该如何? 我没有办法拿到 this
// 伪代码2
// 用这种???
var view = {
element: ${'#div'),
var _this = this
bindEvents: function(){
this.element.onclick = function(){
_this.onClick() // _this.onClick.call(_this)
}
},
onClick: function(){
}
}
// 以上代码直接写成,不就行了
var view = {
element: ${'#div'),
bindEvents: function(){
this.element.onclick = function(){
view.onClick()
}
},
onClick: function(){
}
}
不推荐伪造 this 的这种写法,于是有了另一种写法
// 伪代码3
var view = {
element: ${'#div'),
bindEvents: function(){
this.element.onclick = this.onClick().bind(this) // 这个意思就是上面伪代码2 的意思
},
onClick: function(){
this.element.addClass('active')
}
}
你明明要用 onClick 但是你不得不写一个新的函数,在新的函数里面去 call 这个 onClick
this.element.onclick = this.onClick().bind(this) // 现在的 this 和外面的 this是一样的
// 相当于
function(){
this.onclick.call(this)
}
bind 的作用就是往 onClick 后面加一个 call,在调这个 bind 新的函数出来的时候加
// 模拟 bind
this.onClick.bind = function(x, y, z){
var oldFn = this // 这个 this 是外面的 this.onClick
return function(){
oldFn.call(x, y, z) // 你给 bind 传的是啥,它就全部复制到这里
}
}
this.element.onclick = this.onClick().bind(this)
当用户点击这个 div ,浏览器就会调用 function(){ oldFn.call(x, y, z) },这个函数会调用之前的函数 this.onClick 然后在后面加个 call ,call 的参数就是 this
看完还是有点晕= = ,这里推荐 冴羽 的一篇 JavaScript 深入之 bind 实现
// 以下代码为 冴羽 博客里的,链接在上方已给出
var foo = {
value: 1
}
function bar() {
console.log(this.value);
}
// 返回了一个新函数bar.bind(foo) 名为 bindFoo
var bindFoo = bar.bind(foo) // bar.bind(foo)()
// 新函数会 call 原来的函数
bindFoo() // 1 相当于 bar.call(foo) 这个里面的参数由我指定
使用 .bind 可以让 this 不被改变
function f1(p1, p2){
console.log(this,p1,p2)
}
let f2 = f1.bind({name: 'hone'})
// 那么 f2 就是 f1 绑定了 this 之后的函数
f2() // 等价于 f1.call({name: 'hone'})
.bind 还可以绑定其他参数
let f3 = f1.bind({name: 'hone'}, 'hi')
f3() // 等价于 f1.call({name: 'hone'}, 'hi')
函数柯里化
什么叫柯里化
关于x 和 y 的函数
// z 根据 x, y来动
z = f(x, y) = x + 2y
// g 根据 y 来动
g = f(x=1)(y) = 1 + 2y
g 叫做 z 的偏(Partial)函数, g 只是 z 的一部分
柯里化:把一个函数其中一个参数固定下来,得到一个新的函数,将函数的个数变少,输出一个新的函数
🌰 例子
// 柯里化之前
function sum(x, y){
return x+y
}
// 柯里化之后
function addOne(y){
return sum(1, y)
}
addOne(4) // 5
function addTwo(){
return sum(2, y)
}
addTwo(4) // 6
addOne 就是把 sum 的 x 固定成 1,然后再加上后面的数
柯里化 可以用来做 惰性求值,就是你在调我第一个函数的时候,其实我啥也没做,在最后真正调的时候我才去做
推荐文章
高阶函数
认识
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入:
forEachsortmapfilterreduce - 输出一个函数:
lodash.curry - 不过它也可以同时满足两个条件:
Function.prototype.bind
作用:可以将函数任意的组合
🌰 举例
let sum = 0
let arr = [1, 2, 3, 4, 5, 6]
for(let i = 0; i < arr.length; i++){
if(arr[i] % 2 === 0){
sum += array[i]
}
}
// 以上代码可以写成
arr.filter(n => n % 2 === 0)
.reduce((sum, item) => {return sum +item}, 0)
let arr1 = [1, 4, 6, 2, 3, 8, 7, 9, 5]
arr1.filter( n => n % 2 === 1)
.sort((a, b) => {return a-b},0)
回调和构造函数
回调
名词形式:被当做参数的函数就是回调
动词形式:调用这个回调
回调和异步没有任何关系
图中所示为同步回调
setTimeout(fn,1000) 这个就是异步回调
构造函数
返回对象的函数就是构造函数
一般首字母大写
箭头函数
👉🏻 链接