JS进阶(一)

编程模式

从编程模式的角度看 JavaScript,它是结构化的、事件驱动的动态语言,且支持声明式和指令式两种模式。所以我们说,JavaScript 是一个多模式(multi-paradigm)的语言,也是一门“丰富”的语言。
在 JavaScript 所支持的编程模式中,用得最多的是面向对象(OOP object oriented programming)和函数式(FP functional programming)两种,其中又以面向对象的普及率最高。

函数式编程:管理和解决副作用

减少副作用,纯函数和不可变

副作用:
函数已经把算法封装了起来,那么函数里相对就是可控的,而比较不可控的是外部环境。这里,我们可以把不可控的外部环境分为三大类。

  1. 全局变量。比如在外部定义了一个var类型全局变量,每次函数中使用时,都不确定变量是否变动。
  2. IO影响。Node端文件系统、网络链接以及其他IO,还有前端浏览器中的用户行为,比如鼠标输入输出等。
  3. 网络请求相关。

在函数式编程中,有两个核心概念:纯函数(pure function)和不可变(immutability)。这是一个“双循环”,纯函数更多解决的是“内循环”;而不可变更多考虑的是“外循环”。

  1. 纯函数的意思是说,一个函数的返回结果的变化只依赖其参数,并且执行过程中没有副作用。
  2. 不可变是指函数对外界变量的影响 应该是“不可变”的。
    引用传值的基础上直接修改值,会改变原有地址的值,比如spliceslice
    因为把一个外部变量作为参数作为输入,在函数里做了改变,作为输出返回 的这个过程中,可能不可预料这种变化会对整个系统造成什么样的影响。
  3. 纯函数应该是幂等的。 幂等的意思是一个程序执行多次结果是一样的。

面向对象编程:工具和方法通常是服务于对象的。

函数式编程是倾向于纯函数,那抽象逻辑应该考虑面向对象编程。
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
  1. 记忆函数
    在这里,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
  1. 尾递归
    尾递归的意思就是在函数的尾部执行递归的调用。它有利于降低函数的时间复杂度、空间复杂度。它直接调用和返回函数。
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的所有属性和方法都是静态的。

意义
  1. 现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
  2. 修改某些Object方法的返回结果,让其变得更规范化。如Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  3. 让Object操作都变成函数行为。
  4. 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)

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值