目录
1_函数对象的属性
JavaScript中函数也是一个对象,就有属性和方法。
属性name:一个函数的名词可以通过name来访问
属性length:属性length用于返回函数参数的个数; 注意:rest参数是不参与参数的个数的;
2_arguments
2.1_认识arguments
arguments是一个 对应于 传递给函数的参数 的 类数组(array-like)对象
array-like意味着它不是一个数组类型,而是一个对象类型:
- 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;
- 但是它却没有数组的一些方法,比如filter、map等;
2.2_arguments转Array
在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性
转化方式一: 遍历arguments,添加到一个新数组中;
转化方式二: 调用数组slice函数的call方法;(较难理解了解即可)
// slice方法的回顾: 了解细节
var names = ["abc", "cba", "nba", "mba"]
var newNames = names.slice() // this -> names
// // slice方法 -> 函数
console.log(newNames)
obj.foo() // this -> obj
转化方式三:ES6中的两个方法。Array.from […arguments]
function foo(m, n) {
// arguments 类似数组对象
console.log(arguments)
//将arguments转成数组方式一:
var newArguments = []
for (var arg of arguments) {
newArguments.push(arg)
}
console.log(newArguments)
// 将arguments转成数组方式三: ES6中方式
var newArgs1 = Array.from(arguments)
console.log(newArgs1)
var newArgs2 = [...arguments]
console.log(newArgs2)
//.将arguments转成数组方式二: 调用slice方法
var newArgs = [].slice.apply(arguments)
// var newArgs = Array.prototype.slice.apply(arguments)
console.log(newArgs)
}
foo(10, 25, 32, 41)
2.3_箭头函数不绑定arguments
箭头函数是不绑定arguments的,所以在箭头函数中使用arguments会去上层作用域查找:
<script>
// 1.箭头函数不绑定arguments,会报错
// var bar = () => {
// console.log(arguments)
// }
// bar(11, 22, 33)
// 2.函数的嵌套箭头函数
function foo() {
var bar = () => {
console.log(arguments)
}
bar()
}
foo(111, 222)
</script>
2.4_函数的剩余(rest)参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
如果最后一个参数是 ...
为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
剩余参数和arguments的区别?
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此 来替代arguments的;
剩余参数必须放到最后一个位置,否则会报错。
<script>
// 剩余参数: rest parameters
function foo(num1, num2, ...otherNums) {
// otherNums数组
console.log(otherNums)
}
foo(20, 30, 111, 222, 333)
// 默认一个函数只有剩余参数
function bar(...args) {
console.log(args)
}
bar("abc", 123, "cba", 321)
// 注意事项: 剩余参数需要写到其他的参数最后
</script>
3_JavaScript纯函数
3.1_概念
函数式编程中有一个重要的概念叫纯函数,JavaScript符合函数式编程的范式,也有纯函数的概念;
- 在react开发中纯函数是被多次提及的;
- 比如react中组件就被要求像是一个纯函数(“像”,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
总结
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
3.2_副作用的理解
在计算机科学中,副作用表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用: 副作用往往是产生bug的 “温床
3.3_纯函数的案例
slice
:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;(纯)
splice
:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;(非纯,对原数组进行修改)
3.4_纯函数的优势
-
安心的编写和安心的使用;
-
在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
-
在用的时候,确定输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
4_柯里化
4.1_简介
是一种关于函数的高阶技术; 它不仅被用于 JavaScript,还被用于其他编程语言;
维基百科的解释:
-
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
-
是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
-
柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;
总结:传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数;这个过程就称之为柯里化。
柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©, 柯里化不会调用函数。它只是对函数进行转换
<script>
// 普通的函数
function foo1(x, y, z) {
console.log(x + y + z)
}
foo1(10, 20, 30)
foo1(20, 33, 55)
// 柯里化函数
function foo2(x) {
return function(y) {
return function(z) {
console.log(x + y + z)
}
}
}
foo2(10)(20)(30)
foo2(20)(33)(55)
// 另外一种写法: 箭头函数的写法
// function foo3(x) {
// return y => {
// return z => {
// console.log(x + y + z)
// }
// }
// }
var foo3 = x => y => z => {
console.log(x + y + z)
}
foo3(10)(20)(30)
</script>
4.2_优势
(1)函数的职责单一
在函数式编程中,其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理; 柯里化将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;
上面案例进行修改:传入的函数需要分别被进行如下处理
第一个参数 + 2
第二个参数 * 2
第三个参数 ** 2
function add2(x){
x = ×+2
return function(y){
y =y *2
return function(z){
z=z**2
return x +y +z
}
}
}
(2)函数的参数服用
复用参数逻辑,比如下面的例子
-
makeAdder函数要求我们传入一个num(并且如果我们需要的话,可以在这里对num进行一些修改);
-
在之后使用返回的函数时,我们不需要再继续传入num了;
function makeAdder(num){
return function( count){
return num +·count
}
}
var add5· =makeAdder(5)
add5(10)
add5(100)
var -add10 = makeAdder(10)
add10(10)
add10(1oo)
柯里化案例练习
<script>
// 案例一: 打印一些日志
// 信息一: 日志的时间
// 信息二: 日志的类型: info/debug/feature
// 信息三: 具体的信息
// 1.没有柯里化的时候做法
function logInfo(date, type, message) {
console.log(`时间:${date} 类型:${type} 内容:${message}`)
}
// // 打印日志
// logInfo("2022-06-01", "DEBUG", "修复界面搜索按钮点击的bug")
// // 又修复了一个bug
// logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug")
// logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug")
// logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug")
// logInfo("2022-06-01", "FEATURE", "增加了商品的过滤功能")
// 2.对函数进行柯里化: 柯里化函数的做法
// var logInfo = date => type => message => {
// console.log(`时间:${date} 类型:${type} 内容:${message}`)
// }
function logInfo(date) {
return function(type) {
return function(message) {
console.log(`时间:${date} 类型:${type} 内容:${message}`)
}
}
}
var logToday = logInfo("2022-06-01")
var logTodayDebug = logToday("DEBUG")
var logTodayFeature = logToday("FEATURE")
// 打印debug日志
logTodayDebug("修复了从服务器请求数据后展示的bug")
logTodayDebug("修复界面搜索按钮点击的bug")
logTodayDebug("修复界面搜索按钮点击的bug")
logTodayDebug("修复界面搜索按钮点击的bug")
logTodayDebug("修复界面搜索按钮点击的bug")
logTodayFeature("新建过滤功能")
logTodayFeature("新建搜索功能")
</script>
4.3_ 自动柯里化函数(理解)
有将多个普通的函数,转成柯里化函数。
再次强调柯里化函数的概念:传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数;这个过程就称之为柯里化
<script>
function foo(x, y, z) {
console.log(x + y + z)
}
function sum(num1, num2) {
return num1 + num2
}
function logInfo(date, type, message) {
console.log(`时间:${date} 类型:${type} 内容:${message}`)
}
// 手动转化
// 封装函数: 自动转化柯里化过程(有一点难度)
function hyCurrying(fn) {
function curryFn(...args) {
// 两类操作:
// 第一类操作: 继续返回一个新的函数, 继续接受参数
// 第二类操作: 直接执行fn的函数
if (args.length >= fn.length) { // 执行第二类
// return fn(...args)
return fn.apply(this, args) //考虑到this的情况
} else { // 执行第一类,不断传参进入
return function(...newArgs) {
// return curryFn(...args.concat(newArgs))
return curryFn.apply(this, args.concat(newArgs)) //考虑到this的情况
}
}
}
return curryFn
}
// 对其他的函数进行柯里化
var fooCurry = hyCurrying(foo)
fooCurry(10)(20)(30)
fooCurry(55, 12, 56)
var sumCurry = hyCurrying(sum)
var sum5 = sumCurry(5)
console.log(sum5(10))
console.log(sum5(15))
console.log(sum5(18))
var logInfoCurry = hyCurrying(logInfo)
logInfoCurry("2022-06-01")("DEBUG")("发现一个bug,")
</script>
5_组合函数(理解)
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:
-
比如现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
-
如果每次都需要进行两个函数的调用,操作上就会显得重复;
-
如果将这两个函数组合起来,自动依次调用,这个过程就是对函数的组合,称为 组合函数(Compose Function);
例子
<script>
//原始的组合函数
var num = 100
// 第一步对数字*2
function double(num) {
return num * 2
}
// 第二步对数字**2
function pow(num) {
return num ** 2
}
console.log(pow(double(num)))
console.log(pow(double(55)))
console.log(pow(double(22)))
// 将上面的两个函数组合在一起, 生成一个新的函数
function composeFn(num) {
return pow(double(num))
}
console.log(composeFn(100))
console.log(composeFn(55))
console.log(composeFn(22))
</script>
<script>
//对刚才的组合函数进行封装
// 第一步对数字*2
function double(num) {
return num * 2
}
// 第二步对数字**2
function pow(num) {
return num ** 2
}
// 封装的函数: 你传入多个函数, 我自动的将多个函数组合在一起挨个调用
function composeFn(...fns) {
// 1.边界判断(edge case)
var length = fns.length
if (length <= 0) return
for (var i = 0; i < length; i++) {
var fn = fns[i]
if (typeof fn !== "function") {
throw new Error(`index position ${i} must be function`)
}
}
// 2.返回的新函数
return function(...args) {
var result = fns[0].apply(this, args)
for (var i = 1; i < length; i++) {
var fn = fns[i]
result = fn.apply(this, [result])
}
return result
}
}
var newFn = composeFn(double, pow, console.log)
newFn(100)
newFn(55)
newFn(22)
// console.log(newFn(100))
// console.log(newFn(55))
// console.log(newFn(22))
</script>
6_with语句的使用
with语句 ,强行扩展一个语句的作用域链,一般不推荐使用,可能是混淆错误和兼容性问题的根源
<script>
var obj = {
message: "Hello World"
}
with (obj) {
console.log(message)
}
</script>
7_eval函数
内建函数 eval 允许执行一个代码字符串。
-
eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行;
-
eval会将最后一句执行语句的结果,作为返回值;
<script>
var message = "Hello World"
var codeString = `var name = "666"; console.log(name); console.log(message); "abc";`
var result = eval(codeString)
console.log(result)
</script>
不建议在开发中使用eval:
-
eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
-
eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
-
eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化;
8_严格模式
JavaScript历史的局限性:
- 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
- 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;
- 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中;
在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):
- 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
严格模式对正常的JavaScript语义进行了一些限制:
- 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误;
- 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
- 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;
8.1_开启严格模式
严格模式支持粒度话的迁移:
-
可以支持在js文件中开启严格模式;
-
也支持对某一个函数开启严格模式;
严格模式通过在文件或者函数开头使用 use strict
来开启;
<script>
// 给整个script开启严格模式
"use strict"
// 给一个函数开启严格模式
function foo() {
"use strict"
}
class Person {
}
</script>
没有类似于 “no use strict” 这样的指令可以使程序返回默认模式。 现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict;
8.2_严格模式限制
说几个严格模式下的严格语法限制:
- JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;
- 但是这种方式可能给带来留下来安全隐患;
- 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;
无法意外的创建全局变量
严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
严格模式下试图删除不可删除的属性
严格模式不允许函数参数有相同的名称
不允许0开头的八进制语法
在严格模式下,不允许使用with
在严格模式下,eval不再为上层引用变量
严格模式下,this绑定不会默认转成对象
<script>
"use strict"
// 1.不会意外创建全局变量
// function foo() {
// message = "Hello World"
// }
// foo()
// console.log(message)
// 2.发现静默错误
var obj = {
name: "hhh"
}
Object.defineProperty(obj, "name", {
writable: false,
configurable: false
})
// obj.name = "kobe" //报错,不可写
console.log(obj.name)
// delete obj.name
console.log(obj)
// 3.参数名称不能相同
// function foo(num, num) {
// }
// 4.不能以0开头
// console.log(0o123)
// 5.eval函数不能为上层创建变量
// eval(`var message = "Hello World"`)
// console.log(message)
// 6.严格模式下, this是不会转成对象类型的
function foo() {
console.log(this)
}
foo.apply("abc")
foo.apply(123)
foo.apply(undefined)
foo.apply(null)
// 独立函数执行默认模式下, 绑定window对象
// 在严格模式下, 不绑定全局对象而是undefined
foo()
</script>
9_Object.defineProperty
9.1_属性操作的控制
之前学习对象的属性都是直接定义在对象内部,或者直接添加到对象内部的,但是这样来做的时候就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for in
遍历的时候被遍历出来?
如果想要对一个属性进行比较精准的操作控制,就可以使用属性描述符。
- 通过属性描述符可以精准的添加或修改对象的属性;
- 属性描述符需要使用
Object.defineProperty
来对属性进行添加或者修改;
9.2_Object.defineProperty()
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
可接收三个参数:
-
obj要定义属性的对象;
-
prop要定义或修改的属性的名称或 Symbol;
-
descriptor要定义或修改的属性描述符;
返回值: 被传递给函数的对象。
9.3_属性描述符分类
属性描述符的类型有两种:
-
数据属性(Data Properties)描述符(Descriptor);
-
存取属性(Accessor访问器 Properties)描述符(Descriptor);
数据属性描述符
有如下四个特性:
Configurable
:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
- 直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
- 通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
Enumerable
:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
- 通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
Writable
:表示是否可以修改属性的值;
- 直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
- 通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
value
:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改; 默认情况下这个值是undefined;
<script>
var obj = {
name: "hhh", // configurable: true
age: 18
}
Object.defineProperty(obj, "name", {
configurable: false, // 告诉js引擎, obj对象的name属性不可以被删除
enumerable: false, // 告诉js引擎, obj对象的name属性不可枚举(for in/Object.keys)
writable: false, // 告诉js引擎, obj对象的name属性不写入(只读属性 readonly)
value: "coderhhh" // 告诉js引擎, 返回这个value
})
delete obj.name //删除失败,因为设置了这个属性不可删除
//console.log(obj) //{age: 18, name: 'coderhhh'} 假设下面的代码还没出现的情况
// 通过Object.defineProperty添加一个新的属性
Object.defineProperty(obj, "address", {})
delete obj.address
console.log(obj) //{age: 18, name: 'coderhhh', address: undefined}
console.log(Object.keys(obj)) //['age']
obj.name = "kobe" //写入新的值失败,因为设置了这个属性不可写入,只能读
console.log(obj.name) //coderhhh
</script>
存取属性描述符
有如下四个特性:
configurable
:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
-
和数据属性描述符是一致的;
-
直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
-
通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
enumerable
:表示属性是否可以通过for-in或者Object.keys()返回该属性;
-
和数据属性描述符是一致的;
-
直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
-
通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
get
:获取属性时会执行的函数。默认为undefined
set
:设置属性时会执行的函数。默认为undefined
<script>
// vue2响应式原理
var obj = {
name: "hhh"
}
// 对obj对象中的name添加描述符(存取属性描述符)
var _name = ""
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: false,
set: function(value) {
console.log("set方法被调用了", value)
_name = value
},
get: function() {
console.log("get方法被调用了")
return _name
}
})
obj.name = "kobe" //set方法被调用了 kobe
obj.name = "jame" //set方法被调用了 jame
obj.name = "curry" //set方法被调用了 curry
// 获取值
console.log(obj.name) //get方法被调用了
</script>
9.4_同时定义多个属性
Object.defineProperties()
方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。
<script>
var obj = {
name: "hhh",
age: 18,
height: 1.88
}
// Object.defineProperty(obj, "name", {})
// Object.defineProperty(obj, "age", {})
// Object.defineProperty(obj, "height", {})
// 新增的方法 ,用逗号隔开
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writable: false
},
age: {
configurable: false,
enumerable: true,
writable: false
},
height: {
configurable: true,
enumerable: true,
writable: false
}
})
</script>
9.5_对象方法补充
获取对象的属性描述符:
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
禁止对象扩展新属性:preventExtensions 给一个对象添加新的属性会失败(在严格模式下会报错);
密封对象,不允许配置和删除属性:seal
- 实际是调用preventExtensions
- 并且将现有属性的configurable:false
冻结对象,不允许修改现有属性: freeze
- 实际上是调用seal
- 并且将现有属性的writable: false
<script>
var obj = {
name: "hhh",
age: 18
}
// 1.获取属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, "name")) //{value: 'hhh', writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptors(obj)) //age: {value: 18, writable: true, enumerable: true, configurable: true} name: {value: 'hhh', writable: true, enumerable: true, configurable: true}
// 2.阻止对象的扩展
Object.preventExtensions(obj)
obj.address = "北京"
console.log(obj) //{name: 'hhh', age: 18}
// 3.密封对象(不能进行配置)
Object.seal(obj)
delete obj.name
console.log(obj) //{name: 'hhh', age: 18}
// 4.冻结对象(不能进行写入)
Object.freeze(obj)
obj.name = "kobe"
console.log(obj) //{name: 'hhh', age: 18}
</script>