《Eloquent JavaScript 3rd》笔记

前言篇

计算机是人思想的一部分,逻辑、秩序、规则…… 不常见的任务需要额外的编程才能解决。

机器很死板,无趣。

忍受机器后能享受到便利。

借鉴了自然界以及人类的模式到编程的设计上。

编程的规则原理语法很简单,但创造出满足复杂世界的需求会很难。

学习中的有些困难和痛苦势必要的。这样之后的学习就更容易了。

不要妄自菲薄,换换方式,或者歇一歇,然后坚持学习。

计算机结构是庞大的,找到围绕目标的所需的知识,理解运行原理。

计算机很蠢,但是优点是很快。

控制计算机解决问题,就是掌控复杂度的艺术,掌控不好就崩了。。

新的问题往往用旧的眼光和方式解决不好,或者不够好。

不断的实践出错优化才能融会贯通各种情况下的考虑,而简单的阅读却没办法做到。

语言怎么起作用的

从最底层到最接近问题,编程语言存在着不同层面的抽象,有时候细节需要忽略,有时候又要操纵细节,这方面做得最好的是C++,而JS是主要面向浏览器的。它的很多抽象层级都面向浏览器的应用而不是性能,底层,等等。

干同一件事的代码可以写成很多样子,是更易阅读,更省性能还是写代码用的时间最少(这是人和公司最宝贵的东西,除此之外才是金钱花费),取决于你的需求进行平衡取舍。

什么JS

JS最早是写浏览器交互、特效之类的小脚本。

JS与Java有关,不然为啥不起别名,但仅仅是为了蹭市场名声,技术实现上上大抵都无关。

JS在编程语言中很烂,但它面向新手,使用的人多了,一次次版本更新也变好起来了。曲线救国,人多才是王道。

JS历史不谈,2015年后ES6版本,每年都有更新,所以快更新浏览器与时俱进吧!

JS还在MongoDB中可以作为脚本查询语言、Node.js服务器语言。

编码

不仅要学,还要写出来。这是一门工程学科,实现是根本,学是为了实现的更好。 Don’t assume you understand them until you’ve actually written a working solution.

全局概览

语言,浏览器,Node.js。也就是能干什么,在哪里干,还可以在哪里干。

值,类型,操作符

程序的表面之下是庞大的代码支撑着

计算机中只有数据,数据的信息可以被解释为控制指令、文件内容等。

二进制的bit,binary digit,只有0和1的序列,代表着各种信息。

例如数字13的二进制表示。

   0   0   0   0   1   1   0   1
 128  64  32  16   8   4   2   1
复制代码

易失性内存:8g -> 687 1947 6736 bits,687亿个比特(吓了一跳吧~)

管理如此大量的比特们,需要进行分模块处理,不然会迷失混乱。values, 编程语言中的各种值来划分不同含义的比特信息,值多了抽象出类型,具有同样的特点,细化下去就是值的不同,再向上抽象就是类型的不同。

计算机是复杂的,内存也是复杂的,但人的生命是有限的,没办法了解所有的细节。(这是所有矛盾的源头)

  • 但在编程语言中只关心重要的抽象,省略掉不关心的部分。
  • 比如我需要一个变量来放今天早餐花费了多少钱
    • 关心的部分:
      • 一个是名字花费cost
      • 一个是金额6
      • 合起来就是let cost = 6;
    • 不关心的部分:
      • 上面那行命令怎么从键盘到CPU传递
      • CPU怎么理解6cost绑定的值,还是这行命令执行6次。
      • CPU把6给我存在哪个地方
      • 这个6可以共享给其他变量,还是不能重复利用的的独一无二的
      • cost怎么和6进行的绑定……
    • 事实上是规定不能关心么,不,你可以无聊或者感兴趣研究它到底在某一步干了什么,毕竟种族中随机的变异导致人多了就会有小部分人这么执着于每个地方,关键是大部分人的时间都很宝贵,我们在时时刻刻变老,人脑处理的信息是固定的,而计算机的发展是无数前辈智慧的结晶,我们能用一个人的一生去了解这么庞大的知识?不存在的,只能挑重点。

数字

