编程模式
从编程模式的角度看 JavaScript,它是结构化的、事件驱动的动态语言,且支持声明式和指令式两种模式。所以我们说,JavaScript 是一个多模式(multi-paradigm)的语言,也是一门“丰富”的语言。
在 JavaScript 所支持的编程模式中,用得最多的是面向对象(OOP object oriented programming)和函数式(FP functional programming)两种,其中又以面向对象的普及率最高。
函数式编程:管理和解决副作用
减少副作用,纯函数和不可变
副作用:
函数已经把算法封装了起来,那么函数里相对就是可控的,而比较不可控的是外部环境。这里,我们可以把不可控的外部环境分为三大类。
- 全局变量。比如在外部定义了一个var类型全局变量,每次函数中使用时,都不确定变量是否变动。
- IO影响。Node端文件系统、网络链接以及其他IO,还有前端浏览器中的用户行为,比如鼠标输入输出等。
- 网络请求相关。
在函数式编程中,有两个核心概念:纯函数(pure function)和不可变(immutability)。这是一个“双循环”,纯函数更多解决的是“内循环”;而不可变更多考虑的是“外循环”。
- 纯函数的意思是说,一个函数的返回结果的变化只依赖其参数,并且执行过程中没有副作用。
- 不可变是指函数对外界变量的影响 应该是“不可变”的。
引用传值的基础上直接修改值,会改变原有地址的值,比如splice
和slice
。
因为把一个外部变量作为参数作为输入,在函数里做了改变,作为输出返回 的这个过程中,可能不可预料这种变化会对整个系统造成什么样的影响。 - 纯函数应该是幂等的。 幂等的意思是一个程序执行多次结果是一样的。
面向对象编程:工具和方法通常是服务于对象的。
函数式编程是倾向于纯函数,那抽象逻辑应该考虑面向对象编程。
JS具体而言应该是:基于原型的对象封装、重用、继承。对于JAVA来说,对象于类之间是拷贝、从属的关系,而JS中,类的继承是原型链接关系,对象通过原型链来寻找原型中的功能,利用链接而不是拷贝来。
A.prototype = Object.create(B.prototype); A的原型链上链接到B的原型和B的原型链
有输入输出的函数怎么在不可变原则下管理变化的状态呢?
上文中的不可变原则指的是:不影响外部数据的值变动。
也就是保持一个值在项目的生命周期中的持久化。
我们的应用肯定需要和用户交互,而一旦有交互,我们就需要管理值的状态(state) 和围绕值设计一系列行为(behavior)。在这个过程中,我们需要考虑的就是一个值的结构性不可变的问题。
数据分为基本类型和复杂类型,针对原始类型的数据,无需过度担忧值的不可变。
我们担忧的不可变原则主要针对于复杂类型。
- 通过深拷贝来管理、更新数据。
- 在开发中,比较常用的深拷贝方式是:JSON.parse(JSON.stringify(obj))。虽然stringify方法在转化JSON字符串时有不少特殊状况。这种方式不会影响状态,因为stringify方法返回的是一个常量字符串。
性能考虑:
假如系统内有值一直变动,深拷贝对象将会占据大量内存。因此需要注意深拷贝的值不要被闭包内部引用,或者引起内存泄漏(作用域引用导致无法删除问题)
市面上存在的第三方库处理持久化数据:immutablejs
抽象概念的值操作
看到这一块我就没看懂了,文章中提出了一些reducer的使用方式和设计概念。
Reducer 预置知识
为什么叫 reducer
大概是由于 reducer 函数都能作为数组的 reduce 方法的参数,所以叫 reducer 的吧。
Array 中的 reduce 需要两个参数,一个是回调函数,一个是初始值,没有初始值,会默认把数组第一个当初始值,并从第二个开始reducer 是用于修改state值的纯函数操作,格式为
(previousState, action) => newState
,与 Array.reduce(reducer, ?initialValue) 这个函数很相似。
对于函数的书写,从具象化到抽象化的过程就是:组合和管道的工作机制(Compose Pipeline)
即 上一个函数的输出是下一个函数的输入,然后按顺序执行
Point-Free
Point-Pree 是函数式编程中的一种编程风格,其中的 Point 是指参数,Point-Free 的意思就是没有参数的函数。
通过这种方式,就可以将一个函数和另外一个函数结合起来,形成一个新函数。
迭代方法处理
比如使用到上一个数据叠加的情况,可以使用array.reduce
重用方法:组合、继承
- Object.create()
- new
- class
- Object.assign()可以用于组合。
JS中数据类型
js引擎堆栈、作用域、闭包
JS引擎存储数据时,堆栈内存是如何存储的呢?
栈是线性连续的数据结构的存储空间,里面主要存有 JavaScript 原始数据类型以及对象等复杂数据类型的地址。除此之外还有函数的执行状态和 this 值。堆是树形非连续的数据结构的存储空间,里面存储了对象、数组、函数等复杂数据类型,还有系统内置的 window 和 document 对象。
JS的作用域分成静态作用域(词法作用域)和动态作用域(变量作用域),静态作用域在编译时已经分析完毕,此时会做一些变量提升、函数声明提升的处理,而动态作用域会在执行前进行生成,也是语法解析。
和变量一样,函数声明也会被提升到顶部,而且如果函数和变量的提升同时发生,函数会被提到变量的前面。另外一点值得注意的是,如我们在前一讲所说,函数提升的只是声明式函数,而表达式函数则和变量赋值一样,不会被提升。
编译中:V8 会混合使用编译器和解释器技术的双轮驱动设计实时编译(JIT Just in Time),这个双轮的一个轮子是直接执行,另一个发现热点代码会优化成机器码再执行,这样做的目的是为了性能的权衡和提升。
执行时,函数的变量和上下文被压入栈中,随着函数结束,变量不再引用,函数作用域被释放。
闭包
而闭包的作用域不会被释放,因为它突破了作用域的限制,可谓“守正”配合的“出奇”操作。
function createCounter(){
let i=0;
function increment(){
i++;
}
function getValue(){
return i;
}
return {increment,getValue}
}
const counter = createCounter();
counter.increment();
counter.getValue(); // 返回1
counter.increment();
counter.getValue(); // 返回2
它的原理是什么呢?这要回到我们较早前说到的解析或语法分析步骤。在这个过程中,当 JavaScript 引擎解析函数的时候,会先进行词法分析,只分析到存在increment 和 getValue 函数,然后就不会继续向下分析。
直到执行之前再进行动态作用域生成,分析引用变量,可以看到 increment 和 getValue 会引用一个外部的变量 i,所以JS会把这个变量从栈移到堆中,就用了更长的记忆来记录 i 的值。通过这种方式,闭包就做到了守正出奇,以此突破了作用域和生命周期的限制。
但是有一点要注意的是,考虑到性能、内存和执行速度,当使用闭包的时候,我们就要注意尽量使用本地而不要用全局变量。
算法中的迭代和递归
(不知道为什么就讲到了一些算法内容)
对于有规律的重复功能,迭代就是类似于使用遍历去完成,递归就是反复调用本函数进行重复计算。
递归主要有两个条件:1. 有终止条件。2. 递归内容为重复的公式内容。
作者介绍了递归的一些使用技巧:
递归解决斐波那契数列问题:
function fibRecursive(n){
// 基本条件
if (n < 1) return 0;
// 基本条件
if (n <= 2) return 1;
// 递归+分治
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
console.log(fibRecursive(10)); // 55
- 记忆函数
在这里,fibonacci 是一个递归,但是我们让它调用了一个外部的 memo 参数,这样一来 memo 就带有了“记忆”。我们可以用它来存储上一次计算的值,就可以避免重复计算了。所以记忆函数经常和递归结合起来使用。这里解决的重复计算问题,在算法中也被称为重叠子问题,而记忆函数就是一个备忘录。
function fibMemo(n, memo = [0, 1, 1]) {
if (memo[n]) {
return memo[n];
}
// 递归+分治+闭包
memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
return memo[n];
}
console.log(fibMemo(10)); // 55
- 尾递归
尾递归的意思就是在函数的尾部执行递归的调用。它有利于降低函数的时间复杂度、空间复杂度。它直接调用和返回函数。
function fibTailRecursive(n, lastlast, last){
if (n == 0) {
return lastlast;
}
if (n == 1) {
return last;
}
return fibTailRecursive(n-1, last, lastlast + last);
}
console.log(fibTailRecursive(10, 0, 1)); // 55
在实际操作中,绝大多数浏览器都会自己定义一个防止栈溢出的限制,比如 Chrome 在一个函数执行了 13952 次之后,就出现了一个超出最大栈范围的错误消息,并且停止了递归。
尾调用优化可以避免溢出限制,但是现实浏览器环境中不是所有的引擎都支持尾调用优化,(也是因为优化的效益不明显。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
然后接下来都是一些算法说明,跳过先了。
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,是 ES6 为了操作对象而提供的新 API。
Reflect不是一个函数对象,因此它是不可构造的。
Reflect的所有属性和方法都是静态的。
意义
- 现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
- 修改某些Object方法的返回结果,让其变得更规范化。如Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
// 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。
Reflect.apply(target, thisArg, args)
// 对构造函数进行 new 操作,相当于执行 new target(...args)。
Reflect.construct(target, args)
// 获取对象身上某个属性的值,类似于 target[name]。如果没有该属性,则返回undefined。
Reflect.get(target, name, receiver)
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, name, value, receiver)
// Reflect.defineProperty方法基本等同于Object.defineProperty,直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,不同的是,Object.defineProperty返回此对象。而Reflect.defineProperty会返回布尔值.
Reflect.defineProperty(target, name, desc)
// 作为函数的delete操作符,相当于执行 delete target[name]。
Reflect.deleteProperty(target, name)
// 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.has(target, name)
// 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响, Object.keys返回所有可枚举属性的字符串数组).
Reflect.ownKeys(target)
// 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性),类似于 Object.isExtensible()。返回表示给定对象是否可扩展的一个Boolean 。(Object.seal 或 Object.freeze 方法都可以标记一个对象为不可扩展。)
Reflect.isExtensible(target)
// 让一个对象变的不可扩展,也就是永远不能再添加新的属性。
Reflect.preventExtensions(target)
// 如果对象中存在该属性,如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。类似于 Object.getOwnPropertyDescriptor()。
Reflect.getOwnPropertyDescriptor(target, name)
// 返回指定对象的原型.类似于 Object.getOwnPropertyDescriptor()。
Reflect.getPrototypeOf(target)
// 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。如果 target 不是 Object ,或 prototype 既不是对象也不是 null,抛出一个 TypeError 异常。
Reflect.setPrototypeOf(target, prototype)