目录
02 - JavaScript创建函数的时机不同:JS高级 之 JavaScript的运行原理
优化二 : 如果函数执行体中只有一行代码, 那么可以省略大括号
优化三 :如果函数执行体中只有一行代码时,表达式的结果会作为函数的返回值
优化四 : 如果函数执行体只有返回一个对象, 那么需要给这个对象加上 ( )
三、头等函数 First-class Function(头等函数) | MDN
一、函数的声明
1. function关键字
function sayHello() {
console.log("Hello!")
}
2. 函数表达式
const sayHello = function () {
console.log('Hello!');
};
3. 区别
01 - 语法不同
- 函数声明:在主代码流中声明为单独的语句的函数
- 函数表达式:在一个表达式中或另一个语法结构中创建的函数
02 - JavaScript创建函数的时机不同:JS高级 之 JavaScript的运行原理
- 函数声明 : 在函数声明被定义之前,它就可以被调用
foo() // 可以被使用
function foo() {
console.log("foo函数被执行了~")
}
- 函数表达式 : 是在代码执行到达时被创建,并且仅从那一刻起可用。
bar(); // TypeError: bar is not a function
var bar = function () {
console.log('bar函数被执行了~');
};
二、箭头函数
箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
- 箭头函数不会绑定this、arguments属性
- 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误)
- 箭头函数没有显示原型,显示原型为undefined
- 箭头函数有隐式原型,作为对象来说,隐式原型指向Function.prototype
1. 箭头函数的写法
01 - 普通写法
<script>
function foo() {}
const foo1 = function (name, age) {
console.log(name, age);
};
// 箭头函数写法
const foo2 = (name, age) => {
console.log(name, age);
};
</script>
02 - forEach中使用
<script>
const arr = [1, 2, 3, 4];
arr.forEach((item) => {
console.log(item, this);
});
</script>
03 - setTimeout中使用
<script>
setTimeout(() => {
const arr = [1, 2, 3, 4];
arr.forEach((item) => {
console.log(item);
});
}, 1000);
</script>
2. 箭头函数的编写优化
优化一 : 如果只有一个参数()可以省略
<script>
const arr = [1, 2, 3, 4];
// 因为只使用了一个参数,这里的括号可以省略
arr.forEach(item => {
console.log(item);
});
</script>
优化二 : 如果函数执行体中只有一行代码, 那么可以省略大括号
<script>
const arr = [1, 2, 3, 4];
// 因为函数体中只有一行代码,可以把{}省略
arr.forEach(item => console.log(item))
</script>
优化三 :如果函数执行体中只有一行代码时,表达式的结果会作为函数的返回值
<script>
const arr = [1, 2, 3, 4];
// 因为函数体中只有一行代码,可以把{}省略
const res = arr.filter((item) => item % 2 === 0);
console.log(res); //[2,4]
</script>
优化四 : 如果函数执行体只有返回一个对象, 那么需要给这个对象加上 ( )
<script>
const arr = [1, 2, 3, 4];
// 代表返回这个{name:'star'}对象
const tempArr = arr.map(item => ({name:'star'}))
console.log(tempArr); // [{name: 'star'},{name: 'star'},{name: 'star'},{name: 'star'}]
</script>
三、头等函数 First-class Function(头等函数) | MDN
头等函数(first-class function;第一级函数)是指在程序设计语言中,函数被当作头等公民。
头等函数具有以下三点特征:
- 将函数赋值给变量
- 函数作为函数参数
- 函数作为函数返回值
通常我们对作为头等公民的编程方式,称之为函数式编程
- JavaScript就是符合函数式编程的语言,这个也是JavaScript的一大特点
- 函数式编程就是把代码里的东西抽象成一个个函数,让函数作为一等公民可以传来传去
1. 将函数赋值给变量
// 函数可以被赋值给变量(函数表达式写法)
const foo1 = function() {
console.log("foo1函数被执行~")
}
2. 函数作为函数参数
const foo1 = function () {
console.log('foo1函数被执行~');
};
// 函数可以另外一个函数的参数
function bar(fn) {
console.log('fn:', fn);
fn();
}
bar(foo1);
3. 函数作为函数返回值
//函数作为另外一个函数的返回值
function sayHello() {
function hi() {
console.log("hi kobe")
}
// 1. 把函数返回出去了
return hi
}
// 2. 接受被返回的参数
var fn = sayHello()
fn()
四、立即执行函数
立即执行函数 : Immediately-Invoked Function Expression(IIFE 立即调用函数表达式)
- 表达的含义是一个函数定义完后被立即执行
- 第一部分是定义了一个匿名函数,这个函数有自己独立的作用域。
- 第二部分是后面的(),表示这个函数被执行了
- 会创建一个独立的执行上下文环境,可以避免外界访问或修改内部的变量
1. 栗子
<script>
(function () {
console.log('代码执行了,立即执行函数会立刻执行,不管在什么位置');
})();
</script>
2. 返回值
// 立即执行函数可以通过return,让外部访问内部的变量
const { getNum, setNum } = (function () {
let num = 123;
function getNum() {
return num;
}
function setNum(info) {
num = info;
}
// 返回函数,让外界可以操纵内部的变量
return {
getNum,
setNum
};
})();
console.log(getNum()); // 123
setNum(456);
console.log(getNum()); // 456
3. 编写方式
// 写法一 : ()在外面,常用
(function () {
console.log('我是立即执行函数');
})();
// 写法二 : ()在里面,不常用
(function () {
console.log('我也是立即执行函数,two');
}());
4. 立即执行
注意 : 立即执行函数马上就会被调用
const obj = {
foo: (function () {
console.log('我一上来就被调用了,不用等待,不用彷徨');
return function () {
console.log('我是立即执行函数');
};
})()
};
setTimeout(() => {
// 拿的直接就是返回的函数
obj.foo();
}, 1000);
五、函数的属性
JavaScript中函数也是一个对象,那么对象中就可以有属性和方法
1. 添加自定义属性
function foo() {
}
// 自定义属性
foo.message = "Hello Foo"
console.log(foo.message) // "Hello Foo"
2. name属性
01 - 栗子
function foo() {
}
const bar = function() {
}
// 默认函数对象中已经有自己的属性
console.log(foo.name) // foo
console.log(bar.name) // bar
02 - 应用
function foo(num1, num2) {
console.log('foo', this, num1, num2); // foo {} 123 321
}
function bar(str1, str2) {
console.log('bar', this, str1, str2); // bar String {'放屁'} abc cba
}
const arr = [foo, bar];
for (const fn of arr) {
// 根据函数名,给函数绑定不同的this
switch (fn.name) {
case 'foo':
fn.call({}, 123, 321);
break;
case 'bar':
fn.apply('放屁', ['abc', 'cba']);
break;
}
}
3. length属性
函数的length属性 : 代表 => 参数的个数
1. 正常情况
function foo(a, b, c) {
}
const bar = function (m, n) {
};
console.log(foo.length); // 3
console.log(bar.length); // 2
2. 不正常情况
01 - 没有写形参
function test() {
}
test(111, 222, 333);
// 没写形参的拿不到length属性,虽然会传入arguments中
console.log(test.length); // 0
02 - 写了剩余参数
function foo(a, b, c, ...args) {
}
// 剩余参数不会包括在length中
console.log(foo.length); // 3,所以还是3
03 - 写了默认值
// 有默认参数时,length属性只会计算默认参数之前的参数
// 第一种情况
function foo(a = 1, b, c) {}
console.log(foo.length); // 0,莫得
// 第二种情况
function bar(a, b = 1, c) {}
console.log(bar.length); // 1,只有a算在了里面
// 第三种情况
function baz(a, b, c = 1) {}
console.log(baz.length); // 2,只有a,b算在了里面
六、函数的arguments
arguments 是一个 对应于 传递给函数的参数 的 类数组(array-like)对象
array-like意味着它不是一个数组类型,而是一个对象类型:
- 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问
function foo(m, n) {
// arguments 类似数组对象
console.log(arguments);
// 通过索引获取内容
console.log(arguments[0]); // 10
console.log(arguments[1]); // 25
console.log(arguments[2]); // 32
console.log(arguments[3]); // 41
}
foo(10, 25, 32, 41);
- 但是它却没有数组的一些方法,比如filter、map等
// 报错
function foo(m, n) {
// arguments.forEach is not a function
arguments.forEach((item) => {
console.log(item);
});
}
foo(10, 25, 32, 41);
1. 遍历arguments
01 - 普通for循环
function foo(m, n) {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]); // 10, 25, 32, 41
}
}
foo(10, 25, 32, 41);
02 - for...of...
function foo(m, n) {
for (var arg of arguments) {
console.log(arg); // 10, 25, 32, 41
}
}
foo(10, 25, 32, 41);
2. 将arguments转成数组
00 - 方式零 : 循环逐个push
function foo(m, n) {
const newArguments = []
// 循环推入
for (let arg of arguments) {
newArguments.push(arg)
}
}
foo(10, 25, 32, 41);
01 - 方式一 : ... 展开运算符
function foo(m, n) {
// 展开运算符,使其在数组中展开
const newArguments = [...arguments];
console.log(newArguments); // [10, 25, 32, 41]
}
foo(10, 25, 32, 41);
02 - 方式二 : Array.from
function foo(m, n) {
// 调用方法
const newArguments = Array.from(arguments);
console.log(newArguments); // [10, 25, 32, 41]
}
foo(10, 25, 32, 41);
03 - 方式三 : slice
function foo(m, n) {
// 1. 拿到数组中的slice方法,使用call显示绑定调用
const newArguments = [].slice.call(arguments);
console.log(newArguments); // [10, 25, 32, 41]
// 2. 通过Array的显示原型拿到slice方法,使用apply显示绑定调用
const newArguments2 = Array.prototype.slice.apply(arguments);
console.log(newArguments2); // [10, 25, 32, 41]
}
foo(10, 25, 32, 41);
3. 箭头函数中不绑定argements
箭头函数是不绑定arguments的,所以在箭头函数中使用arguments会去上层作用域查找
// 1.箭头函数不绑定arguments
const bar = () => {
console.log(arguments) // 报错,上层作用域中也没有arguments
}
bar(11, 22, 33)
// 2.函数的嵌套箭头函数
function foo() {
// 找到的是foo中的arguments
const bar = () => {
console.log(arguments)
}
bar()
}
foo(111, 222)
七、函数的剩余参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
- 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
- 剩余参数必须放到最后一个位置,否则会报错
1. 栗子
// 剩余参数: rest parameters
function foo(num1, num2, ...args) {
// otherNums数组
console.log(args) // [111, 222, 333]
}
foo(20, 30, 111, 222, 333)
2. 剩余参数与arguments的区别
- arguments 对象包含了传给函数的所有实参,剩余参数只包含那些没有对应形参的实参
- arguments对象不是一个真正的数组,剩余参数是一个真正的数组
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,剩余参数是ES6中提供并且希望以此 来替代arguments的
八、默认参数
1. 以前写法
01 - 不严谨写法一
function bar(arg1) {
arg1 = arg1 ? arg1 : '我是默认值';
console.log(arg1);
}
bar(0); // 我是默认值
bar(''); // 我是默认值
bar(false); // 我是默认值
// ------按理来说,只有下面两个才显示正确------
bar(undefined); // 我是默认值
bar(null); // 我是默认值
02 - 不严谨写法二
function bar(arg1) {
arg1 = arg1 || '我是默认值';
console.log(arg1);
}
bar(0); // 我是默认值
bar(''); // 我是默认值
bar(false); // 我是默认值
// 按理来说,只有下面两个才显示正确
bar(undefined); // 我是默认值
bar(null); // 我是默认值
03 - 严谨写法
function bar(arg1) {
// 显示正确
arg1 = arg1 === undefined || arg1 === null ? '我是默认值' : arg1;
console.log(arg1);
}
bar(0); // 0
bar(''); // ''
bar(false); // false
bar(undefined); // 我是默认值
bar(null); // 我是默认值
04 - ES6新增语法
function bar(arg1) {
// 显示正确,ES6新增语法
arg1 = arg1 ?? '我是默认值';
console.log(arg1);
}
bar(0); // 0
bar(''); // ''
bar(false); // false
bar(undefined); // 我是默认值
bar(null); // 我是默认值
2. 简单用法
tip :
- 默认参数是不会对 null 进行处理
- 有默认参数的形参尽量写到后面
// 默认参数是不会对null进行处理的
function bar(arg1 = '我是默认值') {
console.log(arg1);
}
bar(0); // 0
bar(''); // ''
bar(false); // false
bar(undefined); // 我是默认值
// 为null时,不会显示我是默认值
bar(null); // null
3. 参数解构
01 - 简单解构
const info = { name: 'star', age: 18 };
// 解构
function bar({ name, age }) {
console.log(name, age); // star 18
}
bar(info);
02 - 默认参数解构
解构方式一
function foo(obj = { name: "star", age: 18 }) {
console.log(obj.name, obj.age) // star 18
}
foo()
解构方式二
function foo({ name, age } = { name: "star", age: 18 }) {
console.log(name, age) // star 18
}
foo()
解构方式三
function foo({ name = 'star', age = 18 } = {}) {
console.log(name, age) // star 18
}
foo()
九、纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
纯函数 :
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
- 一般都不使用闭包,即 不使用外界的变量,防止影响自身
副作用 :
表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
副作用往往是产生bug的 '温床'
栗子 🌰
- slice : slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice : splice截取数组, 会返回一个新的数组, 也会对原数组进行修改
- slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数
React中就有个严格的规则,所有React组件都必须像纯函数一样保护它们的props不被修改
作用
- 可以安心的编写和安心的使用
- 只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改
- 确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
十、函数柯里化
1. 概念
- 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
- 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
- 这个过程就称之为柯里化
- 柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)
- 柯里化不会调用函数。它只是对函数进行转换
2. 优势
01 - 函数的职责单一
// 传入的参数分别处理
function add(num1) {
num1 += 1;
return function (num2) {
num2 *= 2;
return function (num3) {
num3 = num3 ** 2;
console.log(num1 + num2 + num3);
};
};
}
add(2)(3)(4); // 25
02 - 函数的参数复用
function add(num1) {
return function (num2) {
console.log(num1 + num2);
};
}
// 保存20的数值,底层是使用闭包
const baz = add(20);
baz(10); // 30
baz(20); // 40
baz(30); // 50
baz(40); // 60
3. 改造普通函数变为柯里化
栗子 🌰
function foo(x, y, z) {
return x + y + z;
}
foo(1, 2, 3) // 6
改造一 : 愚蠢改造
// 1. 写法一
function foo(x) {
return function (y) {
return function (z) {
return x + y + z;
};
};
}
// 2. 写法二
const foo = x => y => z => {
return x + y + z;
};
console.log(foo(1)(2)(3)); // 6
改造二 : 不够灵活改造
// 使用函数的length属性
function foo(num1, num2, num3) {
let count = num1;
// 记录执行的次数
let recode = 1;
function bar(args) {
count += args;
recode++;
// 判断传入的参数个数是否和最开始设定的一样
// 不一样,返回函数继续调用
if (foo.length !== recode) {
return bar;
}
// 一样,返回结果
return count;
}
return bar;
}
console.log(foo(1)(2)(3)); // 6
改造三 : 终极改造
// 根函数
function foo(x, y, z) {
return x + y + z;
}
// 柯里化
function starCarring(fn) {
// 函数的根长度
let rootLength = fn.length;
function curryFn(...args) {
// 如果传入的参数个数大于或等于根长度,则不需要柯里化,直接调用
if (args.length >= rootLength) {
// 把结果返回出去
return fn.apply(this, args);
} else {
return function (...others) {
// console.log(others);
// 此时的参数个数
let currentArgs = [...args, ...others];
// 注意!!! 这里需要把调用curryFn函数返回的函数 返回出去!!!
// 如果直接 curryFn(...currentArgs),curryFn函数返回的函数只停留在这一层,外界是拿不到的
return curryFn.apply(this, currentArgs);
};
}
}
return curryFn;
}
// 调用
const baz = starCarring(foo);
// 直接调用
console.log(baz(1, 2, 3)); // 6
// 柯里化
console.log(baz(1)(2)(3)); // 6
console.log(baz(1)(2, 3)); // 6
console.log(baz(1, 2)(3)); // 6
十一、组合函数
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式 :
- 比如=需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的
- 那么如果每次都需要进行两个函数的调用,操作上就会显得重复
- 将这两个函数组合起来,依次调用称之为 组合函数(Compose Function)
function foo(num) {
return num + 100;
}
function double(num) {
return num ** 2;
}
// 组合函数
function composeFn(...fns) {
// 判断边界情况
if (fns.length === 0) return new Error('传入了空值');
for (const fn of fns) {
if (typeof fn !== 'function') {
return new Error('类型错误,不是一个函数');
}
}
// 返回新的函数
return function (...args) {
// 一般组合函数,第一个参数的值可能是多个
let result = fns[0].apply(this, args);
for (let i = 1; i < fns.length; i++) {
result = fns[i].call(this, result);
}
return result;
};
}
// 组合起来
const newFn = composeFn(foo, double);
console.log(newFn(100)); // 40000 => (100 + 100) * (100 + 100)
十二、手写call、apply、bind函数
1. call
// 添加到Function的原型上面,这样所有函数都能访问这个方法
Function.prototype.starCall = function (thisArgs, ...args) {
// 当传入undefined 或 null 时,设定为window
// 其他数据类型都转换为object类型
thisArgs = thisArgs === undefined || thisArgs === null ? window : Object(thisArgs);
// 给传入的需要绑定的this,绑定一个方法,通过隐式绑定来实现
// 也可直接 thisArgs.fn = this 下面这么写是为了不可枚举,不过可能也看得见
Object.defineProperty(thisArgs, 'fn', {
enumerable: false,
value: this,
configurable: true,
writable: false
});
// 执行,把参数传入
thisArgs.fn(...args);
// 最后删除这个属性
delete thisArgs.fn;
// this.execFn(thisArgs, args);
};
// 测试函数
function foo(name, age) {
console.log(this, name, age);
}
// 调用自定义的call方法
foo.starCall(null, 'abc', 18); // window abc 18
foo.starCall(123, 'abc', 18); // Number{123} abc 18
2. apply
和call一样,只是传入的参数是个数组
// 添加到Function的原型上面,这样所有函数都能访问这个方法,接受数组
Function.prototype.starAply = function (thisArgs, args) {
thisArgs = thisArgs === undefined || thisArgs === null ? window : Object(thisArgs);
Object.defineProperty(thisArgs, 'fn', {
enumerable: false,
value: this,
configurable: true,
writable: false
});
// 把数组拆解开放入执行的函数中
thisArgs.fn(...args);
delete thisArgs.fn;
};
// 测试函数
function foo(name, age) {
console.log(this, name, age);
}
// 调用自定义的apply方法,传入数组
foo.starAply(null, ['abc', 18]); // window abc 18
foo.starAply(123, ['abc', 18]); // Number{123} abc 18
抽取代码进行封装
// 封装call
Function.prototype.starCall = function (thisArgs, ...args) {
// this => foo 通过this来进行调用,把函数传递过去
this.execFn(thisArgs, args);
};
// 封装apply
Function.prototype.starApply = function (thisArgs, args) {
this.execFn(thisArgs, args);
};
// 封装执行函数
Function.prototype.execFn = function (thisArgs, args) {
thisArgs = thisArgs === undefined || thisArgs === null ? window : Object(thisArgs);
Object.defineProperty(thisArgs, 'fn', {
enumerable: false,
value: this,
configurable: true,
writable: false
});
thisArgs.fn(...args);
delete thisArgs.fn;
};
// 测试函数
function foo(name, age) {
console.log(this, name, age);
}
foo.starCall(123, 'abc', 18); // Number {123, fn: ƒ} 'abc' 18
foo.starApply(null, ['aaa', 233]); // window 'aaa' 233
3. bind
// 封装bind
Function.prototype.starBind = function (thisArgs, ...args) {
// 使用箭头函数,使用的this还是这里的this, this => foo
return (...otherArgs) => {
this.execFn(thisArgs, [...args, ...otherArgs]);
};
};
// 封装执行函数
Function.prototype.execFn = function (thisArgs, args) {
thisArgs = thisArgs === undefined || thisArgs === null ? window : Object(thisArgs);
Object.defineProperty(thisArgs, 'fn', {
enumerable: false,
value: this,
configurable: true,
writable: false
});
thisArgs.fn(...args);
delete thisArgs.fn;
};
// 测试函数
function foo(name, age) {
console.log(this, name, age);
}
const b = foo.starBind({ name: 'star' }, 'coder');
b(18); // {name: 'star'} 'coder' 18
// 这里绑定this无效,因为使用了箭头函数
b.call(null, 90); // {name: 'star'} 'coder' 90