number, 64bits

  • 不同信息的表示共有2^64种,每创建一个数字的变量,计算机会申请这么大的空间,换成十进制有20位,一般是不用关心会超过申请空间(overflow)。
  • 3.14
    • 浮点数表示只会把小数的字面值精确到某位存储起来,不会完全复原小数,所以判断时和标准比较差值,
    • 如我想要一个数是3.14误差在0.01,那么判断if((x-3.14)<0.01), 而不是 if(x==3.14)
  • 314 可以表示为3.14e2, e(exponent)

数字一般还要分出一位用来表示正负,如果还要表示小数就要有更多位的损耗,有各种表示小数的方法,你看想要完成更多的功能,就肯定要付出一些东西。

算术

数字的主要功能是用来算术,所以这些东西不是被凭空发明出来的,而是实实在在有用才会出现在JavaScript中。

  • 二元操作符:+ - * / %

  • precedence: * / % > + -

  • %: remainder operator

    • 除余,咔嚓咔嚓除完剩下的,多好理解,比死背强记简单多了
  • 改变默认优先级:括号

3种特殊数字

  1. Infinity, -Infinity
  2. NaN: not a number

字符串

  • 好宽容,三种符号可表示字符串,没有character类型
    1. backticks,反引号中的string,也叫template literal
    2. 换行符不用进行转义就可以保留
    3. half of 100 is ${100 / 2}
    4. 计算${}
    5. 转换成字符
    6. 被包含
    7. ''
    8. ""
  • 转义字符:backslash,\
    • 意味着这个字符之后的应该特殊对待,
  • 字符串中的每个字母占16bits,小于Unicode数量,所以有些字符用占2个字母的空
  • 只有+操作符:concatenates

一元操作符

操作符除了用符号表示,再多了符号不够用命名关键字表示.

操作符的操作数可以是一个,两个……

一元操作符:

  • typeof: return a string of type of the operand

符号的一词多义:

  • 原因:因为键盘的符号有限不够用,有的符号又当爹又当妈
  • -(2-1):
    • 第一个-,unary operator,操作的对象是一个,即2-1的结果,语义是将一个数置负,
    • 第二个-,binary operator,需要两个操作对象,语义是有两个数,他们想减

布尔值

信息的存储最理想状态是e次方,即2.718,而现实离理想还有段距离,所以目前的计算机是基于二进制的,实际上三进制更理想。不过二进制是第二好的选择,比十进制不知道高到哪里去了。

既然是二进制,就免不了这个二的状态由哪两个表示,计算机说是0和1,实际上在不同器件里面的表示,有用电平高低的,有用正弦不同的,程序利用truefalse,平常我们说话用有和没有,是或者不是,开或者关,如果是三进制那还包括一种情况,那就是不知

比较

比较的结果是个布尔值

字符的比较是按照ASCII码以及Unicode码来的,而不是字典中从A到Z的顺序,注意Unicode字符是ASCII的超集,然后ASCII中的编码在Unicode中大小是相同的,所谓的兼容ASCII,无非是前面多8个0。

只有一种值不等于它自己: NaN,它的含义就是用来表示无意义的结果,所以他也不和其他无意义结果相等。Oh,shit,难以理解。。

逻辑操作符

语义:注意它的操作对象是布尔值,不是用来算数的,尽管数字0和1会被变量提升转换成true 和 false。

  • and: &&
  • or: ||
  • not: !

precedence: > == > && > ||

一个三元的逻辑操作符:true ? 1 : 2, ternary, conditional operator, question mark and a colon

三元操作符短路,意味着有些语句不执行。

Short-circuiting of logical operators

短路,左边算完了如果返回左边,右边就不验证了,称short-circuit evaluation.

||左边true就返回左边,false就返回右边。 &&左边false就返回左边,true就返回右边。

我从未听说过布尔逻辑的处理是这样的,什么叫and运算?左边false,就返回左边对象?为什么不是返回false?我要你这个运算何用?这种神奇的功能我自己八辈子都用不到

垃圾语言,奇葩设定,毁我青春,****。

WTF为什么不和C++,Java一样?

Examples of expressions that can be converted to false are:

null;
NaN;
0;
empty string ("" or '' or ``); 
undefined.
复制代码
  1. ||先把左边转换成布尔值,是个数就返回左边,不是个数返回右边
  2. 转化规则
-  `0`, `NaN`, `""`, `null`, `undifined`会被算作 `false`
- 其他的所有算作`true`
复制代码
  1. true 则返回左边
- `console.log('bat||'ant')  // -> bat`
复制代码
  1. false 则返回右边
- `console.log(null||'hi')  // -> hi`
复制代码
  • 利用这个特性,可以将可能是空值的变量加上||,使之成为备胎。

空值

设计上的委曲求全,唉,可是Rust语言不火啊,人们并不需要正确,人们需要兼容稳定能用。

  1. null
  2. undefined

自动类型转换

JS总是喜欢能处理你给的各种值,也就是对你很包容,但这对于精确的控制却不好。语言的设计

type coercion

  • null*8 -> 0

  • "5" - 1 -> 4字符拼接优先于数值计算。

  • "5" + 1 -> 51

  • NaN如果一旦产生,那么与此相关的结果还是会NaN

  • 自动类型转换和严格相等

    • ==!=
      • same type
        • 除了NaN
      • diff type
        • nullundefined之间为true,和其他为false
        • 其他类型自动转换
    • ===!==
      • 不包括自动类型转换

程序结构

表达式和语句 expressions and statements

砖块在高楼中才能体现更大的价值。意思是良禽择木而栖才能发挥更大价值?

能够产生一个值的代码块称之为表达式,expression。字面值,带括号,运算式都是expression

expression之间互相组合、嵌套组成了语义更复杂的expression。

expression是某一小段子句,而语句,statement是一句完整的话。程序就是一列语句们的集合。

最简单的statement是一个expression加上一个分号。(可忽略的分号。。。)

expression产生一个值,然后被周围的代码所利用。

而statement只立足于他自己,除非指定和其他东西有交集。当改变了屏幕上的文字或者改变了计算机内部的一些状态,以致于影响后来的语句执行的结果,这就叫做影响,effects。像1;这种语句确实改变了了计算机内部的东西,但对于别的代码没有明显的作用。

分号这个东西大部分时间可加可不加,但是你懂的总有意外。有时候不加分号会让会让两行代码变成互相影响的一句statement,为了安全、不找麻烦,还是加上吧,不然要认识很多哪些是必须加分号的复杂情况。

绑定 bindings

表达式会产生一个值,比如1+2,但是产生的结果3,如果不立刻使用它,或找一个空间分给他,它马上就不见了。

let caught = 5 * 5;

variable or binding

keyword: let, 声明+赋值

  • 声明了caught,就是宣布要有光!,然后计算机内存里面就找到一个地方对应着这个caught.
  • 赋值,右边5*5的结果绑定到了caught上。
  • 只声明没赋值的情况为undifined
  • 之后再次出现caught,它就会被要么作为空间地址,要么取它的值25
  • 一次声名,多次绑定。
  • 绑定更像触手一样可以多个绑定指向同一个值。这句话很费解,是指变量只是个引用,这个值是共享的?还是说绑定的值和引用的位置是分离的?

var是个历史遗留问题,const一次声明赋值为常量,鞠躬尽瘁,死而后已。

Binding names

字母、数字、$_,不能以数字开头,不能用关键字,不能用保留字。

其实就一个原则就好了,字母开头,各种驼峰代表变量、函数、还是类。也别起什么各种奇葩名字,没事找事。

Environment

程序启动的时候,环境就激活了语言本身的一部分绑定,以及提供交互的一些绑定。

Functions

默认环境提供的一些变量类型为function.

调用、请求函数的执行,称invoking, calling, applying.

回想数学f(x)=y+1;函数要有输入x,用括号来表示。称arguments, 这个参数可能(x,y),也可以是(x,3,4)

console.log function

console.log 不是一个绑定,它包含了一个句号,对吧,变量名是不允许的,那它是什么呢? console是一个绑定,.log代表检索这个绑定的一个名为log的property。

Return values

side effect: 函数的主要作用是用来返回一个值的,那么显示一段文字,弹出对话框叫做副作用。(这是函数定义,不是写代码的目的。。)

由于函数要return一个结果,所以它符合一个expression,也就是可以和另外的expression组合嵌套在一句statement中。console.log(Math.min(1, 3)+1);

Control flow

straight-line

prompt中输入的值为string,用Number()转换成number进行计算,其实不用人工转化下一行计算的时候会自动转换类型的。。

conditional execution

原先一条路走到黑,现在变成二选一,然后继续走下面的语句。

if (1 + 1 == 2) console.log("It's true");

一般情况下代码要写的让人看着方便,大部分都要加大括号,除非一行简单的if。

多重嵌套的时候每次当下判断都是二选一,注意合并简化

while & do loop

2^10 (2 to the 10th power)

do while,for 和 while(){}的区别就是语义上的

  • 最少干一次用do while
  • 先判断条件再做就用while
  • for和wihle大部分等价
    • 将三个东西作为一个整体划分比while强一点。哪三个东西呢?第一个初始化条件就是while之前的语句,第二个判断条件就是while的判断条件,最后一个执行语句可以放在while执行语句的最后。
    • break时都是直接退出
    • 小部分的区别在于continue
      • while会直接跳过后面代码重新判断,
      • for会直接跳过后面的代码,执行第三条语句后再去重新判断。

缩进空格、换行

纯粹为了易读性,一个还是两个空格、一个还是多个换行都不影响它的逻辑。但换不换行是有区别的,参考加不加;的各种规则。

for循环

明明有while为什么还要发明for呢?因为常用啊,因为代码逻辑惊人的相似重复的部分人就愿意把它封装成新东西,轻轻一挥,魔法就实现了,多好。

for和while的区别就是while经常需要一个循环计数器,并且每轮循环都必须要改动这个循环计数器,再加上本来的条件判断,这不就是for啦?

break continue

for中不设循环终止判断,就可以把判断挪到循环体中if(){*; *; break;}中,这样的好处是判断完之后可以继续在for的环境中执行一些语句。也就是说如果有if(){break;},考虑是否可以放在for语句头中?

break 如果用原来的机制实现,相当于在原代码前加了个if,并且添加的一个分支只有一个改变循环条件、并且还要抵消for第三个语句(如果在for语句中)。你看如今你想要的,一个break就可以解决了。

continue代表着跳过这句之后的循环体中的代码,执行for第三部分(如果在for语句中)。然后继续下一次循环

continue 如果用原来机制实现,相当于原代码前添加if,并且添加的一个分支什么都不做,并且还要抵消for第三个语句(如果在for语句中)。

这两个一个是直接跳出所有循环,一个跳出当前这一轮循环。

updating bindings succinctly

当一个变量的变化是由原来的自己进行更新的话,可以直接在赋值的时候指定什么操作

counter = counter +1;

// 更简便的自我更新

counter += 1;

// 对于自我的加一减一,还可以缩写

counter ++;

// 在Java中 a++ 和 ++a 实现不一样,返回的对象是一份原a对象的拷贝对象,所以值和原a一样,后者返回的是加完的a对象,所以相对于原a值加一。在JS中还不知道啥实现

复制代码

switch

注意default:break:,虽然很多时候工具会自动猜测你的意图,帮你补全一些不严谨的逻辑。

一个多状态的值 --> 多重if的代码的简写模式 --> 就是switch。。常用的东西才会被发明。

多对多的怎么办呢?用函数封装一下,多个输入值,每个值有多个状态,随你所愿的组合判断,

Capitalization

  1. 全小写不易读
  2. 下划线,打字多太累
  3. 全大写是函数绑定的构造器
  4. 驼峰风格是惯例。。

一切都是有原因的,但是所有的事我们真的需要知道原因?

注释

有时候代码并不能层次鲜明的由浅入深,由全局到定位局部信息的导航功能,这个时候需要注释来快速定位一大坨只有一个名字的代码到底是干什么的。

VS Code中快捷键Ctrl /可以快速判断当前是HTML还是CSS还是JavaScript还是JSX代码进行插入相应的注释。

注意多行嵌套没有实现(不是不能实现)交错的处理。以及 //右边全算作注释

练习

Looping a triangle

Q1:

#
##
###
####
#####
######
#######
复制代码

A1:

for (let i=0, j=""; i<7; i++) {
  j += "#";
  console.log(j);
}
复制代码
  • i仅代表从第1到第7行,而这每一行打印出几个的井号跟i无关(至少在这题里面,有的题打印的星星数量可能跟行号有数学关系),这7行每行打印出什么跟j的自加有关。
  • 当然这两行代码因为每次迭代都执行了,可以跟i++放在一起,看个人喜好了,放在那里太挤太丑了不是么。。
  • 仅从该题目的话,这些代码是合格的,但如果考虑扩展性(比如用户指定打多少行、每行的#的数量表达式),效率(是一次性打印,还是逐行打印)等等,就会有不同的解法

Q2:

print number 1-100, 3的倍数用Fizz代替,5的倍数用Buzz代替,若是3和5的倍数,用15代替
复制代码

A2:

for(let i=1; i<=100; i++) {
  let result = new Array();
  let string = "";
  let number = i;
  if(i%3===0) {
    string += "Fizz";
  }
  if(i%5===0) {
    string += "Buzz";
  }
  result.push(string||number);
  for(let i of result) {
    console.log(i);
  }
}
复制代码
  • if 没有else的时候,要注意else和if之后的语句不一样,因为else是做过判断的,跟判断有关就加个else,跟判断无关,就不加else,直接在后面写。
  • i代表着迭代多少次,number其实可以直接用i不用新建,因为正好题目是1到100,但如果是100到200呢?这就需要将打印的次数和打印的数字区分开,虽然有时候他们值相等不用新建变量,但他们语义是不一样的!
  • 首先3和5的倍数是两个原子不可拆的操作,而15不是,所以再添加分支判断15就很浪费,而15实际上就是3和5字符的相加,所以每一步字符是加操作。其次根据JavaScrit||的奇葩设定正好帮助返回FizzBuzz或者备胎数字,纳爱斯。
  • 将所有结果的数据存到了数组中,不仅合乎打印出的结果,更容易日后其他操作,虽然只是一道题,不需要啥扩展性 Q3:
Chessboard
 # # # #
# # # # 
 # # # #
# # # # 
 # # # #
# # # # 
 # # # #
# # # #
复制代码

A3:

// 若将该功能封装为函数,下面就是输入的参数变量,n阶矩阵,两种要打印的符号
  let size = 8;
  let symbol1 = " ";
  let symbol2 = "#";
// 横向重复的最小单元,要进行O(logn) 替换掉 O(n)的重复字符的复制
  let symbolSum = symbol1 + symbol2;
  let contentOddAddon = size%2==1?symbol1:"";
  let contentOdd = repeatCharBinary(symbolSum, Math.floor(size/2)) + contentOddAddon;
// 求偶数行的内容,由奇数行内容转换,踢掉第一个,再根据最后一个添加适当元素
  let contentEvenArray = contentOdd.split("");
  contentEvenArray.shift();
  contentEvenArray.push((contentEvenArray[contentEvenArray.length-1]==symbol1)?symbol2:symbol1);
  let contentEven = contentEvenArray.join("");
// 纵向重复的最小单元,要进行O(logn) 替换掉 O(n)的重复字符的复制
  let contentTwoLine = contentOdd + "\n" + contentEven + "\n";
  let contentOddLineAddon = size%2===1?contentOdd:"";
  let content = repeatCharBinary(contentTwoLine, Math.floor(size/2)) + contentOddLineAddon;
  console.log(content);
// 重复某个字符的nlog函数
  function repeatCharBinary(char, n) {
    // 这个地方需要进行重复计算字符,可以将代码封装为一个函数
    // 线性复制char更改为指数级复制
    // 将十进制n转换为二进制,除2取余,对应1、2、4、8、16的权重
    // 余1则存在,那么将结果加上对应的权重数量的字符串,余0则表示不存在,那就不加这位的字符串。
    let tmp="";
    while(n!=0) {
      if(n%2==1) {
        tmp += char;
      }
      n = parseInt(n/2); //结果从低位算起,余1则存在,结果加之,继续除2余1,算更高一位
      // 之前算每一位的权重是否存在,这一行是算如果存在,对应的字符串长多少,每轮要变为更低位字符串的两倍。
      char += char; 
    }
    return tmp;
  }
复制代码
  • 题目两种不同的符号,交叉成字符串,宽n,高n,奇偶互异,思路是
  1. 计算出第一行的内容
  2. 模式就是两个字符的多次重复
  3. 偶数n/2次
  4. 基数n/2次+第一个字符
  5. 这么多次的重复采用8421的指数翻倍相加?
  6. 第一行内容队列一进一出形成第二行内容
  7. 第一二行作为一个重复的最小单位,然后多次指数重复输出
  8. 取整要用Math.floor(),默认的取整不知实现机制,不过总是少一个,很奇怪。

Function

搞计算机的是天才么?不是的,大家只是站在巨人的肩膀上,用别人生产的砖块垒起自己想要的一面墙。

函数是个很重要的内容。它将一大块代码浓缩成一个名字。这样可以分解一个大问题的庞大代码,变成几个小问题的代码,只抽象出变化的部分,称之为输入,然后输入参数进入同样逻辑的代码中,产生不同的输出。而仅仅只需要用一个函数名字就可以关联变量和逻辑。

优秀的文章经常通过不同的词汇乃至精妙绝伦的句子来体现文章的艺术,而编程不同,它只在乎能干成什么事,所以可能不那么动听引人入胜。

编程中的语言就像公式一样枯燥,但如果懂了就明白它的精确和简洁,不同于文章给人精神上的享受,类似的账单也很枯燥,但它精确简洁,并且你需要它的时候,他能满足你。

defining a function

let area = function(x,y) { return x*y;}
复制代码

可以通过绑定到一个变量来重复调用这个函数。

函数功能

  1. 输入
  2. 可以没有
  3. 输出
  4. 没有return,执行完默认return 一个undefined
  5. return
  6. return; 则return 一个undefined
  7. side effect

bindings and scopes

作用域只在当前以及子作用域

  1. Global
  2. Local bindings
  3. 函数的每次调用都会新建一个实例
  4. ES2015前,只有函数能创建新作用域(比如if for 都默认是global)
  5. ES2015后,JS的作用域终于和C++Java等正常语言一致了。。

nested scope

多级嵌套,lexical scoping

Functions as values

let hi = function() {

};
复制代码

新建变量的函数绑定,要加分号。

函数绑定的名字呢,还可以被绑定为别的东西,所以变量并不是函数,他只是个指示器,代表着它可能指向一个数字、字符串、或者是函数等。

declaration notation

声明式的函数标记, 仅仅声明,不进行调用

function hello() {}
复制代码
  1. 可以不用加分号
  2. 这种函数在执行的时候会挪到当前作用域的最开始部分优先于其他代码

arrow function

let hey = (x, y) => {

};
复制代码

省略了函数名,然后直接将输入指向输出。

const square = x => x*x;
复制代码
  1. 只有1个变量的时候可以省略括号
  2. 只有一行语句要return的话,可以省略大括号。 涉及到知识点
  3. 递归
  • 要有终止条件
  • 为了学习语法的demo并不代表最优解
  • 既然出现递归和循环都可以干的事, 那么说明他们有擅长的方面, 不然没价值的东西不会出现在语言中,但是对于某个问题可能只有其中一种是最好的.
  1. 边界条件, 特殊值的覆盖
  • 关键要找到典型的例子进行覆盖测试.
  • 如果例子不用典型代表, 测试的次数太多.
  • 如果例子选出来了但代表不典型, 覆盖的范围太少.
  1. 有问题先Google
  • 那里是广阔的海洋, 数不清的金银混杂着狗屎, 但一般而言被筛选到第一页搜索结果的都是上好的金子.
  1. 默认参数
  2. 箭头函数
  3. for中的i,默认为循环计数器
  • 如果用它做了别的含义,最好新建变量,使之修改的时候更容易
  1. 统一的函数结果的返回接口, 容易修改和阅读
  • 但在多次递归中直接不管进入到哪个分支中都进行return, 是不是语义更清晰些?少绕一步赋值给结果,再return出去.
  • 但是如果是复杂的大量代码有利于定位return值的变化
  1. 拼写错误, WTF!
  2. 再小的代码, 实现起来也是要费些周折, 不要忽略实际问题而想当然
  3. 语法是会慢慢进化帮助开发者的, 比如最后一个参数之后也可以加,, 方便日后直接添加.
let power = (base, exponent=2) => {
  let result;
  if(exponent===0) {
    result = 1;
  } else {
    result = base * power(base, exponent-1);
  }
  return result;
};
console.log(
  power(0),
  power(1),
  power(0,3),
  power(2,1),
  power(2,2),
  power(3),
  power(100,3),
  power(100,100),
);
复制代码

the call stack

函数执行完会把return的值返还到调用它的地方,然后继续执行接下来的代码。

call stack: 函数调用的栈

  1. 每调用一个新的就会push进一个新的函数执行完再pop出去
  2. 这个栈是占内存空间的,如果调用、递归的函数太多,栈会溢出

optional arguments

给一个函数多个参数,而函数只要一个的时候,它就取一个并且继续执行。

而如果给的函数不够,那么默认undefined

怎么样JS是不是很混蛋?如果程序员无意写错了,他都不报错?

那如果有意写成这样,相当于C++ 中的函数重载。

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}
复制代码

哇,好牛逼。。这样竟然实现了减法。。一个数就是减自己,两个就是互相减

默认参数, 另外指定了则按指定的,无指定的按默认的

funcition area(redius, PI=3.14) {

}
复制代码

closure

ES2015后改为了正常编程语言的作用域规则。chain scope,子可以看到父,父不可看到子的变量。 ES2015之前只有function有自己的作用域。

ES2015之前定义的var关键字最好用let, const代替。 如果没有关键字直接hi="hi",默认是全局作用域

内部作用域可以调用父作用域的变量,那么如果父作用域想调用子作用域呢?

让父函数新建一个子函数,并return这个子函数,子函数可以访问它爹,然后新建一个变量承接父函数,这个变量就绑定到了return回的子函数。

如果这个变量是全局的,那么这个子函数不会回收,所以它爹也不会回收。

同样不用var新建的变量如果绑定到了一个函数,也是全局变量,不会回收。

所以注意垃圾满了。尽量不需要不要用闭包。

这样引用一个函数实例的一个子函数,称之为一个闭包。

recursion

函数调用自己叫做递归,递归要有终止条件return出去.

  1. 递归实现比for 更慢3倍。。因为调用函数需要的资源更多。
  2. 注意栈溢出

优雅和速度总是很有趣的矛盾体。。。更快意味着可能只顾目的,吃相不好看。更好看,可能顾及的就多,妨碍了核心目标。所以以切都是权衡,哪些必须要快不顾长相,快到什么程度?同样相反。

一个程序的项目实现的地方有很多,市场、宣传、技术、组织。而技术之中也有开发效率、机器效率、用户友好、利于维护等一堆互相矛盾的点,要抓住主要的目的,势必就要在其他点上妥协。你不可能用最快的开发速度,写出最省机器资源、最利于维护扩展、最对用户友好的代码。

机器的快速发展,所以程序语言越来越高级,库越来越顺手,你如果老是担心这个地方性能不好,那么你想想你有没有因为自由的呼吸空气而觉得浪费,有没有觉得自己跑步呼吸了更多的空气而浪费?为什么?因为空气与你的呼吸相比很廉价,同样在机器相对于人的时间很廉价。所以那段代码利于你快速理解阅读、并且最快的满足功能,为什么要去省下几口空气呢?除非你掉进了水里(机器运行吃力),当空气(性能)真的很重要的时候,再去考虑空气(性能)。

甚至很多你觉的一定要优化的东西根本不值一提, 就像你多呼吸了两口空气一样无关紧要.你收货的只有沾沾自喜以为赚到了, 但你失去的是你最宝贵的生命的几分钟甚至几个小时.

还有很多时候你所谓的优化在优化编译器的专业大师前不值一提, 它甚至本来可以大幅度自动优化的地方,被你用生涩的奇技淫巧仅仅优化了一点.不要自作聪明, 衡量你生命的付出和换来的产出而不仅仅是沾沾自喜.

求从1开始, [+5]或者[*3]得到指定数的方法

递归在多分支的时候实现起来比循环更好。

  1. 注意递归终结条件, 只能是成功或者找不到, 而不是最短路径.
  2. 本质就是暴力尝试...只不过用递归写起来简单.
  3. 只有反引号里才能透明写string模板, ${ variable }添加变量.
  4. 两个参数一个作为计算, 一个作为输出展示. 这是两个信息, 毕竟没办法打印一个算式的过程? 目前孤陋寡闻...
  5. 运用递归要找到问题中能转化成初始条件, 层层相扣重复的逻辑, 以及终结条件, 不要用过程来思考, 容易卡死..
    let findIt = target => {
      function find(current, history) {
        let result;
        if (current === target) {
          result = history;
        } else if (current > target) {
          result = null;
        } else {
          result =  find(current + 5, `(${history} + 5)`) || 
                    find(current * 3, `(${history} * 3)`);
        }
        return result;
      }
      return find(1, "1");
    }
复制代码

growing functions

我的函数, 进化吧! 一般对函数的需求体现在两方面:

  1. 重复的代码段,很大程度上逻辑相似.如果不用函数的隐患有:
  2. 最重要的不是性能, 而是多次重复的代码更大可能的出错.
  3. 其次是代码太多不利于人阅读.
  4. 再然后才会是机器性能的浪费.
  5. 你觉的那应该有这么一个函数,尽管还没写,预先的感觉.
农场第一版
  1. 007 Cows
  2. 011 Chickens

即保证数字为3, 真的是闻所未闻, 用字符串长度来做判断让其一次次的加0直到3位...

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);
复制代码
农场第二版

当我们在农场又加了猪之后, 重复的代码块被封装再一个函数里面.

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);
复制代码

函数名字又臭又长, 这里面有几个基本的信息

  • 数字补全到多少位(width), 需要补全(width-length)位, 可能农场变大之后就上千上万需要更多位
  • 数字本身是多少(number), 它的范围如果超过宽度呢呢?
  • 用什么补全会变化么? 不会..因为是用0, 这叫封装不变的, 留出变化的接口(函数的输入)

一个名字意思精炼小巧的函数, 不仅能让人一眼看出它的意义, 还可以让别的代码复用这种基础的函数, 想一想语言本身带的那些常用的函数, 越是通用越是功能单一, 越是可以互相组合成为更强大的功能. 想一想键盘上的26个字符, 不仅能打出成千上万的英文文章, 还能打出汉字来.靠的就是抽象基本模式,使得记忆负担小, 这叫讨人喜欢, 然后互相组合能打出各种字, 这叫功能强大. 如果是无意义的编码,你要记2k多种没有规律的基本汉字的编码, 不讨人喜欢吧, 虽然功能也强大.

原则: 不要乱优化修改, 除非你明确的需要, 不然一切从简,快,正确. 当你仅仅需要一朵花的时候, 那就不要又设置花盆又设置肥料. 要控制住你的冲动, 你优化的东西可能和你的目的没多大关联, 仅仅是来自基因的倾向性要让你熟悉探索周围环境, 因为你可能没写多少有意义的代码, 却浪费时间改来改去让机器内存降低0.01%?那主要任务怎么办?

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);
复制代码

functions and side effects

函数可以用来return一个有用的值, 也可以side effect.

但主要作用在return的值上面的函数, 才可以有用的和其他函数组合.为啥? 因为side effect就在函数内部, 输出的信息没法传给别的函数.

pure function: 不依赖其他代码的side effect, 也不输出自己的side effect. 每次同样的输入, 都会产生同样的输出. 一个纯函数如果在某一个地方正常, 那么它在所有地方都正常.而其他函数可能依赖的环境不同.

在机器朝向纯数学发展的路上时, 纯函数肯定拥有更严谨有效的解决问题的能力, 但... 机器并不是纯粹的理想产物,它被现实一代代所创造优化, 所以用面向机器的代码在目前往往更高效, 而纯函数作为高级抽象, 耗费更多的资源.

Exercise

min()函数
let min = (x,y) => x>y?y:x;
复制代码
recusion isEven()函数

基本:

  1. 0是偶数
  2. 1是奇数 演绎: N的奇偶性和N-2是一样的 终止条件: n=0或n=1;
let isEven = (x) => {
  let result;
  switch(x) {
    case 0:
      result = false;
      break;
    case 1:
      result = true;
      break;
    default:
      result = isEven(x>0?x-2:x+2);
      break; 
  }
  return result;
};
console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??
// 这个多次判断正负的语句可以提前置于外部, 讲这个函数封闭为闭包访问静态变量, 使判断正负只运行一次.
复制代码
B counting
    let countChar = (string, char) => {
      let cnt = 0;
      for(let i=0; iif(string[i] === char)cnt++;
      }
      return cnt;
    }
    console.log(countChar("HelHsdfklkjlkhloHi", "H"))
复制代码

转载于:https://juejin.im/post/5bebdea6518825604e0e3f27

你可能感兴趣的:(javascript,java,c/c++,ViewUI)