[{"content":"📕 转载自 emacs lisp 简明教程 - 水木社区 emacs 版\n这是 叶文彬(水木 id: happierbee) 写的一份 emacs lisp 的教程,深入浅出,非常适合初学者。文档的 tex 代码及 pdf 文档可在* 此处下载* 。\nemacs 的高手不能不会 elisp。但是对于很多人来说 elisp 学习是一个痛苦的历程,至少我是有这样一段经历。因此,我写了这一系列文章,希望能为后来者提供一点捷径。\n一个 hello world 例子 自从 k\u0026amp;r 以来,hello world 程序历来都是程序语言教程的第一个例子。我也用一个 hello world 的例子来演示 emacs 里执行 elisp 的环境。下面就是这个语句:\n(message \u0026#34;hello world\u0026#34;) 前面我没有说这个一个程序,这是因为,elisp 不好作为可执行方式来运行(当然也不是不可能),所有的 elisp 都是运行在 emacs 这个环境下。\n首先切换到 *scratch* 缓冲区里,如果当前模式不是 lisp-interaction-mode,用 m-x lisp-interaction-mode 先转换到 lisp-interaction-mode。然后输入前面这一行语句。在行尾右括号后,按 c-j 键。如果 minibuffer 里显示 hello world,光标前一行也显示 \u0026quot;hello world\u0026quot;,那说明你的操作没有问题。我们就可以开始 elisp 学习之旅了。\n注:elisp 里的一个完整表达式,除了简单数据类型(如数字,向量),都是用括号括起来,称为一个 s-表达式。让 elisp 解释器执行一个 s-表达式除了前一种方法之外,还可以用 c-x c-e。它们的区别是,c-x c-e 是一个全局按键绑定,几乎可以在所有地方都能用。它会将运行返回值显示在 minibuffer 里。这里需要强调一个概念是返回值和作用是不同的。比如前面 message 函数它的作用是在 minibuffer 里显示一个字符串,但是它的返回值是 \u0026quot;hello world\u0026quot; 字符串。\n基础知识 这一节介绍一下 elisp 编程中一些最基本的概念,比如如何定义函数,程序的控制结构,变量的使用和作用域等等。\n函数和变量 elisp 中定义一个函数是用这样的形式:\n(defun function-name (arguments-list)\r\u0026#34;document string\u0026#34;\rbody) 比如:\n(defun hello-world (name) \u0026#34;say hello to user whose name is name.\u0026#34; (message \u0026#34;hello, %s\u0026#34; name)) 其中函数的文档字符串是可以省略的。但是建议为你的函数(除了最简单,不作为接口的)都加上文档字符串。这样将来别人使用你的扩展或者别人阅读你的代码或者自己进行维护都提供很大的方便。\n在 emacs 里,当光标处于一个函数名上时,可以用 c-h f 查看这个函数的文档。比如前面这个函数,在 *help* 缓冲区里的文档是:\nhello-world is a lisp function.\r(hello-world name)\rsay hello to user whose name is name. 如果你的函数是在文件中定义的。这个文档里还会给出一个链接能跳到定义的地方。\n要运行一个函数,最一般的方式是:\n(function-name arguments-list) 比如前面这个函数:\n(hello-world \u0026#34;emacser\u0026#34;) ; =\u0026gt; \u0026#34;hello, emacser\u0026#34; 每个函数都有一个返回值。这个返回值一般是函数定义里的最后一个表达式的值。\nelisp 里的变量使用无需象 c 语言那样需要声明,你可以用 setq 直接对一个变量赋值。\n(setq foo \u0026#34;i\u0026#39;m foo\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; (message foo) ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; 和函数一样,你可以用 c-h v 查看一个变量的文档。比如当光标在 foo 上时用 c-h v 时,文档是这样的:\nfoo\u0026#39;s value is \u0026#34;i\u0026#39;m foo\u0026#34;\rdocumentation:\rnot documented as a variable. 有一个特殊表达式(special form)defvar,它可以声明一个变量,一般的形式是:\n(defvar variable-name value\r\u0026#34;document string\u0026#34;) 它与 setq 所不同的是,如果变量在声明之前,这个变量已经有一个值的话, 用 defvar 声明的变量值不会改变成声明的那个值 。另一个区别是 defvar 可以为变量提供文档字符串,当变量是在文件中定义的话,c-h v 后能给出变量定义的位置。比如:\n(defvar foo \u0026#34;did i have a value?\u0026#34; \u0026#34;a demo variable\u0026#34;) ; =\u0026gt; foo foo ; =\u0026gt; \u0026#34;i\u0026#39;m foo\u0026#34; (defvar bar \u0026#34;i\u0026#39;m bar\u0026#34; \u0026#34;a demo variable named \\\u0026#34;bar\\\u0026#34;\u0026#34;) ; =\u0026gt; bar bar ; =\u0026gt; \u0026#34;i\u0026#39;m bar\u0026#34; 用 c-h v 查看 foo 的文档,可以看到它已经变成:\nfoo\u0026#39;s value is \u0026#34;i\u0026#39;m foo\u0026#34;\rdocumentation:\ra demo variable 由于 elisp 中函数是全局的,变量也很容易成为全局变量(因为全局变量和局部变量的赋值都是使用 setq 函数),名字不互相冲突是很关键的。所以除了为你的函数和变量选择一个合适的前缀之外,用 c-h f 和 c-h v 查看一下函数名和变量名有没有已经被使用过是很关键的。\n局部作用域的变量 如果没有局部作用域的变量,都使用全局变量,函数会相当难写。elisp 里可以用 let 和 let* 进行局部变量的绑定。let 使用的形式是:\n(let (bindings)\rbody) bingdings 可以是 (var value) 这样对 var 赋初始值的形式,或者用 var 声明一个初始值为 nil 的变量。比如:\n(defun circle-area (radix) (let ((pi 3.1415926) area) (setq area (* pi radix radix)) (message \u0026#34;直径为 %.2f 的圆面积是 %.2f\u0026#34; radix area))) (circle-area 3) c-h v 查看 area 和 pi 应该没有这两个变量。\nlet* 和 let 的使用形式完全相同,唯一的区别是在 let* 声明中就能使用前面声明的变量,比如:\n(defun circle-area (radix) (let* ((pi 3.1415926) (area (* pi radix radix))) (message \u0026#34;直径为 %.2f 的圆面积是 %.2f\u0026#34; radix area))) lambda 表达式 可能你久闻 lambda 表达式的大名了。其实依我的理解,lambda 表达式相当于其它语言中的匿名函数。比如 perl 里的匿名函数。它的形式和 defun 是完全一样的:\n(lambda (arguments-list)\r\u0026#34;documentation string\u0026#34;\rbody) 调用 lambda 方法如下:\n(funcall (lambda (name) (message \u0026#34;hello, %s!\u0026#34; name)) \u0026#34;emacser\u0026#34;) 你也可以把 lambda 表达式赋值给一个变量,然后用 funcall 调用:\n(setq foo (lambda (name) (message \u0026#34;hello, %s!\u0026#34; name))) (funcall foo \u0026#34;emacser\u0026#34;) ; =\u0026gt; \u0026#34;hello, emacser!\u0026#34; lambda 表达式最常用的是作为参数传递给其它函数,比如 mapc。\n控制结构 顺序执行 一般来说程序都是按表达式顺序依次执行的。这在 defun 等特殊环境中是自动进行的。但是一般情况下都不是这样的。比如你无法用 eval-last-sexp 同时执行两个表达式,在 if 表达式中的条件为真时执行的部分也只能运行一个表达式。这时就需要用 progn 这个特殊表达式。它的使用形式如下:\n(progn a b c ...) 它的作用就是让表达式 a, b, c 顺序执行。比如:\n(progn (setq foo 3) (message \u0026#34;square of %d is %d\u0026#34; foo (* foo foo))) 条件判断 elisp 有两个最基本的条件判断表达式 if 和 cond。使用形式分别如下:\n(if condition\rthen\relse)\r(cond (case1 do-when-case1)\r(case2 do-when-case2)\r...\r(t do-when-none-meet)) 使用的例子如下:\n(defun my-max (a b) (if (\u0026gt; a b) a b)) (my-max 3 4) ; =\u0026gt; 4 (defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; =\u0026gt; 55 还有两个宏 when 和 unless,从它们的名字也就能知道它们是作什么用的。使用这两个宏的好处是使代码可读性提高,when 能省去 if 里的 progn 结构,unless 省去条件为真子句需要的的 nil 表达式。\n循环 循环使用的是 while 表达式。它的形式是:\n(while condition\rbody) 比如:\n(defun factorial (n) (let ((res 1)) (while (\u0026gt; n 1) (setq res (* res n) n (- n 1))) res)) (factorial 10) ; =\u0026gt; 3628800 逻辑运算 条件的逻辑运算和其它语言都是很类似的,使用 and、or、not。and 和 or 也同样具有短路性质。很多人喜欢在表达式短时,用 and 代替 when,or 代替 unless。当然这时一般不关心它们的返回值,而是在于表达式其它子句的副作用。比如 or 经常用于设置函数的缺省值,而 and 常用于参数检查:\n(defun hello-world (\u0026amp;optional name) (or name (setq name \u0026#34;emacser\u0026#34;)) (message \u0026#34;hello, %s\u0026#34; name)) ; =\u0026gt; hello-world (hello-world) ; =\u0026gt; \u0026#34;hello, emacser\u0026#34; (hello-world \u0026#34;ye\u0026#34;) ; =\u0026gt; \u0026#34;hello, ye\u0026#34; (defun square-number-p (n) (and (\u0026gt;= n 0) (= (/ n (sqrt n)) (sqrt n)))) (square-number-p -1) ; =\u0026gt; nil (square-number-p 25) ; =\u0026gt; t 函数列表 (defun name arglist [docstring] body...) (defvar symbol \u0026amp;optional initvalue docstring) (setq sym val sym val ...) (let varlist body...) (let* varlist body...) (lambda args [docstring] [interactive] body) (progn body ...) (if cond then else...) (cond clauses...) (when cond body ...) (unless cond body ...) (when cond body ...) (or conditions ...) (and conditions ...) (not object) 基本数据类型之一 \u0026ndash; 数字 elisp 里的对象都是有类型的,而且每一个对象它们知道自己是什么类型。 你得到一个变量名之后可以用一系列检测方法来测试这个变量是什么类型(好像没有什么方法来让它说出自己是什么类型的)。内建的 emacs 数据类型称为 primitive types,包括整数、浮点数、cons、符号 (symbol)、字符串、向量 (vector)、散列表 (hash-table)、subr(内建函数,比如 cons, if, and 之类)、byte-code function,和其它特殊类型,例如缓冲区(buffer)。\n在开始前有必要先了解一下读入语法和输出形式。所谓读入语法是让 elisp 解释器明白输入字符所代表的对象,你不可能让 elisp 读入 .#@!? 这样奇怪的东西还能好好工作吧(perl 好像经常要受这样的折磨:))。简单的来说,一种数据类型有(也可能没有,比如散列表)对应的规则来让解释器产生这种数据类型,比如 123 产生整数 123, (a . b) 产生一个 cons。所谓输出形式是解释器用产生一个字符串来表示一个数据对象。比如整数 123 的输出形式就是 123,cons cell (a . b) 的输出形式是 (a . b)。与读入语法不同的是,数据对象都有输出形式。比如散列表的输出可能是这样的:\n#\u0026lt;hash-table \u0026#39;eql nil 0/65 0xa7344c8\u0026gt; 通常一个对象的数据对象的输出形式和它的读入形式都是相同的。现在就先从简单的数据类型──数字开始吧。\nemacs 的数字分为整数和浮点数(和 c 比没有双精度数 double)。1, 1.,+1, -1, 536870913, 0, -0 这些都是整数。整数的范围是和机器是有关的,一般来最小范围是在 -268435456 to 268435455(29 位,-2**28 ~ 2**28-1)。可以从 most-positive-fixnum 和 most-negative-fixnum 两个变量得到整数的范围。\n你可以用多种进制来输入一个整数。比如:\n#b101100 =\u0026gt; 44 ; 二进制 #o54 =\u0026gt; 44 ; 八进制 #x2c =\u0026gt; 44 ; 十六进制 最神奇的是你可以用 2 到 36 之间任意一个数作为基数,比如:\n#24r1k =\u0026gt; 44 ; 二十四进制 之所以最大是 36,是因为只有 0-9 和 a-z 36 个字符来表示数字。但是我想基本上不会有人会用到 emacs 的这个特性。\n1500.0, 15e2, 15.0e2, 1.5e3, 和 .15e4 都可以用来表示一个浮点数 1500.。遵循 ieee 标准,elisp 也有一个特殊类型的值称为 nan (not-a-number)。你可以用 (/ 0.0 0.0) 产生这个数。\n测试函数 整数类型测试函数是 integerp,浮点数类型测试函数是 floatp。数字类型测试用 numberp。你可以分别运行这几个例子来试验一下:\n(integerp 1.) ; =\u0026gt; t (integerp 1.0) ; =\u0026gt; nil (floatp 1.) ; =\u0026gt; nil (floatp -0.0e+nan) ; =\u0026gt; t (numberp 1) ; =\u0026gt; t 还提供一些特殊测试,比如测试是否是零的 zerop ,还有非负整数测试的 wholenump 。\n注:elisp 测试函数一般都是用 p 来结尾,p 是 predicate 的第一个字母。如果函数名是一个单词,通常只是在这个单词后加一个 p,如果是多个单词,一般是加 -p。\n数的比较 常用的比较操作符号是我们在其它言中都很熟悉的,比如 \u0026lt;, \u0026gt;, \u0026gt;=, \u0026lt;=,不一样的是,由于赋值是使用 set 函数,所以 = 不再是一个赋值运算符了,而是测试数字相等符号。和其它语言类似, 对于浮点数的相等测试都是不可靠的 。比如:\n(setq foo (- (+ 1.0 1.0e-3) 1.0)) ; =\u0026gt; 0.0009999999999998899 (setq bar 1.0e-3) ; =\u0026gt; 0.001 (= foo bar) ; =\u0026gt; nil 所以一定要确定两个浮点数是否相同,是要在一定误差内进行比较。这里给出一个函数:\n(defvar fuzz-factor 1.0e-6) (defun approx-equal (x y) (or (and (= x 0) (= y 0)) (\u0026lt; (/ (abs (- x y)) (max (abs x) (abs y))) fuzz-factor))) (approx-equal foo bar) ; =\u0026gt; t 还有一个测试数字是否相等的函数 eql ,这是函数不仅测试数字的值是否相等,还测试数字类型是否一致,比如:\n(= 1.0 1) ; =\u0026gt; t (eql 1.0 1) ; =\u0026gt; nil elisp 没有 +=, -=, /=, *= 这样的命令式语言里常见符号,如果你想实现类似功能的语句,只能用赋值函数 setq 来实现了。 /= 符号被用来作为不等于的测试了。\n数的转换 整数向浮点数转换是通过 float 函数进行的。而浮点数转换成整数有这样几个函数:\ntruncate 转换成靠近 0 的整数 floor 转换成最接近的不比本身大的整数 ceiling 转换成最接近的不比本身小的整数 round 四舍五入后的整数,换句话说和它的差绝对值最小的整数 很晕是吧。自己用 1.2, 1.7, -1.2, -1.7 对这四个函数操作一遍就知道区别了(可以直接看 info。按键顺序是 c-h i m elisp ret m numeric conversions ret。以后简写成 info elisp - numeric conversions)。\n这里提一个问题,浮点数的范围是无穷大的,而整数是有范围的,如果用前面的函数转换 1e20 成一个整数会出现什么情况呢?试试就知道了。\n数的运算 四则运算没有什么好说的,就是 + - * /。值得注意的是,和 c 语言类似,如果参数都是整数,作除法时要记住 (/ 5 6) 是会等于 0 的。如果参数中有浮点数,整数会自动转换成浮点数进行运算,所以 (/ 5 6.0) 的值才会是 5/6。\n没有 ++ 和 -- 操作了,类似的两个函数是 1+ 和 1- 。可以用 setq 赋值来代替 ++ 和 --:\n(setq foo 10) ; =\u0026gt; 10 (setq foo (1+ foo)) ; =\u0026gt; 11 (setq foo (1- foo)) ; =\u0026gt; 10 注:可能有人看过有 incf 和 decf 两个实现 ++ 和 -- 操作。这两个宏是可以用的。这两个宏是 common lisp 里的,emacs 有模拟的 common lisp 的库 cl。但是 rms 认为最好不要使用这个库。但是你可以在你的 elisp 包中使用这两个宏,只要在文件头写上:\n(eval-when-compile (require \u0026#39;cl)) 由于 incf 和 decf 是两个宏,所以这样写不会在运行里导入 cl 库。有点离题是,总之一句话,教主说不好的东西,我们最好不要用它。其它无所谓,只可惜了两个我最常用的函数 remove-if 和 remove-if-not 。不过如果你也用 emms 的话,可以在 emms-compat 里找到这两个函数的替代品。\nabs 取数的绝对值。\n有两个取整的函数,一个是符号 % ,一个是函数 mod 。这两个函数有什么差别呢?一是 % 的第一个参数必须是整数,而 mod 的第一个参数可以是整数也可以是浮点数。二是即使对相同的参数,两个函数也不一定有相同的返回值:\n(+ (% dividend divisor)\r(* (/ dividend divisor) divisor)) 和 dividend 是相同的。而:\n(+ (mod dividend divisor)\r(* (floor dividend divisor) divisor)) 和 dividend 是相同的。\n三角运算有函数: sin, cos, tan, asin, acos, atan。开方函数是 sqrt。\nexp 是以 e 为底的指数运算,expt 可以指定底数的指数运算。log 默认底数是 e,但是也可以指定底数。log10 就是 (log x 10)。logb 是以 2 为底数运算,但是返回的是一个整数。这个函数是用来计算数的位。\nrandom 可以产生随机数。可以用 (random t) 来产生一个新种子。虽然 emacs 每次启动后调用 random 总是产生相同的随机数,但是运行过程中,你不知道调用了多少次,所以使用时还是不需要再调用一次 (random t) 来产生新的种子。\n位运算这样高级的操作我就不说了,自己看 info elisp - bitwise operations on integers 吧。\n函数列表 ;; 测试函数 (integerp object) (floatp object) (numberp object) (zerop number) (wholenump object) ;; 比较函数 (\u0026gt; num1 num2) (\u0026lt; num1 num2) (\u0026gt;= num1 num2) (\u0026lt;= num1 num2) (= num1 num2) (eql obj1 obj2) (/= num1 num2) ;; 转换函数 (float arg) (truncate arg \u0026amp;optional divisor) (floor arg \u0026amp;optional divisor) (ceiling arg \u0026amp;optional divisor) (round arg \u0026amp;optional divisor) ;; 运算 (+ \u0026amp;rest numbers-or-markers) (- \u0026amp;optional number-or-marker \u0026amp;rest more-numbers-or-markers) (* \u0026amp;rest numbers-or-markers) (/ dividend divisor \u0026amp;rest divisors) (1+ number) (1- number) (abs arg) (% x y) (mod x y) (sin arg) (cos arg) (tan arg) (asin arg) (acos arg) (atan y \u0026amp;optional x) (sqrt arg) (exp arg) (expt arg1 arg2) (log arg \u0026amp;optional base) (log10 arg) (logb arg) ;; 随机数 (random \u0026amp;optional n) 变量列表 most-positive-fixnum\rmost-negative-fixnum 基本数据类型之二 \u0026ndash; 字符和字符串 在 emacs 里字符串是有序的字符数组。和 c 语言的字符串数组不同,emacs 的字符串可以容纳任何字符,包括 \\0:\n(setq foo \u0026#34;abc\\000abc\u0026#34;) ; =\u0026gt; \u0026#34;abc^@abc\u0026#34; 关于字符串有很多高级的属性,例如字符串的表示有单字节和多字节类型,字符串可以有文本属性(text property)等等。但是对于刚接触字符串,还是先学一些基本操作吧。\n首先 构成字符串的字符其实就是一个整数 。一个字符 \u0026lsquo;a\u0026rsquo; 就是一个整数 65。但是目前字符串中的字符被限制在 0-524287 之间。字符的读入语法是在字符前加上一个问号,比如 ?a 代表字符 \u0026lsquo;a\u0026rsquo;。\n?a ; =\u0026gt; 65 ?a ; =\u0026gt; 97 对于标点来说,也可以用同样的语法,但是最好在前面加上转义字符 \\ ,因为有些标点会有岐义,比如 ?\\(。 \\ 必须用 ?\\ 表示。控制字符,退格、制表符,换行符,垂直制表符,换页符,空格,回车,删除和 escape 表示为 ?\\a, ?\\b, ?\\t, ?\\n, ?\\v, ?\\f, ?\\s, ?\\r, ?\\d, 和 ?\\e。对于没有特殊意义的字符,加上转义字符 \\ 是没有副作用的,比如 ?\\+ 和 ?+ 是完全一样的。所以标点还是都用转义字符来表示吧。\n?\\a =\u0026gt; 7 ; control-g, `c-g\u0026#39; ?\\b =\u0026gt; 8 ; backspace, \u0026lt;bs\u0026gt;, `c-h\u0026#39; ?\\t =\u0026gt; 9 ; tab, \u0026lt;tab\u0026gt;, `c-i\u0026#39; ?\\n =\u0026gt; 10 ; newline, `c-j\u0026#39; ?\\v =\u0026gt; 11 ; vertical tab, `c-k\u0026#39; ?\\f =\u0026gt; 12 ; formfeed character, `c-l\u0026#39; ?\\r =\u0026gt; 13 ; carriage return, \u0026lt;ret\u0026gt;, `c-m\u0026#39; ?\\e =\u0026gt; 27 ; escape character, \u0026lt;esc\u0026gt;, `c-[\u0026#39; ?\\s =\u0026gt; 32 ; space character, \u0026lt;spc\u0026gt; ?\\\\ =\u0026gt; 92 ; backslash character, `\\\u0026#39; ?\\d =\u0026gt; 127 ; delete character, \u0026lt;del\u0026gt; 控制字符可以有多种表示方式,比如 c-i,这些都是对的:\n?\\^i ?\\^i ?\\c-i ?\\c-i 它们都对应数字 9。\nmeta 字符是用 修饰键(通常就是 alt 键)输入的字符。之所以称为修饰键,是因为这样输入的字符就是在其修饰字符的第 27 位由 0 变成 1 而成,也就是如下操作:\n(logior (lsh 1 27) ?a) ; =\u0026gt; 134217793 ?\\m-a ; =\u0026gt; 134217793 你可以用 \\m- 代表 meta 键,加上修饰的字符就是新生成的字符。比如:?\\m-a, ?\\m-\\c-b. 后面这个也可以写成 ?\\c-\\m-b。\n如果你还记得前面说过字符串里的字符不能超过 524287 的话,这就可以看出字符串是不能放下一个 meta 字符的。所以按键序列在这时只能用 vector 来储存。\n其它的修饰键也是类似的。emacs 用 2**25 位来表示 shift 键,2**24 对应 hyper,2**23 对应 super,2**22 对应 alt。\n测试函数 字符串测试使用 stringp ,没有 charp,因为字符就是整数。 string-or-null-p 当对象是一个字符或 nil 时返回 t。 char-or-string-p 测试是否是字符串或者字符类型。比较头疼的是 emacs 没有测试字符串是否为空的函数。这是我用的这个测试函数,使用前要测试字符串是否为 nil:\n(defun string-emptyp (str) (not (string\u0026lt; \u0026#34;\u0026#34; str))) 构造函数 产生一个字符串可以用 make-string 。这样生成的字符串包含的字符都是一样的。要生成不同的字符串可以用 string 函数。\n(make-string 5 ?x) ; =\u0026gt; \u0026#34;xxxxx\u0026#34; (string ?a ?b ?c) ; =\u0026gt; \u0026#34;abc\u0026#34; 在已有的字符串生成新的字符串的方法有 substring, concat 。 substring 的后两个参数是起点和终点的位置。如果终点越界或者终点比起点小都会产生一个错误。这个在使用 substring 时要特别小心。\n(substring \u0026#34;0123456789\u0026#34; 3) ; =\u0026gt; \u0026#34;3456789\u0026#34; (substring \u0026#34;0123456789\u0026#34; 3 5) ; =\u0026gt; \u0026#34;34\u0026#34; (substring \u0026#34;0123456789\u0026#34; -3 -1) ; =\u0026gt; \u0026#34;78\u0026#34; concat 函数相对简单,就是把几个字符串连接起来。\n字符串比较 char-equal 可以比较两个字符是否相等。与整数比较不同,这个函数还考虑了大小写。如果 case-fold-search 变量是 t 时,这个函数的字符比较是忽略大小写的。编程时要小心,因为通常 case-fold-search 都是 t,这样如果要考虑字符的大小写时就不能用 char-equal 函数了。\n字符串比较使用 string= ,string-equal 是一个别名。\nstring\u0026lt; 是按字典序比较两个字符串, string-less 是它的别名。空字符串小于所有字符串,除了空字符串。前面 string-emptyp 就是用这个特性。当然直接用 length 检测字符串长度应该也可以,还可以省去检测字符串是否为空。没有 string\u0026gt; 函数。\n转换函数 字符转换成字符串可以用 char-to-string 函数,字符串转换成字符可以用 string-to-char ,当然只是返回字符串的第一个字符。\n数字和字符串之间的转换可以用 number-to-string 和 string-to-number 。其中 string-to-number 可以设置字符串的进制,可以从 2 到 16。 number-to-string 只能转换成 10 进制的数字。如果要输出八进制或者十六进制,可以用 format 函数:\n(string-to-number \u0026#34;256\u0026#34;) ; =\u0026gt; 256 (number-to-string 256) ; =\u0026gt; \u0026#34;256\u0026#34; (format \u0026#34;%#o\u0026#34; 256) ; =\u0026gt; \u0026#34;0400\u0026#34; (format \u0026#34;%#x\u0026#34; 256) ; =\u0026gt; \u0026#34;0x100\u0026#34; 如果要输出成二进制,好像没有现成的函数了。calculator 库倒是可以,这是我写的函数:\n(defun number-to-bin-string (number) (require \u0026#39;calculator) (let ((calculator-output-radix \u0026#39;bin) (calculator-radix-grouping-mode nil)) (calculator-number-to-string number))) (number-to-bin-string 256) ; =\u0026gt; \u0026#34;100000000\u0026#34; 其它数据类型现在还没有学到,不过可以先了解一下吧。 concat 可以把一个字符构成的列表或者向量转换成字符串, vconcat 可以把一个字符串转换成一个向量, append 可以把一个字符串转换成一个列表。\n(concat \u0026#39;(?a ?b ?c ?d ?e)) ; =\u0026gt; \u0026#34;abcde\u0026#34; (concat [?a ?b ?c ?d ?e]) ; =\u0026gt; \u0026#34;abcde\u0026#34; (vconcat \u0026#34;abdef\u0026#34;) ; =\u0026gt; [97 98 100 101 102] (append \u0026#34;abcdef\u0026#34; nil) ; =\u0026gt; (97 98 99 100 101 102) 大小写转换使用的是 downcase 和 upcase 两个函数。这两个函数的参数既可以字符串,也可以是字符。capitalize 可以使字符串中单词的第一个字符大写,其它字符小写。 upcase-initials 只使第一个单词的第一个字符大写,其它字符小写。 这两个函数的参数如果是一个字符,那么只让这个字符大写。比如:\n(downcase \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (downcase ?x) ; =\u0026gt; 120 (upcase \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (upcase ?x) ; =\u0026gt; 88 (capitalize \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; (upcase-initials \u0026#34;the cat in the hat\u0026#34;) ; =\u0026gt; \u0026#34;the cat in the hat\u0026#34; = 💡 这里 upcase-initials 的作用应该是使单词的第一个字符大写,其它字符大小写保持不变。\n格式化字符串 format 类似于 c 语言里的 printf 可以实现对象的字符串化。数字的格式化和 printf 的参数差不多,值得一提的是 \u0026quot;%s\u0026quot; 这个格式化形式,它可以把对象的输出形式转换成字符串,这在调试时是很有用的。\n查找和替换 字符串查找的核心函数是 string-match 。这个函数可以 从指定的位置对字符串进行正则表达式匹配 ,如果匹配成功,则返回匹配的起点,如:\n(string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34;) ; =\u0026gt; 3 (string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34; 10) ; =\u0026gt; 13 注意 string-match 的参数是一个 regexp。 emacs 好象没有内建的查找子串的函数。如果你想把 string-match 作为一个查找子串的函数,可以先用 regexp-quote 函数先处理一下子串。比如:\n(string-match \u0026#34;2*\u0026#34; \u0026#34;232*3=696\u0026#34;) ; =\u0026gt; 0 (string-match (regexp-quote \u0026#34;2*\u0026#34;) \u0026#34;232*3=696\u0026#34;) ; =\u0026gt; 2 事实上, string-match 不只是查找字符串,它更重要的功能是捕捉匹配的字符串。如果你对正则表达式不了解,可能需要先找一本书,先了解一下什么是正则表达式。 string-match 在查找的同时,还会记录下每个要捕捉的字符串的位置。这个位置可以在匹配后用 match-data、 match-beginning 和 match-end 等函数来获得。先看一下例子:\n(progn (string-match \u0026#34;3\\\\(4\\\\)\u0026#34; \u0026#34;01234567890123456789\u0026#34;) (match-data)) ; =\u0026gt; (3 5 4 5) 最后返回这个数字是什么意思呢?正则表达式捕捉的字符串按括号的顺序对应一个序号,整个模式对应序号 0,第一个括号对应序号 1,第二个括号对应序号 2,以此类推。所以 \u0026ldquo;3(4)\u0026rdquo; 这个正则表达式中有序号 0 和 1,最后 match-data 返回的一系列数字对应的分别是要捕捉字符串的起点和终点位置,也就是说子串 \u0026ldquo;34\u0026rdquo; 起点从位置 3 开始,到位置 5 结束,而捕捉的字符串 \u0026ldquo;4\u0026rdquo; 的起点是从 4 开始,到 5 结束。这些位置可以用 match-beginning 和 match-end 函数用对应的序号得到。要注意的是,起点位置是捕捉字符串的第一个字符的位置,而终点位置不是捕捉的字符串最后一个字符的位置,而是下一个字符的位置。这个性质对于循环是很方便的。比如要查找上面这个字符串中所有 34 出现的位置:\n(let ((start 0)) (while (string-match \u0026#34;34\u0026#34; \u0026#34;01234567890123456789\u0026#34; start) (princ (format \u0026#34;find at %d\\n\u0026#34; (match-beginning 0))) (setq start (match-end 0)))) 查找会了,就要学习替换了。替换使用的函数是 replace-match 。这个函数既可以用于字符串的替换,也可以用于缓冲区的文本替换。对于字符串的替换, replace-match 只是按给定的序号把字符串中的那一部分用提供的字符串替换了而已:\n(let ((str \u0026#34;01234567890123456789\u0026#34;)) (string-match \u0026#34;34\u0026#34; str) (princ (replace-match \u0026#34;x\u0026#34; nil nil str 0)) (princ \u0026#34;\\n\u0026#34;) (princ str)) 可以看出 replace-match 返回的字符串是替换后的新字符串,原字符串被没有改变。\n如果你想挑战一下,想想怎样把上面这个字符串中所有的 34 都替换掉?如果想就使用同一个字符串来存储,可能对于固定的字符串,这个还容易一些,如果不是的话,就要花一些脑筋了,因为替换之后,新的字符串下一个搜索起点的位置就不能用 (match-end 0) 给出来的位置了,而是要扣除替换的字符串和被替换的字符串长度的差值。\nemacs 对字符串的替换有一个函数 replace-regexp-in-string 。这个函数的实现方法是把每次匹配部分之前的子串收集起来,最后再把所有字符串连接起来。\n单字符的替换有 subst-char-in-string 函数。但是 emacs 没有类似 perl 函数或者程序 tr 那样进行字符替换的函数。只能自己建表进行循环操作了。\n函数列表 ;; 测试函数 (stringp object) (string-or-null-p object) (char-or-string-p object) ;; 构建函数 (make-string length init) (string \u0026amp;rest characters) (substring string from \u0026amp;optional to) (concat \u0026amp;rest sequences) ;; 比较函数 (char-equal c1 c2) (string= s1 s2) (string-equal s1 s2) (string\u0026lt; s1 s2) ;; 转换函数 (char-to-string char) (string-to-char string) (number-to-string number) (string-to-number string \u0026amp;optional base) (downcase obj) (upcase obj) (capitalize obj) (upcase-initials obj) (format string \u0026amp;rest objects) ;; 查找与替换 (string-match regexp string \u0026amp;optional start) (replace-match newtext \u0026amp;optional fixedcase literal string subexp) (replace-regexp-in-string regexp rep string \u0026amp;optional fixedcase literal subexp start) (subst-char-in-string fromchar tochar string \u0026amp;optional inplace) 基本数据类型之三 \u0026ndash; cons cell 和列表 如果从概念上来说,cons cell 其实非常简单的,就是两个有顺序的元素。第一个叫 car,第二个就 cdr。car 和 cdr 名字来自于 lisp。它最初在 ibm 704 机器上的实现。在这种机器有一种取址模式,使人可以访问一个存储地址中的“地址(address)”部分和“减量(decrement)”部分。car 指令用于取出地址部分,表示 (contents of address part of register),cdr 指令用于取出地址的减量部分 (contents of the decrement part of register)。cons cell 也就是 construction of cells。car 函数用于取得 cons cell 的 car 部分,cdr 取得 cons cell 的 cdr 部分。cons cell 如此简单,但是它却能衍生出许多高级的数据结构,比如链表,树,关联表等等。\ncons cell 的读入语法是用 . 分开两个部分,比如:\n\u0026#39;(1 . 2) ; =\u0026gt; (1 . 2) \u0026#39;(?a . 1) ; =\u0026gt; (97 . 1) \u0026#39;(1 . \u0026#34;a\u0026#34;) ; =\u0026gt; (1 . \u0026#34;a\u0026#34;) \u0026#39;(1 . nil) ; =\u0026gt; (1) \u0026#39;(nil . nil) ; =\u0026gt; (nil) 注意到前面的表达式中都有一个 ' 号,这是什么意思呢?其实理解了 eval-last-sexp 的作用就能明白了。 eval-last-sexp 其实包含了两个步骤, 一是读入前一个 s-表达式,二是对读入的 s-表达式求值 。这样如果读入的 s-表达式是一个 cons cell 的话,求值时会把这个 cons cell 的第一个元素作为一个函数来调用。而事实上,前面这些例子的第一个元素都不是一个函数,这样就会产生一个错误 invalid-function。之所以前面没有遇到这个问题,那是因为前面数字和字符串是一类特殊的 s-表达式,它们求值后和求值前是不变,称为 自求值表达式 (self-evaluating form)。 ' 号其实是一个特殊的函数 quote ,它的作用是 将它的参数返回而不作求值 。 '(1 . 2) 等价于 (quote (1 . 2))。为了证明 cons cell 的读入语法确实就是它的输出形式,可以看下面这个语句:\n(read \u0026#34;(1 . 2)\u0026#34;) ; =\u0026gt; (1 . 2) 列表包括了 cons cell。但是列表中有一个特殊的元素 - 空表 nil 。\nnil ; =\u0026gt; nil \u0026#39;() ; =\u0026gt; nil 空表不是一个 cons cell,因为它没有 car 和 cdr 两个部分,事实上空表里没有任何内容。但是为了编程的方便,可以认为 nil 的 car 和 cdr 都是 nil:\n(car nil) ; =\u0026gt; nil (cdr nil) ; =\u0026gt; nil 按列表最后一个 cons cell 的 cdr 部分的类型分,可以把列表分为三类。如果它是 nil 的话,这个列表也称为“真列表”(true list)。如果既不是 nil 也不是一个 cons cell,则这个列表称为“点列表”(dotted list)。还有一种可能,它指向列表中之前的一个 cons cell,则称为环形列表 (circular list)。这里分别给出一个例子:\n\u0026#39;(1 2 3) ; =\u0026gt; (1 2 3) \u0026#39;(1 2 . 3) ; =\u0026gt; (1 2 . 3) \u0026#39;(1 . #1=(2 3 . #1#)) ; =\u0026gt; (1 2 3 . #1) 从这个例子可以看出前两种列表的读入语法和输出形式都是相同的,而环形列表的读入语法是很古怪的,输出形式不能作为环形列表的读入形式。\n如果把真列表最后一个 cons cell 的 nil 省略不写,也就是 (1 . nil) 简写成 (1) ,把 ( obj1 . ( obj2 . list)) 简写成 (obj1 obj2 . list) ,那么列表最后可以写成一个用括号括起的元素列表:\n\u0026#39;(1 . (2 . (3 . nil))) ; =\u0026gt; (1 2 3) 尽管这样写是清爽多了,但是,我觉得看一个列表时还是在脑子里反映的前面的形式,这样在和复杂的 cons cell 打交道时就不会搞不清楚这个 cons cell 的 cdr 是一个列表呢,还是一个元素或者是嵌套的列表。\n测试函数 测试一个对象是否是 cons cell 用 consp ,是否是列表用 listp 。\n(consp \u0026#39;(1 . 2)) ; =\u0026gt; t (consp \u0026#39;(1 . (2 . nil))) ; =\u0026gt; t (consp nil) ; =\u0026gt; nil (listp \u0026#39;(1 . 2)) ; =\u0026gt; t (listp \u0026#39;(1 . (2 . nil))) ; =\u0026gt; t (listp nil) ; =\u0026gt; t 没有内建的方法测试一个列表是不是一个真列表。通常如果一个函数需要一个真列表作为参数,都是在运行时发出错误,而不是进行参数检查,因为检查一个列表是真列表的代价比较高。\n测试一个对象是否是 nil 用 null 函数。只有当对象是空表时,null 才返回空值。\n构造函数 生成一个 cons cell 可以用 cons 函数。比如:\n(cons 1 2) ; =\u0026gt; (1 . 2) (cons 1 \u0026#39;()) ; =\u0026gt; (1) 也是在列表前面增加元素的方法。比如:\n(setq foo \u0026#39;(a b)) ; =\u0026gt; (a b) (cons \u0026#39;x foo) ; =\u0026gt; (x a b) 值得注意的是前面这个例子的 foo 值并没有改变。事实上有一个宏 push 可以加入元素的同时改变列表的值:\n(push \u0026#39;x foo) ; =\u0026gt; (x a b) foo ; =\u0026gt; (x a b) 生成一个列表的函数是 list 。比如:\n(list 1 2 3) ; =\u0026gt; (1 2 3) 可能这时你有一个疑惑,前面产生一个列表,我常用 quote (也就是 ' 符号)这个函数,它和这个 cons 和 list 函数有什么区别呢?其实区别是很明显的,quote 是把参数直接返回不进行求值,而 list 和 cons 是对参数求值后再生成一个列表或者 cons cell。看下面这个例子:\n\u0026#39;((+ 1 2) 3) ; =\u0026gt; ((+ 1 2) 3) (list (+ 1 2) 3) ; =\u0026gt; (3 3) 前一个生成的列表的 car 部分是 (+ 1 2) 这个列表,而后一个是先对 (+ 1 2) 求值得到 3 后再生成列表。\n思考题\n如果你觉得你有点明白的话,我提一个问题考考你:怎样用 list 函数构造一个 (a b c) 这样的列表呢?\n前面提到在列表前端增加元素的方法是用 cons ,在列表后端增加元素的函数是用 append 。比如:\n(append \u0026#39;(a b) \u0026#39;(c)) ; =\u0026gt; (a b c) append 的功能可以认为它是把第一个参数最后一个列表的 nil 换成第二个参数,比如前面这个例子,第一个参数写成 cons cell 表示方式是 (a . (b . nil)) ,把这个 nil 替换成 (c) 就成了 (a . (b . (c))) 。对于多个参数的情况也是一样的,依次把下一个参数替换新列表最后一个 nil 就是最后的结果了。\n(append \u0026#39;(a b) \u0026#39;(c) \u0026#39;(d)) ; =\u0026gt; (a b c d) 一般来说 append 的参数都要是列表,但是最后一个参数可以不是一个列表,这也不违背前面说的,因为 cons cell 的 cdr 部分本来就可以是任何对象:\n(append \u0026#39;(a b) \u0026#39;c) ; =\u0026gt; (a b . c) 这样得到的结果就不再是一个真列表了,如果再进行 append 操作就会产生一个错误。\n如果你写过 c 的链表类型,可能就知道如果链表只保留一个指针,那么链表只能在一端增加元素。elisp 的列表类型也是类似的,用 cons 在列表前增加元素比用 append 要快得多。\nappend 的参数不限于列表,还可以是字符串或者向量。前面字符串里已经提到可以把一个字符串转换成一个字符列表,同样可能把向量转换成一个列表:\n(append [a b] \u0026#34;cd\u0026#34; nil) ; =\u0026gt; (a b 99 100) 注意前面最后一个参数 nil 是必要的,不然你可以想象得到的结果是什么 \u0026ndash; (a b . \u0026quot;cd\u0026quot;)。\n把列表当数组用 要得到列表或者 cons cell 里元素,唯一的方法是用 car 和 cdr 函数。很容易明白,car 就是取得 cons cell 的 car 部分,cdr 函数就是取得 cons cell 的 cdr 部分。通过这两个函数,我们就能访问 cons cell 和列表中的任何元素。\n思考题\n你如果知道 elisp 的函数如果定义,并知道 if 的使用方法,不妨自己写一个函数来取得一个列表的第 n 个 cdr。\n通过使用 elisp 提供的函数,我们事实上是可以把列表当数组来用。依惯例,我们用 car 来访问列表的第一个元素,cadr 来访问第二个元素,再往后就没有这样的函数了,可以用 nth 函数来访问:\n(nth 3 \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; 3 获得列表一个区间的函数有 nthcdr、last 和 butlast。nthcdr 和 last 比较类似,它们都是返回列表后端的列表。nthcdr 函数返回第 n 个元素后的列表:\n(nthcdr 2 \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; (2 3 4 5) last 函数返回倒数 n 个长度的列表:\n(last \u0026#39;(0 1 2 3 4 5) 2) ; =\u0026gt; (4 5) butlast 和前两个函数不同,返回的除了倒数 n 个元素的列表。\n(butlast \u0026#39;(0 1 2 3 4 5) 2) ; =\u0026gt; (0 1 2 3) 思考题\n如何得到某个区间(比如从 3 到 5 之间)的列表(提示列表长度可以用 length 函数得到):\n(my-subseq \u0026#39;(0 1 2 3 4 5) 2 5) ; =\u0026gt; (2 3 4) 使用前面这几个函数访问列表是没有问题了。但是你也可以想象,链表这种数据结构是不适合随机访问的,代价比较高,如果你的代码中频繁使用这样的函数或者对一个很长的列表使用这样的函数,就应该考虑是不是应该用数组来实现。\n直到现在为止,我们用到的函数都不会修改一个已有的变量。这是函数式编程的一个特点。只用这些函数编写的代码是很容易调试的,因为你不用去考虑一个变量在执行一个代码后就改变了,不用考虑变量的引用情况等等。下面就要结束这样轻松的学习了。\n首先学习怎样修改一个 cons cell 的内容。首先 setcar 和 setcdr 可以修改一个 cons cell 的 car 部分和 cdr 部分。比如:\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (setcar foo \u0026#39;x) ; =\u0026gt; x foo ; =\u0026gt; (x b c) (setcdr foo \u0026#39;(y z)) ; =\u0026gt; (y z) foo ; =\u0026gt; (x y z) 思考题\n好像很简单是吧。我出一个比较 bt 的一个问题,下面代码运行后 foo 是什么东西呢?\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (setcdr foo foo) 现在来考虑一下,怎样像数组那样直接修改列表。使用 setcar 和 nthcdr 的组合就可以实现了:\n(setq foo \u0026#39;(1 2 3)) ; =\u0026gt; (1 2 3) (setcar foo \u0026#39;a) ; =\u0026gt; a (setcar (cdr foo) \u0026#39;b) ; =\u0026gt; b (setcar (nthcdr 2 foo) \u0026#39;c) ; =\u0026gt; c foo ; =\u0026gt; (a b c) 把列表当堆栈用 前面已经提到过可以用 push 向列表头端增加元素,在结合 pop 函数,列表就可以做为一个堆栈了。\n(setq foo nil) ; =\u0026gt; nil (push \u0026#39;a foo) ; =\u0026gt; (a) (push \u0026#39;b foo) ; =\u0026gt; (b a) (pop foo) ; =\u0026gt; b foo ; =\u0026gt; (a) 重排列表 如果一直用 push 往列表里添加元素有一个问题是这样得到的列表和加入的顺序是相反的。通常我们需要得到一个反向的列表。reverse 函数可以做到这一点:\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (reverse foo) ; =\u0026gt; (c b a) 需要注意的是使用 reverse 后 foo 值并没有改变。不要怪我太啰唆,如果你看到一个函数 nreverse,而且确实它能返回逆序的列表,不明所以就到处乱用,迟早会写出一个错误的函数。这个 nreverse 和前面的 reverse 差别就在于它是一个有破坏性的函数,也就是说它会修改它的参数。\n(nreverse foo) ; =\u0026gt; (c b a) foo ; =\u0026gt; (a) 为什么现在 foo 指向的是列表的末端呢?如果你实现过链表就知道,逆序操作是可以在原链表上进行的,这样原来头部指针会变成链表的尾端。列表也是(应该是,我也没有看过实现)这个原理。使用 nreverse 的唯一的好处是速度快,省资源。所以如果你只是想得到逆序后的列表就放心用 nreverse,否则还是用 reverse 的好。\nelisp 还有一些是具有破坏性的函数。最常用的就是 sort 函数:\n(setq foo \u0026#39;(3 2 4 1 5)) ; =\u0026gt; (3 2 4 1 5) (sort foo \u0026#39;\u0026lt;) ; =\u0026gt; (1 2 3 4 5) foo ; =\u0026gt; (3 4 5) 这一点请一定要记住,我就曾经在 sort 函数上犯了好几次错误。那如果我既要保留原列表,又要进行 sort 操作怎么办呢?可以用 copy-sequence 函数。这个函数只对列表进行复制,返回的列表的元素还是原列表里的元素,不会拷贝列表的元素。\nnconc 和 append 功能相似,但是它会修改除最后一个参数以外的所有的参数,nbutlast 和 butlast 功能相似,也会修改参数。这些函数都是在效率优先时才使用。总而言之,以 n 开头的函数都要慎用。\n把列表当集合用 列表可以作为无序的集合。合并集合用 append 函数。去除重复的 equal 元素用 delete-dups。查找一个元素是否在列表中,如果测试函数是用 eq,就用 memq,如果测试用 equal,可以用 member。删除列表中的指定的元素,测试函数为 eq 对应 delq 函数,equal 对应 delete。还有两个函数 remq 和 remove 也是删除指定元素。它们的差别是 delq 和 delete 可能会修改参数,而 remq 和 remove 总是返回删除后列表的拷贝。注意前面这是说的是可能会修改参数的值,也就是说可能不会,所以保险起见,用 delq 和 delete 函数要么只用返回值,要么用 setq 设置参数的值为返回值。\n(setq foo \u0026#39;(a b c)) ; =\u0026gt; (a b c) (remq \u0026#39;b foo) ; =\u0026gt; (a c) foo ; =\u0026gt; (a b c) (delq \u0026#39;b foo) ; =\u0026gt; (a c) foo ; =\u0026gt; (a c) (delq \u0026#39;a foo) ; =\u0026gt; (c) foo ; =\u0026gt; (a c) 把列表当关联表 用在 elisp 编程中,列表最常用的形式应该是作为一个关联表了。所谓关联表,就是可以用一个字符串(通常叫关键字,key)来查找对应值的数据结构。由列表实现的关联表有一个专门的名字叫 association list。尽管 elisp 里也有 hash table,但是 hash table 相比于 association list 至少这样几个缺点:\nhash table 里的关键字(key)是无序的,而 association list 的关键字 可以按想要的顺序排列 hash table 没有列表那样丰富的函数,只有一个 maphash 函数可以遍历列 表。而 association list 就是一个列表,所有列表函数都能适用 hash table 没有读入语法和输入形式,这对于调试和使用都带来很多不便 所以 elisp 的 hash table 不是一个首要的数据结构,只要不对效率要求很高,通常直接用 association list。数组可以作为关联表,但是数组不适合作为与人交互使用数据结构(毕竟一个有意义的名字比纯数字的下标更适合人脑)。所以关联表的地位在 elisp 中就非比寻常了,emacs 为关联表专门用 c 程序实现了查找的相关函数以提高程序的效率。在 association list 中关键字是放在元素的 car 部分,与它对应的数据放在这个元素的 cdr 部分。根据比较方法的不同,有 assq 和 assoc 两个函数,它们分别对应查找使用 eq 和 equal 两种方法。例如:\n(assoc \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (\u0026#34;a\u0026#34; 97) (assq \u0026#39;a \u0026#39;((a . 97) (b . 98))) ; =\u0026gt; (a . 97) 通常我们只需要查找对应的数据,所以一般来说都要用 cdr 来得到对应的数据:\n(cdr (assoc \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98)))) ; =\u0026gt; (97) (cdr (assq \u0026#39;a \u0026#39;((a . 97) (b . 98)))) ; =\u0026gt; 97 assoc-default 可以一步完成这样的操作:\n(assoc-default \u0026#34;a\u0026#34; \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (97) 如果查找用的键值(key)对应的数据也可以作为一个键值的话,还可以用 rassoc 和 rassq 来根据数据查找键值:\n(rassoc \u0026#39;(97) \u0026#39;((\u0026#34;a\u0026#34; 97) (\u0026#34;b\u0026#34; 98))) ; =\u0026gt; (\u0026#34;a\u0026#34; 97) (rassq \u0026#39;97 \u0026#39;((a . 97) (b . 98))) ; =\u0026gt; (a . 97) 如果要修改关键字对应的值,最省事的作法就是用 cons 把新的键值对加到列表的头端。但是这会让列表越来越长,浪费空间。如果要替换已经存在的值,一个想法就是用 setcdr 来更改键值对应的数据。但是在更改之前要先确定这个键值在对应的列表里,否则会产生一个错误。另一个想法是用 assoc 查找到对应的元素,再用 delq 删除这个数据,然后用 cons 加到列表里:\n(setq foo \u0026#39;((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98))) ; =\u0026gt; ((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98)) ;; update value by setcdr (if (setq bar (assoc \u0026#34;a\u0026#34; foo)) (setcdr bar \u0026#34;this is a\u0026#34;) (setq foo (cons \u0026#39;(\u0026#34;a\u0026#34; . \u0026#34;this is a\u0026#34;) foo))) ; =\u0026gt; \u0026#34;this is a\u0026#34; foo ; =\u0026gt; ((\u0026#34;a\u0026#34; . \u0026#34;this is a\u0026#34;) (\u0026#34;b\u0026#34; . 98)) ;; update value by delq and cons (setq foo (cons \u0026#39;(\u0026#34;a\u0026#34; . 97) (delq (assoc \u0026#34;a\u0026#34; foo) foo))) ; =\u0026gt; ((\u0026#34;a\u0026#34; . 97) (\u0026#34;b\u0026#34; . 98)) 如果不对顺序有要求的话,推荐用后一种方法吧。这样代码简洁,而且让最近更新的元素放到列表前端,查找更快。\n把列表当树用 列表的第一个元素如果作为结点的数据,其它元素看作是子节点,就是一个树了。由于树的操作都涉及递归,现在还没有说到函数,我就不介绍了。(其实是我不太熟,就不班门弄斧了)。\n遍历列表 遍历列表最常用的函数就是 mapc 和 mapcar 了。它们的第一个参数都是一个函数,这个函数只接受一个参数,每次处理一个列表里的元素。这两个函数唯一的差别是前者返回的还是输入的列表,而 mapcar 返回的函数返回值构成的列表:\n(mapc \u0026#39;1+ \u0026#39;(1 2 3)) ; =\u0026gt; (1 2 3) (mapcar \u0026#39;1+ \u0026#39;(1 2 3)) ; =\u0026gt; (2 3 4) 另一个比较常用的遍历列表的方法是用 dolist。它的形式是:\n(dolist (var list [result]) body...) 其中 var 是一个临时变量,在 body 里可以用来得到列表中元素的值。使用 dolist 的好处是不用写 lambda 函数。一般情况下它的返回值是 nil,但是你也可以指定一个值作为返回值(我觉得这个特性没有什么用,只省了一步而已):\n(dolist (foo \u0026#39;(1 2 3)) (incf foo)) ; =\u0026gt; nil (setq bar nil) (dolist (foo \u0026#39;(1 2 3) bar) (push (incf foo) bar)) ; =\u0026gt; (4 3 2) 其它常用函数 如果看过一些函数式语言教程的话,一定对 fold(或叫 accumulate、reduce)和 filter 这些函数记忆深刻。不过 elisp 里好像没有提供这样的函数。remove-if 和 remove-if-not 可以作 filter 函数,但是它们是 cl 里的,自己用用没有关系,不能强迫别人也跟着用,所以不能写到 elisp 里。如果不用这两个函数,也不用别人的函数的话,自己实现不妨用这样的方法:\n(defun my-remove-if (predicate list) (delq nil (mapcar (lambda (n) (and (not (funcall predicate n)) n)) list))) (defun evenp (n) (= (% n 2) 0)) (my-remove-if \u0026#39;evenp \u0026#39;(0 1 2 3 4 5)) ; =\u0026gt; (1 3 5) fold 的操作只能用变量加循环或 mapc 操作来代替了:\n(defun my-fold-left (op initial list) (dolist (var list initial) (setq initial (funcall op initial var)))) (my-fold-left \u0026#39;+ 0 \u0026#39;(1 2 3 4)) ; =\u0026gt; 10 这里只是举个例子,事实上你不必写这样的函数,直接用函数里的遍历操作更好一些。\n产生数列常用的方法是用 number-sequence(这里不禁用说一次,不要再用 loop 产生 tab-stop-list 了,你们 too old 了)。不过这个函数好像 在 emacs21 时好像还没有。\n解析文本时一个很常用的操作是把字符串按分隔符分解,可以用 split-string 函数:\n(split-string \u0026#34;key = val\u0026#34; \u0026#34;\\\\s-*=\\\\s-*\u0026#34;) ; =\u0026gt; (\u0026#34;key\u0026#34; \u0026#34;val\u0026#34;) 与 split-string 对应是把几个字符串用一个分隔符连接起来,这可以用 mapconcat 完成。比如:\n(mapconcat \u0026#39;identity \u0026#39;(\u0026#34;a\u0026#34; \u0026#34;b\u0026#34; \u0026#34;c\u0026#34;) \u0026#34;\\t\u0026#34;) ; =\u0026gt; \u0026#34;a b c\u0026#34; identity 是一个特殊的函数,它会直接返回参数。mapconcat 第一个参数是一个函数,可以很灵活的使用。\n函数列表 ;; 列表测试 (consp object) (listp object) (null object) ;; 列表构造 (cons car cdr) (list \u0026amp;rest objects) (append \u0026amp;rest sequences) ;; 访问列表元素 (car list) (cdr list) (cadr x) (caar x) (cddr x) (cdar x) (nth n list) (nthcdr n list) (last list \u0026amp;optional n) (butlast list \u0026amp;optional n) ;; 修改 cons cell (setcar cell newcar) (setcdr cell newcdr) ;; 列表操作 (push newelt listname) (pop listname) (reverse list) (nreverse list) (sort list predicate) (copy-sequence arg) (nconc \u0026amp;rest lists) (nbutlast list \u0026amp;optional n) ;; 集合函数 (delete-dups list) (memq elt list) (member elt list) (delq elt list) (delete elt seq) (remq elt list) (remove elt seq) ;; 关联列表 (assoc key list) (assq key list) (assoc-default key alist \u0026amp;optional test default) (rassoc key list) (rassq key list) ;; 遍历函数 (mapc function sequence) (mapcar function sequence) (dolist (var list [result]) body...) ;; 其它 (number-sequence from \u0026amp;optional to inc) (split-string string \u0026amp;optional separators omit-nulls) (mapconcat function sequence separator) (identity arg) 问题解答 用 list 生成 (a b c) 答案是 (list 'a 'b 'c)。很简单的一个问题。从这个例子可以看出为什么要想出 用 \u0026rsquo; 来输入列表。这就是程序员“懒”的美德呀!\nnthcdr 的一个实现 (defun my-nthcdr (n list) (if (or (null list) (= n 0)) (car list) (my-nthcdr (1- n) (cdr list)))) 这样的实现看上去很简洁,但是一个最大的问题的 elisp 的递归是有限的,所以如果想这个函数没有问题,还是用循环还实现比较好。\nmy-subseq 函数的定义 (defun my-subseq (list from \u0026amp;optional to) (if (null to) (nthcdr from list) (butlast (nthcdr from list) (- (length list) to)))) (setcdr foo foo) 是什么怪东西? 可能你已经想到了,这就是传说中的环呀。这在 info elisp - circular objects 里有介绍。elisp 里用到这样的环状列表并不多见,但是也不是没有,org 和 session 那个 bug 就是由于一个环状列表造成的。\n基本数据类型之四 \u0026ndash; 数组和序列 序列是列表和数组的统称,也就是说列表和数组都是序列。它们的共性是内部的元素都是有序的。elisp 里的数组包括字符串、向量、char-table 和布尔向量。它们的关系可以用下面图表示:\n_____________________________________________\r| |\r| sequence |\r| ______ ________________________________ |\r| | | | | |\r| | list | | array | |\r| | | | ________ ________ | |\r| |______| | | | | | | |\r| | | vector | | string | | |\r| | |________| |________| | |\r| | ____________ _____________ | |\r| | | | | | | |\r| | | char-table | | bool-vector | | |\r| | |____________| |_____________| | |\r| |________________________________| |\r|_____________________________________________| 组有这样一些特性:\n数组内的元素都对应一个下标,第一个元素下标为 0,接下来是 1。数组内 的元素可以在常数时间内访问。 数组在创建之后就无法改变它的长度。 数组是自求值的。 数组里的元素都可以用 aref 来访问,用 aset 来设置。 向量可以看成是一种通用的数组,它的元素可以是任意的对象。而字符串是一种特殊的数组,它的元素只能是字符。如果元素是字符时,使用字符串相比向量更好,因为字符串需要的空间更少(只需要向量的 1/4),输出更直观,能用文本属性(text property),能使用 emacs 的 io 操作。但是有时必须使用向量,比如存储按键序列。\n由于 char-table 和 bool-vector 使用较少,而且较难理解,这里就不介绍了。\n测试函数 sequencep 用来测试一个对象是否是一个序列。arrayp 测试对象是否是数组。vectorp、char-table-p 和 bool-vector-p 分别测试对象是否是向量、char-table、bool-vector。\n序列的通用函数 一直没有提到一个重要的函数 length,它可以得到序列的长度。但是这个函数只对真列表有效。对于一个点列表和环形列表这个函数就不适用了。点列表会出参数类型不对的错误,而环形列表就更危险,会陷入死循环。如果不确定参数类型,不妨用 safe-length。比如:\n(safe-length \u0026#39;(a . b)) ; =\u0026gt; 1 (safe-length \u0026#39;#1=(1 2 . #1#)) ; =\u0026gt; 3 思考题\n写一个函数来检测列表是否是一个环形列表。由于现在还没有介绍 let 绑定和循环,不过如果会函数定义,还是可以用递归来实现的。\n取得序列里第 n 个元素可以用 elt 函数。但是我建议,对于已知类型的序列,还是用对应的函数比较好。也就是说,如果是列表就用 nth,如果是数组就用 aref。这样一方面是省去 elt 内部的判断,另一方面读代码时能很清楚知道序列的类型。\ncopy-sequence 在前面已经提到了。不过同样 copy-sequence 不能用于点列表和环形列表。对于点列表可以用 copy-tree 函数。环形列表就没有办法复制了。 好在这样的数据结构很少用到。\n数组操作 创建字符串已经说过了。创建向量可以用 vector 函数:\n(vector \u0026#39;foo 23 [bar baz] \u0026#34;rats\u0026#34;) 当然也可以直接用向量的读入语法创建向量,但是由于数组是自求值的,所以这样得到的向量和原来是一样的,也就是说参数不进行求值,看下面的例子就明白了:\nfoo ; =\u0026gt; (a b) [foo] ; =\u0026gt; [foo] (vector foo) ; =\u0026gt; [(a b)] 用 make-vector 可以生成元素相同的向量。\n(make-vector 9 \u0026#39;z) ; =\u0026gt; [z z z z z z z z z] fillarray 可以把整个数组用某个元素填充。\n(fillarray (make-vector 3 \u0026#39;z) 5) ; =\u0026gt; [5 5 5] aref 和 aset 可以用于访问和修改数组的元素。如果使用下标超出数组长度的话,会产生一个错误。所以要先确定数组的长度才能用这两个函数。\nvconcat 可以把多个序列用 vconcat 连接成一个向量。但是这个序列必须是真列表。这也是把列表转换成向量的方法。\n(vconcat [a b c] \u0026#34;aa\u0026#34; \u0026#39;(foo (6 7))) ; =\u0026gt; [a b c 97 97 foo (6 7)] 把向量转换成列表可以用 append 函数,这在前一节中已经提到。\n思考题\n如果知道 elisp 的 let 绑定和循环的使用方法,不妨试试实现一个 elisp 的 tr 函数,它接受三个参数,一是要操作的字符串,另外两个分别是要替换的字符集,和对应的替换后的字符集(当它是空集时,删除字符串中所有对应的字符)。\n函数列表 ;; 测试函数 (sequencep object) (arrayp object) (vectorp object) (char-table-p object) (bool-vector-p object) ;; 序列函数 (length sequence) (safe-length list) (elt sequence n) (copy-sequence arg) (copy-tree tree \u0026amp;optional vecp) ;; 数组函数 (vector \u0026amp;rest objects) (make-vector length init) (aref array idx) (aset array idx newelt) (vconcat \u0026amp;rest sequences) (append \u0026amp;rest sequences) 问题解答 测试列表是否是环形列表 这个算法是从 safe-length 定义中得到的。你可以直接看它的源码。下面是我写的函数。\n(defun circular-list-p (list) (and (consp list) (circular-list-p-1 (cdr list) list 0))) (defun circular-list-p-1 (tail halftail len) (if (eq tail halftail) t (if (consp tail) (circular-list-p-1 (cdr tail) (if (= (% len 2) 0) (cdr halftail) halftail) (1+ len)) nil))) 转换字符的 tr 函数 (defun my-tr (str from to) (if (= (length to) 0) ; 空字符串 (progn (setq from (append from nil)) (concat (delq nil (mapcar (lambda (c) (if (member c from) nil c)) (append str nil))))) (let (table newstr pair) ;; 构建转换表 (dotimes (i (length from)) (push (cons (aref from i) (aref to i)) table)) (dotimes (i (length str)) (push (if (setq pair (assoc (aref str i) table)) (cdr pair) (aref str i)) newstr)) (concat (nreverse newstr) nil)))) 这里用到的 dotimes 函数相当于一个 c 里的 for 循环。如果改写成 while 循环,相当于:\n(let (var) (while (\u0026lt; var count) body (setq var (1+ var))) result) 从这个例子也可以看出,由于列表具有丰富的函数和可变长度,使列表比数组使用更方便,而且效率往往更高。\n基本数据类型之五 \u0026ndash; 符号 符号是有名字的对象。可能这么说有点抽象。作个不恰当的比方,符号可以看作是 c 语言里的指针。通过符号你可以得到和这个符号相关联的信息,比如值,函数,属性列表等等。\n首先必须知道的是符号的命名规则。符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点“-+=*/”。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \\ 表示为符号,比如:\n(symbolp \u0026#39;+1) ; =\u0026gt; nil (symbolp \u0026#39;\\+1) ; =\u0026gt; t (symbol-name \u0026#39;\\+1) ; =\u0026gt; \u0026#34;+1\u0026#34; 其它字符 _~!@$%^\u0026amp;:\u0026lt;\u0026gt;{}? 用的比较少。但是也可以直接作为符号的名字。任何其它字符都可以用 \\ 转义后用在符号名字里。但是和字符串里字符表示不同,\\ 转义后只是表示其后的字符,比如 \\t 代表的字符 t,而不是制表符。如果要在符号名里使用制表符,必须在 \\ 后加上制表符本身。\n符号名是区分大小写的。这里有一些符号名的例子:\nfoo ; 名为 `foo\u0026#39; 的符号 foo ; 名为 `foo\u0026#39; 的符号,和 `foo\u0026#39; 不同 char-to-string ; 名为 `char-to-string\u0026#39; 的符号 1+ ; 名为 `1+\u0026#39; 的符号 (不是整数 `+1\u0026#39;) \\+1 ; 名为 `+1\u0026#39; 的符号 (可读性很差的名字) \\(*\\ 1\\ 2\\) ; 名为 `(* 1 2)\u0026#39; 的符号 (更差劲的名字). +-*/_~!@$%^\u0026amp;=:\u0026lt;\u0026gt;{} ; 名为 `+-*/_~!@$%^\u0026amp;=:\u0026lt;\u0026gt;{}\u0026#39; 的符号。 ; 这些字符无须转义 创建符号 一个名字如何与数据对应上呢?这就要了解一下符号是如何创建的了。符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。从这个名字可以看出这个表是用数组类型,事实上是一个向量。当 emacs 创建一个符号时,首先会对这个名字求 hash 值以得到一个在 obarray 这个向量中查找值所用的下标。hash 是查找字符串的很有效的方法。这里强调的是 obarray 不是一个特殊的数据结构,就是一个一般的向量。全局变量 obarray 里 emacs 所有变量、函数和其它符号所使用的 obarray(注意不同语境中 obarray 的含义不同。前一个 obarray 是变量名,后一个 obarray 是数据类型名)。也可以自己建立向量,把这个向量作为 obarray 来使用。这是一种代替散列的一种方法。它比直接使用散列有这样一些好处:\n符号不仅可以有一个值,还可以用属性列表,后者又可以相当于一个关联列表。这样有很高的扩展性,而且可以表达更高级的数据结构。 emacs 里有一些函数可以接受 obarray 作为参数,比如补全相关的函数。 当 lisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的 obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray 里。为了不污染 obarray,我下面的例子中尽量在 foo 这个 obarray 里进行。一般来说,去了 foo 参数,则会在 obarray 里进行。其结果应该是相同的:\n(setq foo (make-vector 10 0)) ; =\u0026gt; [0 0 0 0 0 0 0 0 0 0] (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; nil (intern \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc lisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #::\n(intern-soft \u0026#34;abc\u0026#34;) ; =\u0026gt; nil \u0026#39;abc ; =\u0026gt; abc (intern-soft \u0026#34;abc\u0026#34;) ; =\u0026gt; abc (intern-soft \u0026#34;abcd\u0026#34;) ; =\u0026gt; nil \u0026#39;#:abcd ; =\u0026gt; abcd (intern-soft \u0026#34;abcd\u0026#34;) ; =\u0026gt; nil 如果想除去 obarray 里的符号,可以用 unintern 函数。unintern 可以用符号名或符号作参数在指定的 obarray 里去除符号,成功去除则返回 t,如果没有查找到对应的符号则返回 nil:\n(intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; abc (unintern \u0026#34;abc\u0026#34; foo) ; =\u0026gt; t (intern-soft \u0026#34;abc\u0026#34; foo) ; =\u0026gt; nil 和 hash-table 一样,obarray 也提供一个 mapatoms 函数来遍历整个 obarray。比如要计算 obarray 里所有的符号数量:\n(setq count 0) ; =\u0026gt; 0 (defun count-syms (s) (setq count (1+ count))) ; =\u0026gt; count-syms (mapatoms \u0026#39;count-syms) ; =\u0026gt; nil count ; =\u0026gt; 28371 (length obarray) ; =\u0026gt; 1511 思考题\n由前面的例子可以看出 elisp 中的向量长度都是有限的,而 obarray 里的符号有成千上万个。那这些符号是怎样放到 obarray 里的呢?\n符号的组成 每个符号可以对应四个组成部分,一是符号的名字,可以用 symbol-name 访问。二是符号的值。符号的值可以通过 set 函数来设置,用 symbol-value 来访问。\n(set (intern \u0026#34;abc\u0026#34; foo) \u0026#34;i\u0026#39;m abc\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m abc\u0026#34; (symbol-value (intern \u0026#34;abc\u0026#34; foo)) ; =\u0026gt; \u0026#34;i\u0026#39;m abc\u0026#34; 可能大家最常见到 setq 函数,而 set 函数确很少见到。setq 可以看成是一个宏,它可以让你用 (setq sym val) 代替 (set (quote sym) val)。事实上这也是它名字的来源 (q 代表 quoted)。但是 setq 只能设置 obarray 里的变量,前面这个例子中就只能用 set 函数。\n思考题\n参考 assoc-default 的代码,写一个函数从一个关联列表中除去一个关键字对应的元素。这个函数可以直接修改关联列表符号的值。要求可以传递一个参数作为测试关键字是否相同的函数。比如:\n(setq foo \u0026#39;((?a . a) (?a . c) (?b . d))) (remove-from-list \u0026#39;foo ?b \u0026#39;char-equal) ; =\u0026gt; ((97 . a) (65 . c)) foo ; =\u0026gt; ((97 . a) (65 . c)) 如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil。对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误。\n符号的第三个组成部分是函数。它可以用 symbol-function 来访问,用 fset 来设置\n(fset (intern \u0026#34;abc\u0026#34; foo) (symbol-function \u0026#39;car)) ; =\u0026gt; #\u0026lt;subr car\u0026gt; (funcall (intern \u0026#34;abc\u0026#34; foo) \u0026#39;(a . b)) ; =\u0026gt; a 类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。\n符号的第四个组成部分是属性列表 (property list)。通常属性列表用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:\n(put (intern \u0026#34;abc\u0026#34; foo) \u0026#39;doc \u0026#34;this is abc\u0026#34;) ; =\u0026gt; \u0026#34;this is abc\u0026#34; (get (intern \u0026#34;abc\u0026#34; foo) \u0026#39;doc) ; =\u0026gt; \u0026#34;this is abc\u0026#34; (symbol-plist (intern \u0026#34;abc\u0026#34; foo)) ; =\u0026gt; (doc \u0026#34;this is abc\u0026#34;) 关联列表和属性列表很相似。符号的属性列表在内部表示上是用 (prop1 value1 prop2 value2 \u0026hellip;) 的形式,和关联列表也是很相似的。属性列表在查找和这个符号相关的信息时,要比直接用关联列表要简单快捷的多。所以变量的文档等信息都是放在符号的属性列表里。但是关联表在头端加入元素是很快的,而且它可以删除表里的元素。而属性列表则不能删除一个属性。\n如果已经把属性列表取出,那么还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表\n(plist-get \u0026#39;(foo 4) \u0026#39;foo) ; =\u0026gt; 4 (plist-get \u0026#39;(foo 4 bad) \u0026#39;bar) ; =\u0026gt; nil (setq my-plist \u0026#39;(bar t foo 4)) ; =\u0026gt; (bar t foo 4) (setq my-plist (plist-put my-plist \u0026#39;foo 69)) ; =\u0026gt; (bar t foo 69) (setq my-plist (plist-put my-plist \u0026#39;quux \u0026#39;(a))) ; =\u0026gt; (bar t foo 69 quux (a)) 思考题\n你能不能用已经学过的函数来实现 plist-get 和 plist-put?\n函数列表 (symbolp object) (intern-soft name \u0026amp;optional obarray) (intern string \u0026amp;optional obarray) (unintern name \u0026amp;optional obarray) (mapatoms function \u0026amp;optional obarray) (symbol-name symbol) (symbol-value symbol) (boundp symbol) (set symbol newval) (setq sym val sym val ...) (symbol-function symbol) (fset symbol definition) (fboundp symbol) (symbol-plist symbol) (get symbol propname) (put symbol propname value) 问题解答 obarray 里符号数为什么大于向量长度 其实这和散列的的实现是一样的。obarray 里的每一个元素通常称为 bucket。 一个 bucket 是可以容纳多个相同 hash 值的字符串和它们的数据。我们可以用 这样的方法来模拟一下:\n(defun hash-string (str) (let ((hash 0) c) (dotimes (i (length str)) (setq c (aref str i)) (if (\u0026gt; c #o140) (setq c (- c 40))) (setq hash (+ (setq hash (lsh hash 3)) (lsh hash -28) c))) hash)) (let ((len 10) str hash) (setq foo (make-vector len 0)) (dotimes (i (1+ len)) (setq str (char-to-string (+ ?a i)) hash (% (hash-string str) len)) (message \u0026#34;i put %s in slot %d\u0026#34; str hash) (if (eq (aref foo hash) 0) (intern str foo) (message \u0026#34;i found %s is already taking the slot: %s\u0026#34; (aref foo hash) foo) (intern str foo) (message \u0026#34;now i\u0026#39;am in the slot too: %s\u0026#34; foo)))) 在我这里的输出是\ni put a in slot 7\ri put b in slot 8\ri put c in slot 9\ri put d in slot 0\ri put e in slot 1\ri put f in slot 2\ri put g in slot 3\ri put h in slot 4\ri put i in slot 5\ri put j in slot 6\ri put k in slot 7\ri found a is already taking the slot: [d e f g h i j a b c]\rnow i\u0026#39;am in the slot too: [d e f g h i j k b c] 当然,这个 hash-string 和实际 obarray 里用的 hash-string 只是算法上是 相同的,但是由于数据类型和 c 不是完全相同,所以对于长一点的字符串结果 可能不一样,我只好用单个字符来演示一下。\n根据关键字删除关联列表中的元素 (defun remove-from-list (list-var key \u0026amp;optional test) (let ((prev (symbol-value list-var)) tail found value elt) (or test (setq test \u0026#39;equal)) (if (funcall test (caar prev) key) (set list-var (cdr prev)) (setq tail (cdr prev)) (while (and tail (not found)) (setq elt (car tail)) (if (funcall test (car elt) key) (progn (setq found t) (setcdr prev (cdr tail))) (setq tail (cdr tail) prev (cdr prev))))) (symbol-value list-var))) 注意这个函数的参数 list-var 是一个符号,所以这个函数不能直接传递一个列表。这和 add-to-list 的参数是一样的。\nplist-get 和 plist-put 的实现 (defun my-plist-get (plist prop) (cadr (memq plist prop))) (defun my-plist-put (plist prop val) (let ((tail (memq prop plist))) (if tail (setcar (cdr tail) val) (setcdr (last plist) (list prop val)))) plist) my-plist-put 函数没有 plist-put 那样 robust,如果属性列表是 \u0026lsquo;(bar t foo) 这样的话,这个函数就会出错。而且加入一个属性的时间复杂度比 plist 更高(memq 和 last 都是 o(n)),不过可以用循环来达到相同的时间复杂度。\n求值规则 至此,elisp 中最常见的数据类型已经介绍完了。我们可以真正开始学习怎样写一个 elisp 程序。如果想深入了解一下 lisp 是如何工作的,不妨先花些时间看看 lisp 的求值过程。当然忽略这一部分也是可以的,因为我觉得这个求值规则是那么自然,以至于你会认为它就是应该这样的。\n求值是 lisp 解释器的核心,理解了求值过程也就学会了 lisp 编程的一半。正因为这样,我有点担心自己说得不清楚或者理解错误,会误导了你。所以如果真想深入了解的话,还是自己看 info elisp - evaluation 这一章吧。\n一个要求值的 lisp 对象被称为表达式(form)。所有的表达式可以分为三种:符号、列表和其它类型(废话)。下面一一说明各种表达式的求值规则。\n第一种表达式是最简单的,自求值表达式。前面说过数字、字符串、向量都是自求值表达式。还有两个特殊的符号 t 和 nil 也可以看成是自求值表达式。\n第二种表达式是符号。符号的求值结果就是符号的值。如果它没有值,就会出现 void-variable 的错误。\n第三种表达式是列表表达式。而列表表达式又可以根据第一个元素分为函数调用、宏调用和特殊表达式(special form)三种。列表的第一个表达式如果是一个符号,解释器会查找这个表达式的函数值。如果函数值是另一个符号,则会继续查找这个符号的函数值。这称为“symbol function indirection”。最后直到某个符号的函数值是一个 lisp 函数(lambda 表达式)、byte-code 函数、原子函数(primitive function)、宏、特殊表达式或 autoload 对象。如果不是这些类型,比如某个符号的函数值是前面出现的某个符号导致无限循环,或者某个符号函数值为空,都会导致一个错误 invalid-function。\n这个函数显示 indirection function\n(symbol-function \u0026#39;car) ; =\u0026gt; #\u0026lt;subr car\u0026gt; (fset \u0026#39;first \u0026#39;car) ; =\u0026gt; car (fset \u0026#39;erste \u0026#39;first) ; =\u0026gt; first (erste \u0026#39;(1 2 3)) ; =\u0026gt; 1 对于第一个元素是 lisp 函数对象、byte-code 对象和原子函数时,这个列表也称为函数调用(funtion call)。对这样的列表求值时,先对列表中其它元素先求值,求值的结果作为函数调用的真正参数。然后使用 apply 函数用这些参数调用函数。如果函数是用 lisp 写的,可以理解为把参数和变量绑定到函数后,对函数体顺序求值,返回最后一个 form 的值。\n如果第一个元素是一个宏对象,列表里的其它元素不会立即求值,而是根据宏定义进行扩展。如果扩展后还是一个宏调用,则会继续扩展下去,直到扩展的结果不再是一个宏调用为止。例如\n(defmacro cadr (x) (list \u0026#39;car (list \u0026#39;cdr x))) 这样 (cadr (assq 'handler list)) 扩展后成为 (car (cdr (assq 'handler list)))。\n第一个元素如果是一个特殊表达式时,它的参数可能并不会全求值。这些特殊表达式通常是用于控制结构或者变量绑定。每个特殊表达式都有对应的求值规则。这在下面会提到。\n最后用这个伪代码来说明一下 elisp 中的求值规则:\n(defun (eval exp) (cond ((numberp exp) exp) ((stringp exp) exp) ((arrayp exp) exp) ((symbolp exp) (symbol-value exp)) ((special-form-p (car exp)) (eval-special-form exp)) ((fboundp (car exp)) (apply (car exp) (cdr exp))) (t (error \u0026#34;unknown expression type -- eval %s\u0026#34; exp)))) 变量 在此之前,我们已经见过 elisp 中的两种变量,全局变量和 let 绑定的局部变量。它们相当于其它语言中的全局变量和局部变量。\n关于 let 绑定的变量,有两点需要补充的。当同一个变量名既是全局变量也是局部变量,或者用 let 多层绑定,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如\n(progn (setq foo \u0026#34;i\u0026#39;m global variable!\u0026#34;) (let ((foo 5)) (message \u0026#34;foo value is: %s\u0026#34; foo) (let (foo) (setq foo \u0026#34;i\u0026#39;m local variable!\u0026#34;) (message foo)) (message \u0026#34;foo value is still: %s\u0026#34; foo)) (message foo)) 另外需要注意一点的是局部变量的绑定不能超过一定的层数,也就是说,你不能把 foo 用 let 绑定 10000 层。当然普通的函数是不可能写成这样的,但是递归函数就不一定了。限制层数的变量在 max-specpdl-size 中定义。如果你写的递归函数有这个需要的话,可以先设置这个变量的值。\nemacs 有一种特殊的局部变量 ── buffer-local 变量。\nbuffer-local 变量 emacs 能有如此丰富的模式,各个缓冲区之间能不相互冲突,很大程度上要归功于 buffer-local 变量。\n声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是相当于在所有变量中都产生一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。\n为了方便演示,下面的代码我假定你是在 *scratch* 缓冲区里运行。我使用另一个一般都会有的缓冲区 *messages* 作为测试。先介绍两个用到的函数( with-current-buffer 其实是一个宏)。\nwith-current-buffer 的使用形式是\n(with-current-buffer buffer body) 其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。\nget-buffer 可以用缓冲区的名字得到对应的缓冲区对象。如果没有这样名字的缓冲区会返回 nil。\n下面是使用 buffer-local 变量的例子:\n(setq foo \u0026#34;i\u0026#39;m global variable!\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; (make-local-variable \u0026#39;foo) ; =\u0026gt; foo foo ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; (setq foo \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34;) ; =\u0026gt; \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34; foo ; =\u0026gt; \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34; (with-current-buffer \u0026#34;*messages*\u0026#34; foo) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; 从这个例子中可以看出,当一个符号作为全局变量时有一个值的话,用 make-local-variable 声明为 buffer-local 变量时,这个变量的值还是全局变量的值。这时候全局的值也称为缺省值。你可以用 default -value 来访问这个符号的全局变量的值\n(default-value \u0026#39;foo) ; =\u0026gt; \u0026#34;i\u0026#39;m global variable!\u0026#34; 如果一个变量是 buffer-local,那么在这个缓冲区内使用用 setq 就只能用改变当前缓冲区里这个变量的值。setq-default 可以修改符号作为全局变量的值。通常在 .emacs 里经常使用 setq-default,这样可以防止修改的是导入 .emacs 文件对应的缓冲区里的 buffer-local 变量,而不是设置全局的值。\n测试一个变量是不是 buffer-local 可以用 local-variable-p\n(local-variable-p \u0026#39;foo) ; =\u0026gt; t (local-variable-p \u0026#39;foo (get-buffer \u0026#34;*messages*\u0026#34;)) ; =\u0026gt; nil 如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量可以用 buffer-local-value\n(with-current-buffer \u0026#34;*messages*\u0026#34; (buffer-local-value \u0026#39;foo (get-buffer \u0026#34;*scratch*\u0026#34;))) ; =\u0026gt; \u0026#34;i\u0026#39;m buffer local variable!\u0026#34; 变量的作用域 我们现在已经学习这样几种变量:\n全局变量 buffer-local 变量 let 绑定局部变量 如果还要考虑函数的参数列表声明的变量,也就是 4 种类型的变量。那这种变量的作用范围 (scope) 和生存期(extent)分别是怎样的呢?\n作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。\n比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子:\n(defun binder (x) ; `x\u0026#39; is bound in `binder\u0026#39;. (foo 5)) ; `foo\u0026#39; is some other function. (defun user () ; `x\u0026#39; is used \u0026#34;free\u0026#34; in `user\u0026#39;. (list x)) (defun foo (ignore) (user)) (binder 10) ; =\u0026gt; (10) 对于词法作用域的语言,在 user 函数里无论如何是不能访问 binder 函数中绑定的 x。但是在 elisp 中可以。\n生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭 emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,emacs lisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。这和 c 和 pascal 里的 local 和 automatic 变量是一样的。与此相对的是 indefinite extent,变量即使离开绑定它的表达式还能有效。比如:\n(defun make-add (n) (function (lambda (m) (+ n m)))) ; return a function. (fset \u0026#39;add2 (make-add 2)) ; define function `add2\u0026#39; ; with `(make-add 2)\u0026#39;. (add2 4) ; try to add 2 to 4. 其它 lisp 方言中有闭包,但是 emacs lisp 中没有。\n说完这些概念,可能你还是一点雾水。我给一个判断变量是否有效的方法吧:\n看看包含这个变量的 form 中是否有 let 绑定这个局部变量。如果这个 form 不是在定义一个函数,则跳到第 3 步。 如果是在定义函数,则不仅要看这个函数的参数中是否有这个变量,而且还要看所有直接或间接调用这个函数的函数中是否有用 let 绑定或者参数列表里有这个变量名。这就没有办法确定了,所以你永远无法判断一个函数中出现的没有用 let 绑定,也不在参数列表中的变量是否是没有定义过的。但是一般来说这不是一个好习惯。 看这个变量是否是一个全局变量或者是 buffer-local 变量。 对于在一个函数中绑定一个变量,而在另一个函数中还在使用,manual 里认为这两个种情况下是比较好的:\n这个变量只有相关的几个函数中使用,在一个文件中放在一起。这个变量起程序里通信的作用。而且需要写好注释告诉其它程序员怎样使用它。 如果这个变量是定义明确、有很好文档作用的,可能让所有函数使用它,但是不要设置它。比如 case-fold-search。(我怎么觉得这里是用全局变量呢。) 思考题\n先在 *scratch* 缓冲区里运行了 (kill-local-variable 'foo) 后,运行几次下面的表达式,你能预测它们结果吗?\n(progn (setq foo \u0026#34;i\u0026#39;m local variable!\u0026#34;) (let ((foo \u0026#34;i\u0026#39;m local variable!\u0026#34;)) (set (make-local-variable \u0026#39;foo) \u0026#34;i\u0026#39;m buffer-local variable!\u0026#34;) (setq foo \u0026#34;this is a variable!\u0026#34;) (message foo)) (message foo)) 其它函数 一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 xemacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。\n使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。\nfoo ; =\u0026gt; \u0026#34;i\u0026#39;m local variable!\u0026#34; (boundp \u0026#39;foo) ; =\u0026gt; t (default-boundp \u0026#39;foo) ; =\u0026gt; t (makunbound \u0026#39;foo) ; =\u0026gt; foo foo ; this will signal an error (default-boundp \u0026#39;foo) ; =\u0026gt; t (kill-local-variable \u0026#39;foo) ; =\u0026gt; foo 变量名习惯 对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:\nhook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。 function 值为一个函数 functions 值为一个函数列表 flag 值为 nil 或 non-nil predicate 值是一个作判断的函数,返回 nil 或 non-nil program 或 -command 一个程序或 shell 命令名 form 一个表达式 forms 一个表达式列表。 map 一个按键映射(keymap) 函数列表 (make-local-variable variable) (make-variable-buffer-local variable) (with-current-buffer buffer \u0026amp;rest body) (get-buffer name) (default-value symbol) (local-variable-p variable \u0026amp;optional buffer) (buffer-local-value variable buffer) (boundp symbol) (default-boundp symbol) (makunbound symbol) (kill-local-variable variable) (kill-all-local-variables) 变量列表 max-specpdl-size 问题解答 同一个表达式运行再次结果不同? 运行第一次时,foo 缺省值为 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;,而 buffer-local 值为 \u0026ldquo;this is a variable!\u0026quot;。第一个和第二个 message 都会显示 \u0026ldquo;this is a variable!\u0026quot;。运行第二次时,foo 缺省值和 buffer-local 值都成了 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;,而第一次 message 显示 \u0026ldquo;this is a variable!\u0026quot;,第二次 显示 \u0026ldquo;i\u0026rsquo;m local variable!\u0026quot;。这是由于 make-local-variable 在这个符号是 否已经是 buffer-local 变量时有不同表现造成的。如果已经是一个 buffer-local 变量,则它什么也不做,而如果不是,则会生成一个 buffer-local 变量,这时在这个表达式内的所有 foo 也被重新绑定了。希望你 写的函数能想到一点。\n函数和命令 在 elisp 里类似函数的对象很多,比如:\n函数。这里的函数特指用 lisp 写的函数。 原子函数(primitive)。用 c 写的函数,比如 car、append。 lambda 表达式 特殊表达式 宏 (macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。 命令。命令能用 command-execute 调用。函数也可以是命令。 以上这些用 functionp 来测试都会返回 t。\n我们已经学过如何定义一个函数。但是这些函数的参数个数都是确定。但是你可以看到 emacs 里有很多函数是接受可选参数,比如 random 函数。还有一些函数可以接受不确定的参数,比如加减乘除。这样的函数在 elisp 中是如何定义的呢?\n参数列表的语法 这是参数列表的方法形式:\n(required-vars...\r[\u0026amp;optional optional-vars...]\r[\u0026amp;rest rest-var]) 它的意思是说,你必须把必须提供的参数写在前面,可选的参数写在后面,最后用一个符号表示剩余的所有参数。比如\n(defun foo (var1 var2 \u0026amp;optional opt1 opt2 \u0026amp;rest rest) (list var1 var2 opt1 opt2 rest)) (foo 1 2) ; =\u0026gt; (1 2 nil nil nil) (foo 1 2 3) ; =\u0026gt; (1 2 3 nil nil) (foo 1 2 3 4 5 6) ; =\u0026gt; (1 2 3 4 (5 6)) 从这个例子可以看出,当可选参数没有提供时,在函数体里,对应的参数值都是 nil。同样调用函数时没有提供剩余参数时,其值也为 nil,但是一旦提供了剩余参数,则所有参数是以列表的形式放在对应变量里。\n思考题\n写一个函数测试两个浮点数是否相等,设置一个可选参数,如果提供这个参数,则用这个参数作为测试误差,否则用 1.0e-6 作为误差。\n关于文档字符串 最好为你的函数都提供一个文档字符串。关于文档字符串有一些规范,最好遵守这些约定。\n字符串的第一行最好是独立的。因为 apropos 命令只能显示第一行的文档。所以最好用一行(一两个完整的句子)总结这个函数的目的。\n文档的缩进最好要根据最后的显示的效果来调用。因为引号之类字符会多占用一个字符,所以在源文件里缩进最好看,不一定显示的最好。\n如果你想要让你的函数参数显示的与函数定义的不同(比如提示用户如何调用这个函数),可以在文档最后一行,加上一行:\n\\(fn arglist) 注意这一行前面要有一个空行,这一行后不能再有空行。比如\n(defun foo (var1 var2 \u0026amp;optional opt1 opt2 \u0026amp;rest rest) \u0026#34;you should call the function like: \\(fn v1 v2)\u0026#34; (list var1 var2 opt1 opt2 rest)) 还有一些有特殊标记功能的符号,比如 ``\u0026rsquo;引起的符号名可以生成一个链接,这样可以在help 中更方便的查看相关变量或函数的文档。\\{major-mode-map}` 可以显示扩展成这个模式按键的说明,例如:\n(defun foo () \u0026#34;a simple document string to show how to use `\u0026#39; and \\\\=\\\\{}. you can press this button `help\u0026#39; to see the document of function \\\u0026#34;help\\\u0026#34;. this is keybind of text-mode(substitute from \\\\=\\\\{text-mode-map}): \\\\{text-mode-map} see also `substitute-command-keys\u0026#39; and `documentation\u0026#39;\u0026#34; ) 调用函数 通常函数的调用都是用 eval 进行的,但是有时需要在运行时才决定使用什么函数,这时就需要用 funcall 和 apply 两个函数了。这两个函数都是把其余的参数作为函数的参数进行调用。那这两个函数有什么参数呢?唯一的区别就在于 funcall 是直接把参数传递给函数,而 apply 的最后一个参数是一个列表,传入函数的参数把列表进行一次平铺后再传给函数,看下面这个例子就明白了\n(funcall \u0026#39;list \u0026#39;x \u0026#39;(y) \u0026#39;(z)) ; =\u0026gt; (x (y) (z)) (apply \u0026#39;list \u0026#39;x \u0026#39;(y ) \u0026#39;(z)) ; =\u0026gt; (x (y) z) 思考题\n如果一个 list 作为一个树的结构,任何是 cons cell 的元素都是一个内部节点(不允许有 dotted list 出现),任何不是 cons cell 的元素都是树的叶子。请写一个函数,调用的一个类似 mapcar 的函数,调用一个函数遍历树的叶子,并收集所有的结果,返回一个结构相同的树,比如:\n(tree-mapcar \u0026#39;1+ \u0026#39;(1 (2 (3 4)) (5))) ; =\u0026gt; (2 (3 (4 5)) (6)) 宏 前面在已经简单介绍过宏。宏的调用和函数是很类似的,它的求值和函数差不多,但是有一个重要的区别是,宏的参数是出现在最后扩展后的表达式中,而函数参数是求值后才传递给这个函数:\n(defmacro foo (arg) (list \u0026#39;message \u0026#34;%d %d\u0026#34; arg arg)) (defun bar (arg) (message \u0026#34;%d %d\u0026#34; arg arg)) (let ((i 1)) (bar (incf i))) ; =\u0026gt; \u0026#34;2 2\u0026#34; (let ((i 1)) (foo (incf i))) ; =\u0026gt; \u0026#34;2 3\u0026#34; 也许你对前面这个例子 foo 里为什么要用 list 函数很不解。其实宏可以这样看,如果把宏定义作一个表达式来运行,最后把参数用调用时的参数替换,这样就得到了宏调用最后用于求值的表达式。这个过程称为扩展。可以用 macroexpand 函数进行模拟\n(macroexpand \u0026#39;(foo (incf i))) ; =\u0026gt; (message \u0026#34;%d %d\u0026#34; (incf i) (incf i)) 上面用 macroexpand 得到的结果就是用于求值的表达式。\n使用 macroexpand 可以使宏的编写变得容易一些。但是如果不能进行 debug 是很不方便的。在宏定义里可以引入 declare 表达式,它可以增加一些信息。目前只支持两类声明:debug 和 indent。debug 可选择的类型很多,具体参考 info elisp - edebug 一章,一般情况下用 t 就足够了。indent 的类型比较简单,它可以使用这样几种类型:\nnil 也就是一般的方式缩进 defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进 整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1 符号 这个是最坏情况,你要写一个函数自己处理缩进。 看 when 的定义就能知道 declare 如何使用了\n(defmacro when (cond \u0026amp;rest body) (declare (indent 1) (debug t)) (list \u0026#39;if cond (cons \u0026#39;progn body))) 实际上,declare 声明只是设置这个符号的属性列表\n(symbol-plist \u0026#39;when) ; =\u0026gt; (lisp-indent-function 1 edebug-form-spec t) 思考题\n一个比较常用的结构是当 buffer 是可读情况下,绑定 inhibit-read-only 值为 t 来强制插入字符串。请写一个这样的宏,处理好缩进和调用。\n从前面宏 when 的定义可以看出直接使用 list,cons,append 构造宏是很麻烦的。为了使记号简洁,lisp 中有一个特殊的宏 \u0026ldquo;`\u0026quot;,称为 backquote。在这个宏里,所有的表达式都是引起(quote)的,如果要让一个表达式不引起(也就是列表中使用的是表达式的值),需要在前面加 “,”,如果要让一个列表作为整个列表的一部分(slice),可以用 \u0026ldquo;,@\u0026quot;。\n`(a list of ,(+ 2 3) elements) ; =\u0026gt; (a list of 5 elements) (setq some-list \u0026#39;(2 3)) ; =\u0026gt; (2 3) `(1 ,some-list 4 ,@some-list) ; =\u0026gt; (1 (2 3) 4 2 3) 有了这些标记,前面 when 这个宏可以写成\n(defmacro when (cond \u0026amp;rest body) `(if ,cond (progn ,@body))) 值得注意的是这个 backquote 本身就是一个宏,从这里可以看出宏除了减少重复代码这个作用之外的另一个用途:定义新的控制结构,甚至增加新的语法特性。\n命令 emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。lisp 编写的命令都含有一个 interactive 表达式。这个表达式指明了这个命令的参数。比如下面这个命令\n(defun hello-world (name) (interactive \u0026#34;swhat you name? \u0026#34;) (message \u0026#34;hello, %s\u0026#34; name)) 现在你可以用 m-x 来调用这个命令。让我们来看看 interactive 的参数是什么意思。这个字符串的第一个字符(也称为代码字符)代表参数的类型,比如这里 s 代表参数的类型是一个字符串,而其后的字符串是用来提示的字符串。如果这个命令有多个参数,可以在这个提示字符串后使用换行符分开,比如:\n(defun hello-world (name time) (interactive \u0026#34;swhat you name? \\nnwhat the time? \u0026#34;) (message \u0026#34;good %s, %s\u0026#34; (cond ((\u0026lt; time 13) \u0026#34;morning\u0026#34;) ((\u0026lt; time 19) \u0026#34;afternoon\u0026#34;) (t \u0026#34;evening\u0026#34;)) name)) interactive 可以使用的代码字符很多,虽然有一定的规则,比如字符串用 s,数字用 n,文件用 f,区域用 r,但是还是很容易忘记,用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。\n首先学习和代码字符等价的几个函数。s 对应的函数是 read-string。比如\n(read-string \u0026#34;what your name? \u0026#34; user-full-name) n 对应的函数是 read-number,文件对应 read-file-name。很容易记对吧。其实大部分代码字符都是有这样对应的函数或替换的方法(见下表)。\n代码字符 代替的表达式 a (completing-read prompt obarray \u0026lsquo;fboundp t) b (read-buffer prompt nil t) b (read-buffer prompt) c (read-char prompt) c (read-command prompt) d (point) d (read-directory-name prompt) e (read-event) f (read-file-name prompt nil nil t) f (read-file-name prompt) g 暂时不知道和 f 的差别 k (read-key-sequence prompt) k (read-key-sequence prompt nil t) m (mark) n (read-number prompt) n (if current-prefix-arg (prefix-numeric-value current-prefix-arg) (read-number prompt)) p (prefix-numeric-value current-prefix-arg) p current-prefix-arg r (region-beginning) (region-end) s (read-string prompt) s (completing-read prompt obarray nil t) v (read-variable prompt) x (read-from-minibuffer prompt nil nil t) x (eval (read-from-minibuffer prompt nil nil t)) z (read-coding-system prompt) z (and current-prefix-arg (read-coding-system prompt)) 知道这些表达式如何用于 interactive 表达式里呢?简而言之,如果 interactive 的参数是一个表达式,则这个表达式求值后的列表元素对应于这个命令的参数。请看这个例子:\n(defun read-hiden-file (file arg) (interactive (list (read-file-name \u0026#34;choose a hiden file: \u0026#34; \u0026#34;~/\u0026#34; nil nil nil (lambda (name) (string-match \u0026#34;^\\\\.\u0026#34; (file-name-nondirectory name)))) current-prefix-arg)) (message \u0026#34;%s, %s\u0026#34; file arg)) 第一个参数是读入一个以 \u0026ldquo;.\u0026rdquo; 开头的文件名,第二个参数为当前的前缀参数(prefix argument),它可以用 c-u 或 c-u 加数字提供。list 把这两个参数构成一个列表。这就是命令一般的自定义设定参数的方法。\n需要注意的是 current-prefix-arg 这个变量。这个变量当一个命令被调用,它就被赋与一个值,你可以用 c-u 就能改变它的值。在命令运行过程中,它的值始终都存在。即使你的命令不用参数,你也可以访问它\n(defun foo () (interactive) (message \u0026#34;%s\u0026#34; current-prefix-arg)) 用 c-u foo 调用它,你可以发现它的值是 (4)。那为什么大多数命令还单独为它设置一个参数呢?这是因为命令不仅是用户可以调用,很可能其它函数也可以调用,单独设置一个参数可以方便的用参数传递的方法调用这个命令。事实上所有的命令都可以不带参数,而使用前面介绍的方法在命令定义的部分读入需要的参数,但是为了提高命令的可重用性和代码的可读性,还是把参数分离到 interactive 表达式里好。\n从现在开始可能会遇到很多函数,它们的用法有的简单,有的却复杂的要用大段篇幅来解释。我可能就会根据需要来解释一两个函数,就不一一介绍了。自己看 info elisp,用 i 来查找对应的函数。\n思考题\n写一个命令用来切换 major-mode。要求用户输入一个 major-mode 的名字,就切换到这个 major-mode,而且要提供一种补全的办法,去除所有不是 major-mode 的符号,这样用户需要输入少量词就能找到对应的 major-mode。\n函数列表 (functionp object) (apply function \u0026amp;rest arguments) (funcall function \u0026amp;rest arguments) (defmacro name arglist [docstring] [decl] body...) (macroexpand form \u0026amp;optional environment) (declare \u0026amp;rest specs) (` arg) (interactive args) (read-string prompt \u0026amp;optional initial-input history default-value inherit-input-method) (read-file-name prompt \u0026amp;optional dir default-filename mustmatch initial predicate) (completing-read prompt collection \u0026amp;optional predicate require-match initial-input hist def inherit-input-method) (read-buffer prompt \u0026amp;optional def require-match) (read-char \u0026amp;optional prompt inherit-input-method seconds) (read-command prompt \u0026amp;optional default-value) (read-directory-name prompt \u0026amp;optional dir default-dirname mustmatch initial) (read-event \u0026amp;optional prompt inherit-input-method seconds) (read-key-sequence prompt \u0026amp;optional continue-echo dont-downcase-last can-return-switch-frame command-loop) (read-number prompt \u0026amp;optional default) (prefix-numeric-value raw) (read-from-minibuffer prompt \u0026amp;optional initial-contents keymap read hist default-value inherit-input-method) (read-coding-system prompt \u0026amp;optional default-coding-system) 变量列表 current-prefix-arg 问题解答 可选误差的浮点数比较 (defun approx-equal (x y \u0026amp;optional err) (if err (setq err (abs err)) (setq err 1.0e-6)) (or (and (= x 0) (= y 0)) (\u0026lt; (/ (abs (- x y)) (max (abs x) (abs y))) err))) 这个应该是很简单的一个问题。\n遍历树的函数 (defun tree-mapcar (func tree) (if (consp tree) (mapcar (lambda (child) (tree-mapcar func child)) tree) (funcall func tree))) 这个函数可能对于树算法比较熟悉的人一点都不难,就当练手吧。\n宏 with-inhibit-read-only-t (defmacro with-inhibit-read-only-t (\u0026amp;rest body) (declare (indent 0) (debug t)) (cons \u0026#39;let (cons \u0026#39;((inhibit-read-only t)) body))) 如果用 backquote 来改写一个就会发现这个宏会很容易写,而且更容易读了。\n切换 major-mode 的命令 (defvar switch-major-mode-history nil) (defun switch-major-mode (mode) (interactive (list (intern (completing-read \u0026#34;switch to mode: \u0026#34; obarray (lambda (s) (and (fboundp s) (string-match \u0026#34;-mode$\u0026#34; (symbol-name s)))) t nil \u0026#39;switch-major-mode-history)))) (setq switch-major-mode-history (cons (symbol-name major-mode) switch-major-mode-history)) (funcall mode)) 这是我常用的一个命令之一。这个实现也是一个使用 minibuffer 历史的例子。\n正则表达式 如果你不懂正则表达式,而你还想进一步学习编程的话,那你应该停下手边的事情,先学学正则表达式。即使你不想学习编程,也不喜欢编程,学习一点正则表达式,也可能让你的文本编辑效率提高很多。\n在这里,我不想详细介绍正则表达式,因为我觉得这类的文档已经很多了,比我写得好的文章多的是。如果你找不到一个好的入门教程,我建议你不妨看看 perlretut。我想说的是和 emacs 有关的正则表达式的内容,比如,和 perl 正则表达式的差异、语法表格(syntax table)和字符分类(category)等。\n与 perl 正则表达式比较 perl 是文本处理的首选语言。它内置强大而简洁的正则表达式,许多程序也都兼容 perl 的正则表达式。说实话,就简洁而言,我对 emacs 的正则表达式是非常反感的,那么多的反斜线经常让我抓狂。首先,emacs 里的反斜线构成的特殊结构(backslash construct)出现是相当频繁的。在 perl 正则表达式里,()[]{}| 都是特殊字符,而 emacs 它们不是这样。所以它们匹配字符时是不用反斜线,而作为特殊结构时就要用反斜线。而事实上()|作为字符来匹配的情形远远小于作为捕捉字符串和作或运算的情形概率小。而 emacs 的正则表达式又没有 perl 那种简洁的记号,完全用字符串来表示,这样使得一个正则表达式常常一眼看去全是 \\\\。\n到底要用多少个\\? 经常会记不住在 emacs 的正则表达式中应该用几个 \\。有一个比较好的方法,首先想想没有引号引起时的正则表达式是怎样。比如对于特殊字符 $ 要用 \\$,对于反斜线结构是 \\(, \\{,\\| 等等。知道这个怎样写之后,再把所有 \\ 替换成 \\\\,这就是最后写到双引号里形式。所以要匹配一个 \\,应该用 \\\\,而写在引号里就应该用 \\\\\\\\ 来匹配。\nemacs 里匹配的对象不仅包括字符串,还有 buffer,所以有一些对字符串和 buffer 有区分的结构。比如 $ 对于字符串是匹配字符串的末尾,而在 buffer 里是行尾。而 \\' 匹配的是字符串和 buffe 的末尾。对应 ^ 和 ``` 也是这样。\nemacs 对字符有很多种分类方法,在正则表达式里也可以使用。比如按语法类型分类,可以用 \u0026ldquo;\\s\u0026rdquo; 结构匹配一类语法分类的字符,最常用的比如匹配空格的 \\s- 和匹配词的 \\sw(等价于 \\w)。这在 perl 里是没有的。另外 emacs 里字符还对应一个或多个分类(category),比如所有汉字在分类 c 里。这样可以用 \\cc 来匹配一个汉字。这在 perl 里也有类似的分类。除此之外,还有一些预定义的字符分类,可以用 [:class:] 的形式,比如 [:digit:] 匹配 0-9 之间的数,[:ascii:] 匹配所有 ascii 字符等等。在 perl 里只定义几类最常用的字符集,比如 \\d, \\s, \\w,但是我觉得非常实用。比 emacs 这样长的标记好用的多。\n另外在用 [] 表示一个字符集时,emacs 里不能用 \\ 进行转义,事实上 \\ 在这里不是一个特殊字符。所以 emacs 里的处理方法是,把特殊字符提前或放在后面,比如如果要在字符集里包括 ] 的话,要把 ] 放在第一位。而如果要包括 -,只能放在最后一位,如果要包括 ^ 不能放在第一位。如果只想匹配一个 ^,就只能用 \\^ 的形式。比较拗口,希望下面这个例子能帮你理解\n(let ((str \u0026#34;abc]-^]123\u0026#34;)) (string-match \u0026#34;[]^0-9-]+\u0026#34; str) (match-string 0 str)) ; =\u0026gt; \u0026#34;]-^]123\u0026#34; 最后提示一下,emacs 提供一个很好的正则表达式调试工具:m-x re-builder。它能显示 buffer 匹配正则表达式的字符串,并用不同颜色显示捕捉的字符串。\n语法表格和分类表格简介 语法表格指的是 emacs 为每个字符都指定了语法功能,这为解析函数,复杂的移动命令等等提供了各种语法结构的起点和终点。语法表使用的数据结构是一种称为字符表(char-table)的数组,它能以字符作为下标(还记得 emacs 里的字符就是整数吗)来得到对应的值。语法表里一个字符对应一个语法分类的列表。每一个分类都有一个助记字符(mnemonic character)。一共有哪几类分类呢?\n名称 助记符 说明 空白(whitespace) - 或 \u0026rsquo; ' 词(word) w 符号(symbol) _ 这是除 word 之外其它用于变量和命令名的字符。 标点(punctuation) . open 和 close ( 和 ) 一般是括号 ()[]{} 字符串引号(string quote) \u0026quot; 转义符(escape-syntax) \\ 用于转义序列,比如 c 和 lisp 字符串中的 \\。 字符引号(character quote) / paired delimiter $ 只有 tex 模式中使用 expression prefix ' 注释开始和注释结束 \u0026lt; 和 \u0026gt; inherit standard syntax @ generic comment delimiter ! 语法表格可以继承,所以基本上所有语法表格都是从 standard-syntax-table 继承而来,作少量修改,加上每个模式特有的语法构成就行了。一般来说记住几类重要的分类就行了,比如,空白包括空格,制表符,换行符,换页符。词包括所有的大小写字母,数字。符号一般按使用的模式而定,比如 c 中包含 _,而 lisp 中是 $\u0026amp;*+-_\u0026lt;\u0026gt;。可以用 m-x describe-syntax 来查看所有字符的语法分类。\n字符分类(category)是另一种分类方法,每个分类都有一个名字,对应一个从 ``到 ~ 的 ascii 字符。可以用 m-x describe-categories 查看所有字符的分类。每一种分类都有说明,我就不详细解释了。\n几个常用的函数 如果你要匹配的字符串中含有很多特殊字符,而你又想用正则表达式进行匹配,可以使用 regexp-quote 函数,它可以让字符串中的特殊字符自动转义。\n一般多个可选词的匹配可以用或运算连接起来,但是这有两个不好的地方,一是要写很长的正则表达式,还含有很多反斜线,不好看,容易出错,也不好修改,二是效率很低。这时可以使用 regexp-opt 还产生一个更好的正则表达式\n(regexp-opt \u0026#39;(\u0026#34;foobar\u0026#34; \u0026#34;foobaz\u0026#34; \u0026#34;foo\u0026#34;)) ; =\u0026gt; \u0026#34;foo\\\\(?:ba[rz]\\\\)?\u0026#34; 函数列表 (regexp-quote string) (regexp-opt strings \u0026amp;optional paren) 命令列表 describe-syntax\rdescribe-categories 操作对象之一 \u0026ndash; 缓冲区 缓冲区(buffer)是用来保存要编辑文本的对象。通常缓冲区都是和文件相关联的,但是也有很多缓冲区没有对应的文件。emacs 可以同时打开多个文件,也就是说能同时有很多个缓冲区存在,但是在任何时候都只有一个缓冲区称为当前缓冲区(current buffer)。即使在 lisp 编程中也是如此。许多编辑和移动的命令只能针对当前缓冲区。\n缓冲区的名字 emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。下面的函数列表中如果参数是 buffer-or-name 则是能同时接受缓冲区对象和缓冲区名的函数,否则只能接受一种参数。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。\n要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 m-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 \u0026lt;序号\u0026gt; 的方式使名字变得不同。你也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名。\n当前缓冲区 当前缓冲区可以用 current-buffer 函数得到。当前缓冲区不一定是显示在屏幕上的那个缓冲区,你可以用 set-buffer 来指定当前缓冲区。但是需要注意的是,当命令返回到命令循环时,光标所在的缓冲区 会自动成为当前缓冲区。这也是单独在 *scratch* 中执行 set-buffer 后并不能改变当前缓冲区,而必须使用 progn 语句同时执行多个语句才能改变当前缓冲区的原因\n(set-buffer \u0026#34;*messages*\u0026#34;) ; =\u0026gt; #\u0026lt;buffer *messages*\u0026gt; (message (buffer-name)) ; =\u0026gt; \u0026#34;*scratch*\u0026#34; (progn (set-buffer \u0026#34;*messages*\u0026#34;) (message (buffer-name))) ; \u0026#34;*messages*\u0026#34; 但是你不能依赖命令循环来把当前缓冲区设置成使用 set-buffer 之前的。因为这个命令很可以会被另一个程序员来调用。你也不能直接用 set-buffer 设置成原来的缓冲区,比如\n(let (buffer-read-only (obuf (current-buffer))) (set-buffer ...) ... (set-buffer obuf)) 因为 set-buffer 不能处理错误或退出情况。正确的作法是使用 save-current-buffer、with-current-buffer 和 save-excursion 等方法。save-current-buffer 能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区。如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区。lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句。比如 with-current-buffer 使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区\n(with-current-buffer buffer-or-name body) 相当于\n(save-current-buffer (set-buffer buffer-or-name) body) save-excursion 与 save-current-buffer 不同之处在于,它不仅保存当前缓冲区,还保存了当前的位置和 mark。在 *scratch* 缓冲区中运行下面两个语句就能看出它们的差别了\n(save-current-buffer (set-buffer \u0026#34;*scratch*\u0026#34;) (goto-char (point-min)) (set-buffer \u0026#34;*messages*\u0026#34;)) (save-excursion (set-buffer \u0026#34;*scratch*\u0026#34;) (goto-char (point-min)) (set-buffer \u0026#34;*messages*\u0026#34;)) 创建和关闭缓冲区 产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 \u0026lt;n\u0026gt;(n 是一个整数,从 2 开始) 的名字创建新的缓冲区。\n关闭一个缓冲区可以用 kill-buffer。当关闭缓冲区时,如果要用户确认是否要关闭缓冲区,可以加到 kill-buffer-query-functions 里。如果要做一些善后处理,可以用 kill-buffer-hook。\n通常一个接受缓冲区作为参数的函数都需要参数所指定的缓冲区是存在的。如果要确认一个缓冲区是否依然还存在可以使用 buffer-live-p。\n要对所有缓冲区进行某个操作,可以用 buffer-list 获得所有缓冲区的列表。\n如果你只是想使用一个临时的缓冲区,而不想先建一个缓冲区,使用结束后又需要关闭这个缓冲区,可以用 with-temp-buffer 这个宏。从这个宏的名字可以看出,它所做的事情是先新建一个临时缓冲区,并把这个缓冲区作为当前缓冲区,使用结束后,关闭这个缓冲区,并恢复之前的缓冲区为当前缓冲区。\n在缓冲区内移动 在学会移动函数之前,先要理解两个概念:位置(position)和标记(mark)。位置是指某个字符在缓冲区内的下标,它从 1 开始。更准确的说位置是在两个字符之间,所以有在位置之前的字符和在位置之后的字符之说。但是通常我们说在某个位置的字符都是指在这个位置之后的字符。\n标记和位置的区别在于位置会随文本插入和删除而改变位置。一个标记包含了缓冲区和位置两个信息。在插入和删除缓冲区里的文本时,所有的标记都会检查一遍,并重新设置位置。这对于含有大量标记的缓冲区处理是很花时间的,所以当你确认某个标记不用的话应该释放这个标记。\n创建一个标记使用函数 make-marker。这样产生的标记不会指向任何地方。你需要用 set-marker 命令来设置标记的位置和缓冲区\n(setq foo (make-marker)) ; =\u0026gt; #\u0026lt;marker in no buffer\u0026gt; (set-marker foo (point)) ; =\u0026gt; #\u0026lt;marker at 3594 in *scratch*\u0026gt; 也可以用 point-marker 直接得到 point 处的标记。或者用 copy-marker 复制一个标记或者直接用位置生成一个标记\n(point-marker) ; =\u0026gt; #\u0026lt;marker at 3516 in *scratch*\u0026gt; (copy-marker 20) ; =\u0026gt; #\u0026lt;marker at 20 in *scratch*\u0026gt; (copy-marker foo) ; =\u0026gt; #\u0026lt;marker at 3502 in *scratch*\u0026gt; 如果要得一个标记的内容,可以用 marker-position,marker-buffer\n(marker-position foo) ; =\u0026gt; 3502 (marker-buffer foo) ; =\u0026gt; #\u0026lt;buffer *scratch*\u0026gt; 位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。\n和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 point。point 是一个特殊的缓冲区位置,许多命令在这个位置进行文本插入。每个缓冲区都有一个 point 值,它总是比函数 point-min 大,比另一个函数 point-max 返回值小。注意,point-min 的返回值不一定是 1,point-max 的返回值也不定是比缓冲区大小函数 buffer-size 的返回值大 1 的数,因为 emacs 可以把一个缓冲区缩小(narrow)到一个区域,这时 point-min 和 point-max 返回值就是这个区域的起点和终点位置。所以要得到 point 的范围,只能用这两个函数,而不能用 1 和 buffer-size 函数。\n和 point 类似,有一个特殊的标记称为 \u0026ldquo;the mark\u0026rdquo;。它指定了一个区域的文本用于某些命令,比如 kill-region,indent-region。可以用 mark 函数返回当前 mark 的值。如果使用 transient-mark-mode,而且 mark-even-if-inactive 值是 nil 的话,在 mark 没有激活时(也就是 mark-active 的值为 nil),调用 mark 函数会产生一个错误。如果传递一个参数 force 才能返回当前缓冲区 mark 的位置。mark-marker 能返回当前缓冲区的 mark,这不是 mark 的拷贝,所以设置它的值会改变当前 mark 的值。set-mark 可以设置 mark 的值,并激活 mark。每个缓冲区还维护一个 mark-ring,这个列表里保存了 mark 的前一个值。当一个命令修改了 mark 的值时,通常要把旧的值放到 mark-ring 里。可以用 push-mark 和 pop-mark 加入或删除 mark-ring 里的元素。当缓冲区里 mark 存在且指向某个位置时,可以用 region-beginning 和 region-end 得到 point 和 mark 中较小的和较大的值。当然如果使用 transient-mark-mode 时,需要激活 mark,否则会产生一个错误。\n思考题\n写一个命令,对于使用 transient-mark-mode 时,当选中一个区域时显示区域 的起点和终点,否则显示 point-min 和 point-max 的位置。如果不使用 transient-mark-mode,则显示 point 和 mark 的位置。\n按单个字符位置来移动的函数主要使用 goto-char 和 forward-char、backward-char。前者是按缓冲区的绝对位置移动,而后者是按 point 的偏移位置移动比如\n(goto-char (point-min)) ; 跳到缓冲区开始位置 (forward-char 10) ; 向前移动 10 个字符 (forward-char -10) ; 向后移动 10 个字符 可能有一些写 elisp 的人没有读文档或者贪图省事,就在写的 elisp 里直接用 beginning-of-buffer 和 end-of-buffer 来跳到缓冲区的开头和末尾,这其实是不对的。因为这两个命令还做了其它事情,比如设置标记等等。同样,还有一些函数都是不推荐在 elisp 中使用的,如果你准备写一个要发布 elisp,还是要注意一下。\n按词移动使用 forward-word 和 backward-word。至于什么是词,这就要看语法表格的定义了。\n按行移动使用 forward-line。没有 backward-line。forward-line 每次移动都是移动到行首的。所以,如果要移动到当前行的行首,使用 (forward-line 0)。如果不想移动就得到行首和行尾的位置,可以用 line-beginning-position 和 line-end-position。得到当前行的行号可以用 line-number-at-pos。需要注意的是这个行号是从当前状态下的行号,如果使用 narrow-to-region 或者用 widen 之后都有可能改变行号。\n由于 point 只能在 point-min 和 point-max 之间,所以 point 位置测试有时是很重要的,特别是在循环条件测试里。常用的测试函数是 bobp(beginning of buffer predicate)和 eobp(end of buffer predicate)。对于行位置测试使用 bolp(beginning of line predicate)和 eolp(end of line predicate)。\n缓冲区的内容 要得到整个缓冲区的文本,可以用 buffer-string 函数。如果只要一个区间的文本,使用 buffer-substring 函数。point 附近的字符可以用 char-after 和 char-before 得到。point 处的词可以用 current-word 得到,其它类型的文本,比如符号,数字,s 表达式等等,可以用 thing-at-point 函数得到。\n思考题\n参考 thing-at-point 写一个命令标记光标处的 s 表达式。这个命令和 mark-sexp 不同的是,它能从整个 s 表达式的开始标记。\n修改缓冲区的内容 要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。\n插入文本最常用的命令是 insert。它可以插入一个或者多个字符串到当前缓冲区的 point 后。也可以用 insert-char 插入单个字符。插入另一个缓冲区的一个区域使用 insert-buffer-substring。\n删除一个或多个字符使用 delete-char 或 delete-backward-char。删除一个区间使用 delete-region。如果既要删除一个区间又要得到这部分的内容使用 delete-and-extract-region,它返回包含被删除部分的字符串。\n最常用的查找函数是 re-search-forward 和 re-search-backward。这两个函数参数如下\n(re-search-forward regexp \u0026amp;optional bound noerror count) (re-search-backward regexp \u0026amp;optional bound noerror count) 其中 bound 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward),noerror 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil。count 是指定查找匹配的次数。\n替换一般都是在查找之后进行,也是使用 replace-match 函数。和字符串的替换不同的是不需要指定替换的对象了。\n思考题\n从 openoffice 字处理程序里拷贝到 emacs 里的表格通常都是每一个单元格就是一行的。写一个命令,让用户输入表格的列数,把选中区域转换成用制表符分隔的表格。\n函数列表 (buffer-name \u0026amp;optional buffer) (rename-buffer newname \u0026amp;optional unique) (generate-new-buffer-name name \u0026amp;optional ignore) (current-buffer) (set-buffer buffer-or-name)) (save-current-buffer \u0026amp;rest body) (with-current-buffer buffer-or-name \u0026amp;rest body) (save-excursion \u0026amp;rest body) (get-buffer-create name) (generate-new-buffer name) (kill-buffer buffer-or-name) (buffer-live-p object) (buffer-list \u0026amp;optional frame) (with-temp-buffer \u0026amp;rest body) (make-marker) (set-marker marker position \u0026amp;optional buffer) (point-marker) (copy-marker marker \u0026amp;optional type) (marker-position marker) (marker-buffer marker) (point) (point-min) (point-max) (buffer-size \u0026amp;optional buffer) (mark \u0026amp;optional force) (mark-marker) (set-mark pos) (push-mark \u0026amp;optional location nomsg activate) (pop-mark) (region-beginning) (region-end) (goto-char position) (forward-char \u0026amp;optional n) (backward-char \u0026amp;optional n) (beginning-of-buffer \u0026amp;optional arg) (end-of-buffer \u0026amp;optional arg) (forward-word \u0026amp;optional arg) (backward-word \u0026amp;optional arg) (forward-line \u0026amp;optional n) (line-beginning-position \u0026amp;optional n) (line-end-position \u0026amp;optional n) (line-number-at-pos \u0026amp;optional pos) (narrow-to-region start end) (widen) (bobp) (eobp) (bolp) (eolp) (buffer-string) (buffer-substring start end) (char-after \u0026amp;optional pos) (char-before \u0026amp;optional pos) (current-word \u0026amp;optional strict really-word) (thing-at-point thing) (insert \u0026amp;rest args) (insert-char character count \u0026amp;optional inherit) (insert-buffer-substring buffer \u0026amp;optional start end) (delete-char n \u0026amp;optional killflag) (delete-backward-char n \u0026amp;optional killflag) (delete-region start end) (delete-and-extract-region start end) (re-search-forward regexp \u0026amp;optional bound noerror count) (re-search-backward regexp \u0026amp;optional bound noerror count) 问题解答 可选择区域也可不选择区域的命令 (defun show-region (beg end) (interactive (if (or (null transient-mark-mode) mark-active) (list (region-beginning) (region-end)) (list (point-min) (point-max)))) (message \u0026#34;region start from %d to %d\u0026#34; beg end)) 这是通常那种如果选择区域则对这个区域应用命令,否则对整个缓冲区应用命令的方法。我喜欢用 transient-mark-mode,因为它让这种作用于区域的命令更灵活。当然也有人反对,无所谓了,emacs 本身就是很个性化的东西。\n标记整个 s 表达式 (defun mark-whole-sexp () (interactive) (let ((bound (bounds-of-thing-at-point \u0026#39;sexp))) (if bound (progn (goto-char (car bound)) (set-mark (point)) (goto-char (cdr bound))) (message \u0026#34;no sexp found at point!\u0026#34;)))) 学习过程中应该可以看看其它一些函数是怎样实现的,从这些源代码中常常能学到很多有用的技巧和方法。比如要标记整个 s 表达式,联想到 thing-at-point 能得到整个 s 表达式,那自然能得到整个 s 表达式的起点和终点了。所以看看 thing-at-point 的实现,一个很简单的函数,一眼就能发现其中最关键的函数是 bounds-of-thing-at-point,它能返回某个语法实体(syntactic entity)的起点和终点。这样这个命令就很容易就能写出来了。从这个命令中还应该注意到的是对于错误应该很好的处理,让用户明白发生什么错了。比如这里,如果当前光标下没有 s 表达式时,bound 变量为 nil,如果不进行判断,会出现错误:\nwrong type argument: integer-or-marker-p, nil 加上这个判断,用户就明白发生什么事了。\noowriter 表格转换 实现这个目的有多种方法:\n一行一行移动,删除回车,替换成制表符 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (setq col (1- col)) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (dotimes (i col) (forward-line 1) (backward-delete-char 1) (insert-char ?\\t 1)) (forward-line 1))))) 用 subst-char-in-region 函数直接替换 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (subst-char-in-region (point) (progn (forward-line col) (1- (point))) ?\\n ?\\t))))) 用 re-search-forward 和 replace-match 查找替 (defun oowrite-table-convert (col beg end) (interactive \u0026#34;ncolumns of table: \\nr\u0026#34;) (let (start bound) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (setq start (point)) (forward-line col) (setq bound (copy-marker (1- (point)))) (goto-char start) (while (re-search-forward \u0026#34;\\n\u0026#34; bound t) (replace-match \u0026#34;\\t\u0026#34;)) (goto-char (1+ bound))))))) 之所以要给出这三种方法,是想借此说明 elisp 编程其实要实现一个目的通常有 很多种方法,选择一种适合的方法。比如这个问题较好的方法是使用第二种方法, 前提是你要知道有 subst-char-in-region 这个函数,这就要求你对 emacs 提供 的内置的函数比较熟悉了,没有别的办法,只有自己多读 elisp manual,如果你 真想学习 elisp 的话,读 manual 还是值得的,我每读一遍都会有一些新的发 现。如果你不知道这个函数,只知道常用的函数,那么相比较而言,第一种方法 是比较容易想到,也比较容易实现的。但是事实上第三种方法才是最重要的方法, 因为这个方法是适用范围最广的。试想一下你如果不是替换两个字符,而是字符 串的话,前面两种方法都没有办法使用了,而这个方法只要稍微修改就能用了。\n另外,需要特别说明的是这个命令中 bound 使用的是一个标记而不是一个位置, 如果替换的字符串和删除的字符串是相同长度的,当前用什么都可以,否则就要 注意了,因为在替换之后,边界就有可能改变。这也是写查找替换的函数中很容 易出现的一个错误。解决的办法,一是像我这样用一个标记来记录边界位置。另 一种就是用 narrow-to-region 的方法,先把缓冲区缩小到查找替换的区域,结 束后用 widen 展开。当然为了省事,可以直接用 save-restriction。\n操作对象之二 \u0026ndash; 窗口 首先还是要定义一下什么是窗口(window)。窗口是屏幕上用于显示一个缓冲区 的部分。和它要区分开来的一个概念是 frame。frame 是 emacs 能够使用屏幕的 部分。可以用窗口的观点来看 frame 和窗口,一个 frame 里可以容纳多个(至 少一个)窗口,而 emacs 可以有多个 frame。(可能需要和通常所说的窗口的概 念要区分开来,一般来说,我们所说的其它程序的窗口更类似于 emacs 的一个 frame,所以也有人认为这里 window 译为窗格更好一些。但是窗格这个词是一个 生造出来的词,我还是用窗口比较顺一些,大家自己注意就行了。)在任何时候, 都有一个被选中的 frame,而在这个 frame 里有一个被选中的窗口,称为选择的 窗口(selected window)。\n分割窗口 刚启动时,emacs 都是只有一个 frame 一个窗口。多个窗口都是用分割窗口的函 数生成的。分割窗口的内建函数是 split-window。这个函数的参数如下:\n(split-window \u0026amp;optional window size horizontal) 这个函数的功能是把当前或者指定窗口进行分割,默认分割方式是水平分割,可 以将参数中的 horizontal 设置为 non-nil 的值,变成垂直分割。如果不指定 大小,则分割后两个窗口的大小是一样的。分割后的两个窗口里的缓冲区是同 一个缓冲区。使用这个函数后,光标仍然在原窗口,而返回的新窗口对象:\n(selected-window) ; =\u0026gt; #\u0026lt;window 136 on *scratch*\u0026gt; (split-window) ; =\u0026gt; #\u0026lt;window 138 on *scratch*\u0026gt; 需要注意的是,窗口的分割也需要用树的结构来看分割后的窗口,比如这样一个过程:\n+---------------+ +---------------+\r| | | | |\r| win1 | | win1 | win2 |\r| | --\u0026gt; | | |\r| | | | |\r+---------------+ +---------------+\r|\rv\r+---------------+ +---------------+\r| win1 | | | | |\r| | win2 | | win1 | win2 |\r|--------| | \u0026lt;-- |-------| |\r| 3 | 4 | | | win3 | |\r| | | | | | |\r+---------------+ +---------------+ 可以看成是这样一种结构:\n(win1) -\u0026gt; (win1 win2) -\u0026gt; ((win1 win3) win2) -\u0026gt; ((win1 (win3 win4)) win2) 事实上可以用 window-tree 函数得到当前窗口的结构,如果忽略 minibuffer 对应的窗口,得到的应该类似这样的一个结果:\n(nil (0 0 170 42) (t (0 0 85 42) #\u0026lt;win 3\u0026gt; (nil (0 21 85 42) #\u0026lt;win 8\u0026gt; #\u0026lt;win 10\u0026gt;)) #\u0026lt;win 6\u0026gt;) window-tree 返回值的第一个元素代表子窗口的分割方式,nil 表示水平分割, t 表示垂直分割。第二个元素代表整个结构的大小,这四个数字可以看作是左上 和右下两个顶点的坐标。其余元素是子窗口。每个子窗口也是同样的结构。所以 把前面这个列表还原成窗口排列应该是这样:\n(0,0) +-------------------+\r| | |\r| win 3 | win6 |\r| | |\r(0,21) |---------| |\r| | | |\r| 8 | 10 | |\r| | | |\r+-------------------+ (170, 42)\r(85, 42) 由上面的图可以注意到由 window-tree 返回的结果一些窗口的大小不能确定, 比较上面的 win 8 和 win 10 只能知道它们合并起来的大小,不能确定它们分 别的宽度是多少。\n删除窗口 如果要让一个窗口不显示在屏幕上,要使用 delete-window 函数。如果没有指定 参数,删除的窗口是当前选中的窗口,如果指定了参数,删除的是这个参数对应 的窗口。删除的窗口多出来的空间会自动加到它的邻接的窗口中。如果要删除除 了当前窗口之外的窗口,可以用 delete-other-windows 函数。\n当一个窗口不可见之后,这个窗口对象也就消失了\n(setq foo (selected-window)) ; =\u0026gt; #\u0026lt;window 90 on *scratch*\u0026gt; (delete-window) (windowp foo) ; =\u0026gt; t (window-live-p foo) ; =\u0026gt; nil 窗口配置 窗口配置 (window configuration) 包含了 frame 中所有窗口的位置信息:窗口 大小,显示的缓冲区,缓冲区中光标的位置和 mark,还有 fringe,滚动条等等。 用 current-window-configuration 得到当前窗口配置,用 set-window-configuration 来还原\n(setq foo (current-window-configuration)) ;; do sth to make some changes on windows (set-window-configuration foo) 选择窗口 可以用 selected-window 得到当前光标所在的窗口\n(selected-window) ; =\u0026gt; #\u0026lt;window 104 on *scratch*\u0026gt; 可以用 select-window 函数使某个窗口变成选中的窗口\n(progn (setq foo (selected-window)) (message \u0026#34;original window: %s\u0026#34; foo) (other-window 1) (message \u0026#34;current window: %s\u0026#34; (selected-window)) (select-window foo) (message \u0026#34;back to original window: %s\u0026#34; foo)) 两个特殊的宏可以保存窗口位置执行语句:save-selected-window 和 with-selected-window。它们的作用是在执行语句结束后选择的窗口仍留在执行 语句之前的窗口。with-selected-window 和 save-selected-window 几乎相同, 只不过 save-selected-window 选择了其它窗口。这两个宏不会保存窗口的位置 信息,如果执行语句结束后,保存的窗口已经消失,则会选择最后一个选择的窗 口。\n;; 让另一个窗口滚动到缓冲区开始 (save-selected-window (select-window (next-window)) (goto-char (point-min))) 当前 frame 里所有的窗口可以用 window-list 函数得到。可以用 next-window 来得到在 window-list 里排在某个 window 之后的窗口。对应的用 previous-window 得到排在某个 window 之前的窗口。\n(selected-window) ; =\u0026gt; #\u0026lt;window 245 on *scratch*\u0026gt; (window-list) ;; =\u0026gt; (#\u0026lt;window 245 on *scratch*\u0026gt; #\u0026lt;window 253 on *scratch*\u0026gt; #\u0026lt;window 251 on *info*\u0026gt;) (next-window) ; =\u0026gt; #\u0026lt;window 253 on *scratch*\u0026gt; (next-window (next-window)) ; =\u0026gt; #\u0026lt;window 251 on *info*\u0026gt; (next-window (next-window (next-window))) ; =\u0026gt; #\u0026lt;window 245 on *scratch*\u0026gt; walk-windows 可以遍历窗口,相当于 (mapc proc (window-list))。 get-window-with-predicate 用于查找符合某个条件的窗口。\n窗口大小信息 窗口是一个长方形区域,所以窗口的大小信息包括它的高度和宽度。用来度量窗 口大小的单位都是以字符数来表示,所以窗口高度为 45 指的是这个窗口可以容 纳 45 行字符,宽度为 140 是指窗口一行可以显示 140 个字符。\nmode line 和 header line 都包含在窗口的高度里,所以有 window-height 和 window-body-height 两个函数,后者返回把 mode-line 和 header line 排除后 的高度。\n(window-height) ; =\u0026gt; 45 (window-body-height) ; =\u0026gt; 44 滚动条和 fringe 不包括在窗口的亮度里,window-width 返回窗口的宽度\n(window-width) ; =\u0026gt; 72 也可以用 window-edges 返回各个顶点的坐标信息\n(window-edges) ; =\u0026gt; (0 0 73 45) window-edges 返回的位置信息包含了滚动条、fringe、mode line、header line 在内,window-inside-edges 返回的就是窗口的文本区域的位置\n(window-inside-edges) ; =\u0026gt; (1 0 73 44) 如果需要的话也可以得到用像素表示的窗口位置信息\n(window-pixel-edges) ; =\u0026gt; (0 0 511 675) (window-inside-pixel-edges) ; =\u0026gt; (7 0 511 660) 思考题\ncurrent-window-configuration 可以将当前窗口的位置信 息保存到一个变量中以便将来恢复窗口。但是这个对象没有读入形式,所以不 能保存到文件中。请写一个函数可以把当前窗口的位置信息生成一个列表,然 后用一个函数就能从这个列表恢复窗口。提示:这个列表结构用窗口的分割顺 序表示。比如用这样一个列表表示对应的窗口:\n;; +---------------+\r;; | | | |\r;; |-------| |\r;; | | |\r;; +---------------+\r;; =\u0026gt;\r(horizontal 73\r(vertical 22\r(horizontal 36 win win)\rwin)\rwin) 窗口对应的缓冲区 窗口对应的缓冲区可以用 window-buffer 函数得到:\n(window-buffer) ; =\u0026gt; #\u0026lt;buffer *scratch*\u0026gt; (window-buffer (next-window)) ; =\u0026gt; #\u0026lt;buffer *info*\u0026gt; 缓冲区对应的窗口也可以用 get-buffer-window 得到。如果有多个窗口显示同一 个缓冲区,那这个函数只能返回其中的一个,由 window-list 决定。如果要得到 所有的窗口,可以用 get-buffer-window-list\n(get-buffer-window (get-buffer \u0026#34;*scratch*\u0026#34;)) ;; =\u0026gt; #\u0026lt;window 268 on *scratch*\u0026gt; (get-buffer-window-list (get-buffer \u0026#34;*scratch*\u0026#34;)) ;; =\u0026gt; (#\u0026lt;window 268 on *scratch*\u0026gt; #\u0026lt;window 270 on *scratch*\u0026gt;) 让某个窗口显示某个缓冲区可以用 set-window-buffer 函数。 让选中窗口显示某个缓冲区也可以用 switch-to-buffer,但是一般不要在 elisp 编程中用这个命令,如果需要让某个缓冲区成为当前缓冲区使用 set-buffer 函数,如果要让当前窗口显示某个缓冲区,使用 set-window-buffer 函数。\n让一个缓冲区可见可以用 display-buffer。默认的行为是当缓冲区已经显示在某 个窗口中时,如果不是当前选中窗口,则返回那个窗口,如果是当前选中窗口, 且如果传递的 not-this-window 参数为 non-nil 时,会新建一个窗口,显示缓 冲区。如果没有任何窗口显示这个缓冲区,则新建一个窗口显示缓冲区,并返回 这个窗口。display-buffer 是一个比较高级的命令,用户可以通过一些变量来改 变这个命令的行为。比如控制显示的 pop-up-windows, display-buffer-reuse-frames,pop-up-frames,控制新建窗口高度的 split-height-threshold,even-window-heights,控制显示的 frame 的 special-display-buffer-names,special-display-regexps, special-display-function,控制是否应该显示在当前选中窗口 same-window-buffer-names,same-window-regexps 等等。如果这些还不能满 足你的要求(事实上我觉得已经太复杂了),你还可以自己写一个函数,将 display-buffer-function 设置成这个函数。\n思考题\n前一个思考题只能还原窗口,不能还原缓冲区。请修改一下使它能保存缓冲区信息,还原时让对应的窗口显示对应的缓冲区。\n改变窗口显示区域 每个窗口会保存一个显示缓冲区的起点位置,这个位置对应于窗口左上角光标在 缓冲区里的位置。可以用 window-start 函数得到某个窗口的起点位置。可以通 过 set-window-start 来改变显示起点位置。可以通过 pos-visible-in-window-p 来检测缓冲区中某个位置是否是可见的。 但是直接通过 set-window-start 来控制显示比较容易出现错误,因为 set-window-start 并不会改变 point 所在的位置,在窗口调用 redisplay 函 数之后 point 会跳到相应的位置。如果你确实有这个需要,我建议还是用: (with-selected-window window (goto-char pos)) 来代替。\n函数列表 (windowp object) (split-window \u0026amp;optional window size horflag) (selected-window) (window-tree \u0026amp;optional frame) (delete-window \u0026amp;optional window) (delete-other-windows \u0026amp;optional window) (current-window-configuration \u0026amp;optional frame) (set-window-configuration configuration) (other-window arg \u0026amp;optional all-frames) (save-selected-window \u0026amp;rest body) (with-selected-window window \u0026amp;rest body) (window-list \u0026amp;optional frame minibuf window) (next-window \u0026amp;optional window minibuf all-frames) (previous-window \u0026amp;optional window minibuf all-frames) (walk-windows proc \u0026amp;optional minibuf all-frames) (get-window-with-predicate predicate \u0026amp;optional minibuf all-frames default) (window-height \u0026amp;optional window) (window-body-height \u0026amp;optional window) (window-width \u0026amp;optional window) (window-edges \u0026amp;optional window) (window-inside-edges \u0026amp;optional window) (window-pixel-edges \u0026amp;optional window) (window-inside-pixel-edges \u0026amp;optional window) (window-buffer \u0026amp;optional window) (get-buffer-window buffer-or-name \u0026amp;optional frame) (get-buffer-window-list buffer-or-name \u0026amp;optional minibuf frame) (set-window-buffer window buffer-or-name \u0026amp;optional keep-margins) (switch-to-buffer buffer-or-name \u0026amp;optional norecord) (display-buffer buffer-or-name \u0026amp;optional not-this-window frame) (window-start \u0026amp;optional window) (set-window-start window pos \u0026amp;optional noforce) 问题解答 保存窗口位置信息 这是我的答案。欢迎提出改进意见\n(defun my-window-tree-to-list (tree) (if (windowp tree) \u0026#39;win (let ((dir (car tree)) (children (cddr tree))) (list (if dir \u0026#39;vertical \u0026#39;horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (\u0026gt; (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-window-width (win) (if (windowp win) (window-width win) (let ((edge (cadr win))) (- (nth 2 edge) (car edge))))) (defun my-window-height (win) (if (windowp win) (window-height win) (let ((edge (cadr win))) (- (nth 3 edge) (cadr edge))))) (defun my-list-to-window-tree (conf) (when (listp conf) (let (newwin) (setq newwin (split-window nil (cadr conf) (eq (car conf) \u0026#39;horizontal))) (my-list-to-window-tree (nth 2 conf)) (select-window newwin) (my-list-to-window-tree (nth 3 conf))))) (defun my-restore-window-configuration (winconf) (delete-other-windows) (my-list-to-window-tree winconf)) (defun my-current-window-configuration () (my-window-tree-to-list (car (window-tree)))) ;; test code here (setq foo (my-current-window-configuration)) ;; do sth to change windows (my-restore-window-configuration foo) 改进的保存窗口信息的函数 由于缓冲区对象也是没有读入形式的,所以返回的列表里只能用缓冲区名来代表 缓冲区,只要没有修改过缓冲区的名字,就能正确的还原缓冲区。如果对于访问 文件的缓冲区,使用文件名可能是更好的想法。保存信息只要对 my-window-tree-to-list 函数做很小的修改就能用了。而恢复窗口则要做较大 改动。my-list-to-window-tree 加了一个函数参数,这样这个函数的可定制性 更高一些。\n(defun my-window-tree-to-list (tree) (if (windowp tree) (buffer-name (window-buffer tree)) (let ((dir (car tree)) (children (cddr tree))) (list (if dir \u0026#39;vertical \u0026#39;horizontal) (if dir (my-window-height (car children)) (my-window-width (car children))) (my-window-tree-to-list (car children)) (if (\u0026gt; (length children) 2) (my-window-tree-to-list (cons dir (cons nil (cdr children)))) (my-window-tree-to-list (cadr children))))))) (defun my-list-to-window-tree (conf set-winbuf) (let ((newwin (split-window nil (cadr conf) (eq (car conf) \u0026#39;horizontal)))) (if (eq (car conf) \u0026#39;horizontal) (progn (funcall set-winbuf (selected-window) (nth 2 conf)) (select-window newwin) (if (listp (nth 3 conf)) (my-list-to-window-tree (nth 3 conf)) (funcall set-winbuf newwin (nth 3 conf)))) (if (listp (nth 2 conf)) (my-list-to-window-tree (nth 2 conf)) (funcall set-winbuf (selected-window) (nth 2 conf))) (select-window newwin) (funcall set-winbuf newwin (nth 3 conf))))) (defun my-restore-window-configuration (winconf) (let ((buf (current-buffer))) (delete-other-windows) (my-list-to-window-tree winconf (lambda (win name) (set-window-buffer win (or (get-buffer name) buf)))))) 操作对象之三 \u0026ndash; 文件 作为一个编辑器,自然文件是最重要的操作对象之一。这一节要介绍有关文件的一系列命令,比如查找文件,读写文件,文件信息、读取目录、文件名操作等。\n打开文件的过程 当你打开一个文件时,实际上 emacs 做了很多事情:\n把文件名展开成为完整的文件名 判断文件是否存在 判断文件是否可读或者文件大小是否太大 查看文件是否已经打开,是否被锁定 向缓冲区插入文件内容 设置缓冲区的模式 这还只是简单的一个步骤,实际情况比这要复杂的多,许多异常需要考虑。而且 为了所有函数的可扩展性,许多变量、handler 和 hook 加入到文件操作的函数 中,使得每一个环节都可以让用户或者 elisp 开发者可以定制,甚至完全接管 所有的文件操作。\n这里需要区分两个概念:文件和缓冲区。它们是两个不同的对象,文件是在计算 机上可持久保存的信息,而缓冲区是 emacs 中包含文件内容信息的对象,在 emacs 退出后就会消失,只有当保存缓冲区之后缓冲区里的内容才写到文件中去。\n文件读写 打开一个文件的命令是 find-file。这命令使一个缓冲区访问某个文件,并让这 个缓冲区成为当前缓冲区。在打开文件过程中会调用 find-file-hook。 find-file-noselect 是所有访问文件的核心函数。与 find-file 不同,它只返 回访问文件的缓冲区。这两个函数都有一个特点,如果 emacs 里已经有一个缓冲 区访问这个文件的话,emacs 不会创建另一个缓冲区来访问文件,而只是简单返 回或者转到这个缓冲区。怎样检查有没有缓冲区是否访问某个文件呢?所有和文 件关联的缓冲区里都有一个 buffer-local 变量 buffer-file-name。但是不要直 接设置这个变量来改变缓冲区关联的文件。而是使用 set-visited-file-name 来 修改。同样不要直接从 buffer-list 里搜索 buffer-file-name 来查找和某个文 件关联的缓冲区。应该使用 get-file-buffer 或者 find-buffer-visiting。\n(find-file \u0026#34;~/temp/test.txt\u0026#34;) (with-current-buffer (find-file-noselect \u0026#34;~/temp/test.txt\u0026#34;) buffer-file-name) ; =\u0026gt; \u0026#34;/home/ywb/temp/test.txt\u0026#34; (find-buffer-visiting \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; #\u0026lt;buffer test.txt\u0026gt; (get-file-buffer \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; #\u0026lt;buffer test.txt\u0026gt; 保存一个文件的过程相对简单一些。首先创建备份文件,处理文件的位模式,将 缓冲区写入文件。保存文件的命令是 save-buffer。相当于其它编辑器里另存为 的命令是 write-file。在这个过程中会调用一些函数或者 hook。 write-file-functions 和 write-contents-functions 几乎功能完全相同。它们 都是在写入文件之前运行的函数,如果这些函数中有一个返回了 non-nil 的值, 则会认为文件已经写入了,后面的函数都不会运行,而且也不会使用再调用其它 写入文件的函数。这两个变量有一个重要的区别是 write-contents-functions 在 改变主模式之后会被修改,因为它没有 permanent-local 属性,而 write-file-functions 则会仍然保留。before-save-hook 和 write-file-functions 功能也比较类似,但是这个变量里的函数会逐个执行,不 论返回什么值也不会影响后面文件的写入。after-save-hook 是在文件已经写入 之后才调用的 hook,它是 save-buffer 最后一个动作。\n但是实际上在 elisp 编程过程中经常遇到的一个问题是读取一个文件中的内容, 读取完之后并不希望这个缓冲区还留下来,如果直接用 kill-buffer 可能会把 用户打开的文件关闭。而且 find-file-noselect 做的事情实在超出我们的需要 的。这时你可能需要的是更底层的文件读写函数,它们是 insert-file-contents 和 write-region,调用形式分别是\n(insert-file-contents filename \u0026amp;optional visit beg end replace) (write-region start end filename \u0026amp;optional append visit lockname mustbenew) insert-file-contents 可以插入文件中指定部分到当前缓冲区中。如果指定 visit 则会标记缓冲区的修改状态并关联缓冲区到文件,一般是不用的。 replace 是指是否要删除缓冲区里其它内容,这比先删除缓冲区其它内容后插入文 件内容要快一些,但是一般也用不上。insert-file-contents 会处理文件的编 码,如果不需要解码文件的话,可以用 insert-file-contents-literally。\nwrite-region 可以把缓冲区中的一部分写入到指定文件中。如果指定 append 则是添加到文件末尾。和 insert-file-contents 相似,visit 参数也会把缓冲 区和文件关联,lockname 则是文件锁定的名字,mustbenew 确保文件存在时会 要求用户确认操作。\n思考题\n写一个函数提取出某个 c 头文件中的函数声明中的函数名和声明位置。\n文件信息 文件是否存在可以使用 file-exists-p 来判断。对于目录和一般文件都可以用 这个函数进行判断,但是符号链接只有当目标文件存在时才返回 t。\n如何判断文件是否可读或者可写呢?file-readable-p、file-writable-p, file-executable-p 分用来测试用户对文件的权限。文件的位模式还可以用 file-modes 函数得到。\n(file-exists-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-readable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-writable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; t (file-executable-p \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; nil (format \u0026#34;%o\u0026#34; (file-modes \u0026#34;~/temp/test.txt\u0026#34;)) ; =\u0026gt; \u0026#34;644\u0026#34; 文件类型判断可以使用 file-regular-p、file-directory-p、file-symlink-p, 分别判断一个文件名是否是一个普通文件(不是目录,命名管道、终端或者其它 io 设备)、文件名是否一个存在的目录、文件名是否是一个符号链接。其中 file-symlink-p 当文件名是一个符号链接时会返回目标文件名。文件的真实名 字也就是除去相对链接和符号链接后得到的文件名可以用 file-truename 得到。 事实上每个和文件关联的 buffer 里也有一个缓冲区局部变量 buffer-file-truename 来记录这个文件名。\n$ ls -l t.txt lrwxrwxrwx 1 ywb ywb 8 2007-07-15 15:51 t.txt -\u0026gt; test.txt (file-regular-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; t (file-directory-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; nil (file-symlink-p \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; \u0026#34;test.txt\u0026#34; (file-truename \u0026#34;~/temp/t.txt\u0026#34;) ; =\u0026gt; \u0026#34;/home/ywb/temp/test.txt\u0026#34; 文件更详细的信息可以用 file-attributes 函数得到。这个函数类似系统的 stat 命令,返回文件几乎所有的信息,包括文件类型,用户和组用户,访问日 期、修改日期、status change 日期、文件大小、文件位模式、inode number、 system number。这是我写的方便使用的帮助函数:\n(defun file-stat-type (file \u0026amp;optional id-format) (car (file-attributes file id-format))) (defun file-stat-name-number (file \u0026amp;optional id-format) (cadr (file-attributes file id-format))) (defun file-stat-uid (file \u0026amp;optional id-format) (nth 2 (file-attributes file id-format))) (defun file-stat-gid (file \u0026amp;optional id-format) (nth 3 (file-attributes file id-format))) (defun file-stat-atime (file \u0026amp;optional id-format) (nth 4 (file-attributes file id-format))) (defun file-stat-mtime (file \u0026amp;optional id-format) (nth 5 (file-attributes file id-format))) (defun file-stat-ctime (file \u0026amp;optional id-format) (nth 6 (file-attributes file id-format))) (defun file-stat-size (file \u0026amp;optional id-format) (nth 7 (file-attributes file id-format))) (defun file-stat-modes (file \u0026amp;optional id-format) (nth 8 (file-attributes file id-format))) (defun file-stat-guid-changep (file \u0026amp;optional id-format) (nth 9 (file-attributes file id-format))) (defun file-stat-inode-number (file \u0026amp;optional id-format) (nth 10 (file-attributes file id-format))) (defun file-stat-system-number (file \u0026amp;optional id-format) (nth 11 (file-attributes file id-format))) (defun file-attr-type (attr) (car attr)) (defun file-attr-name-number (attr) (cadr attr)) (defun file-attr-uid (attr) (nth 2 attr)) (defun file-attr-gid (attr) (nth 3 attr)) (defun file-attr-atime (attr) (nth 4 attr)) (defun file-attr-mtime (attr) (nth 5 attr)) (defun file-attr-ctime (attr) (nth 6 attr)) (defun file-attr-size (attr) (nth 7 attr)) (defun file-attr-modes (attr) (nth 8 attr)) (defun file-attr-guid-changep (attr) (nth 9 attr)) (defun file-attr-inode-number (attr) (nth 10 attr)) (defun file-attr-system-number (attr) (nth 11 attr)) 前一组函数是直接由文件名访问文件信息,而后一组函数是由 file-attributes 的返回值来得到文件信息。\n修改文件信息 重命名和复制文件可以用 rename-file 和 copy-file。删除文件使用 delete-file。创建目录使用 make-directory 函数。不能用 delete-file 删除 目录,只能用 delete-directory 删除目录。当目录不为空时会产生一个错误。\n设置文件修改时间使用 set-file-times。设置文件位模式可以用 set-file-modes 函数。set-file-modes 函数的参数必须是一个整数。你可以用位 函数 logand、logior 和 logxor 函数来进行位操作。\n思考题\n写一个函数模拟 chmod 命令的行为。\n文件名操作 虽然 mswin 的文件名使用的路径分隔符不同,但是这里介绍的函数都能用于 mswin 形式的文件名,只是返回的文件名都是 unix 形式了。路径一般由目录和 文件名,而文件名一般由主文件名 (basename)、文件名后缀和版本号构成。 emacs 有一系列函数来得到路径中的不同部分\n(file-name-directory \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/\u0026#34; (file-name-nondirectory \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;test.txt\u0026#34; (file-name-sans-extension \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test\u0026#34; (file-name-extension \u0026#34;~/temp/test.txt\u0026#34;) ; =\u0026gt; \u0026#34;txt\u0026#34; (file-name-sans-versions \u0026#34;~/temp/test.txt~\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test.txt\u0026#34; (file-name-sans-versions \u0026#34;~/temp/test.txt.~1~\u0026#34;) ; =\u0026gt; \u0026#34;~/temp/test.txt\u0026#34; 路径如果是从根目录开始的称为是绝对路径。测试一个路径是否是绝对路径使用 file-name-absolute-p。如果在 unix 或 gnu/linux 系统,以 ~ 开头的路径也是绝对路径。在 mswin 上,以 \u0026ldquo;/\u0026rdquo; 、 \u0026ldquo;\u0026quot;、\u0026ldquo;x:\u0026rdquo; 开头的路径都是绝对路径。如果不是绝对路径,可以使用 expand-file-name 来得到绝对路径。把一个绝对路径转换成相对某个路径的相 对路径的可以用 file-relative-name 函数。\n(file-name-absolute-p \u0026#34;~rms/foo\u0026#34;) ; =\u0026gt; t (file-name-absolute-p \u0026#34;/user/rms/foo\u0026#34;) ; =\u0026gt; t (expand-file-name \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;/home/ywb/foo\u0026#34; (expand-file-name \u0026#34;foo\u0026#34; \u0026#34;/usr/spool/\u0026#34;) ; =\u0026gt; \u0026#34;/usr/spool/foo\u0026#34; (file-relative-name \u0026#34;/foo/bar\u0026#34; \u0026#34;/foo/\u0026#34;) ; =\u0026gt; \u0026#34;bar\u0026#34; (file-relative-name \u0026#34;/foo/bar\u0026#34; \u0026#34;/hack/\u0026#34;) ; =\u0026gt; \u0026#34;../foo/bar\u0026#34; 对于目录,如果要将其作为目录,也就是确保它是以路径分隔符结束,可以用 file-name-as-directory。不要用 (concat dir \u0026ldquo;/\u0026rdquo;) 来转换,这会有移植问题。 和它相对应的函数是 directory-file-name\n(file-name-as-directory \u0026#34;~rms/lewis\u0026#34;) ; =\u0026gt; \u0026#34;~rms/lewis/\u0026#34; (directory-file-name \u0026#34;~lewis/\u0026#34;) ; =\u0026gt; \u0026#34;~lewis\u0026#34; 如果要得到所在系统使用的文件名,可以用 convert-standard-filename。比如 在 mswin 系统上,可以用这个函数返回用 \u0026ldquo;\u0026quot; 分隔的文件名\n(convert-standard-filename \u0026#34;c:/windows\u0026#34;) ;=\u0026gt; \u0026#34;c:\\\\windows\u0026#34; 临时文件 如果需要产生一个临时文件,可以使用 make-temp-file。这个函数按给定前缀产 生一个不和现有文件冲突的文件,并返回它的文件名。如果给定的名字是一个相 对文件名,则产生的文件名会用 temporary-file-directory 进行扩展。也可以 用这个函数产生一个临时文件夹。如果只想产生一个不存在的文件名,可以用 make-temp-name 函数\n(make-temp-file \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;/tmp/foo5611dxf\u0026#34; (make-temp-name \u0026#34;foo\u0026#34;) ; =\u0026gt; \u0026#34;foo5611q7l\u0026#34; 读取目录内容 可以用 directory-files 来得到某个目录中的全部或者符合某个正则表达式的 文件名。\n(directory-files \u0026#34;~/temp/dir/\u0026#34;) ;; =\u0026gt; ;; (\u0026#34;#foo.el#\u0026#34; \u0026#34;.\u0026#34; \u0026#34;.#foo.el\u0026#34; \u0026#34;..\u0026#34; \u0026#34;foo.el\u0026#34; \u0026#34;t.pl\u0026#34; \u0026#34;t2.pl\u0026#34;) (directory-files \u0026#34;~/temp/dir/\u0026#34; t) ;; =\u0026gt; ;; (\u0026#34;/home/ywb/temp/dir/#foo.el#\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/.\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/.#foo.el\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/..\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/foo.el\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/t.pl\u0026#34; ;; \u0026#34;/home/ywb/temp/dir/t2.pl\u0026#34;) (directory-files \u0026#34;~/temp/dir/\u0026#34; nil \u0026#34;\\\\.pl$\u0026#34;) ; =\u0026gt; (\u0026#34;t.pl\u0026#34; \u0026#34;t2.pl\u0026#34;) directory-files-and-attributes 和 directory-files 相似,但是返回的列表 中包含了 file-attributes 得到的信息。file-name-all-versions 用于得到某 个文件在目录中的所有版本,file-expand-wildcards 可以用通配符来得到目录 中的文件列表。\n思考题\n写一个函数返回当前目录包括子目录中所有文件名。\n神奇的 handle 如果不把文件局限在存储在本地机器上的信息,如果有一套基本的文件操作,比 如判断文件是否存在、打开文件、保存文件、得到目录内容之类,那远程的文件 和本地文件的差别也仅在于文件名表示方法不同而已。在 emacs 里,底层的文件 操作函数都可以托管给 elisp 中的函数,这样只要用 elisp 实现了某种类型文 件的基本操作,就能像编辑本地文件一样编辑其它类型文件了。\n决定何种类型的文件名使用什么方式来操作是在 file-name-handler-alist 变 量定义的。它是由形如 (regexp . handler) 的列表。如果文件名匹配这个 regexp 则使用 handler 来进行相应的文件操作。这里所说的文件操作,具体的 来说有这些函数:\n`access-file\u0026#39;, `add-name-to-file\u0026#39;, `byte-compiler-base-file-name\u0026#39;,\r`copy-file\u0026#39;, `delete-directory\u0026#39;, `delete-file\u0026#39;,\r`diff-latest-backup-file\u0026#39;, `directory-file-name\u0026#39;, `directory-files\u0026#39;,\r`directory-files-and-attributes\u0026#39;, `dired-call-process\u0026#39;,\r`dired-compress-file\u0026#39;, `dired-uncache\u0026#39;,\r`expand-file-name\u0026#39;, `file-accessible-directory-p\u0026#39;, `file-attributes\u0026#39;,\r`file-directory-p\u0026#39;, `file-executable-p\u0026#39;, `file-exists-p\u0026#39;,\r`file-local-copy\u0026#39;, `file-remote-p\u0026#39;, `file-modes\u0026#39;,\r`file-name-all-completions\u0026#39;, `file-name-as-directory\u0026#39;,\r`file-name-completion\u0026#39;, `file-name-directory\u0026#39;, `file-name-nondirectory\u0026#39;,\r`file-name-sans-versions\u0026#39;, `file-newer-than-file-p\u0026#39;,\r`file-ownership-preserved-p\u0026#39;, `file-readable-p\u0026#39;, `file-regular-p\u0026#39;,\r`file-symlink-p\u0026#39;, `file-truename\u0026#39;, `file-writable-p\u0026#39;,\r`find-backup-file-name\u0026#39;, `find-file-noselect\u0026#39;,\r`get-file-buffer\u0026#39;, `insert-directory\u0026#39;, `insert-file-contents\u0026#39;,\r`load\u0026#39;, `make-auto-save-file-name\u0026#39;, `make-directory\u0026#39;,\r`make-directory-internal\u0026#39;, `make-symbolic-link\u0026#39;,\r`rename-file\u0026#39;, `set-file-modes\u0026#39;, `set-file-times\u0026#39;,\r`set-visited-file-modtime\u0026#39;, `shell-command\u0026#39;, `substitute-in-file-name\u0026#39;,\r`unhandled-file-name-directory\u0026#39;, `vc-registered\u0026#39;,\r`verify-visited-file-modtime\u0026#39;,\r`write-region\u0026#39;. 在 handle 里,可以只接管部分的文件操作,其它仍交给 emacs 原来的函数来完 成。举一个简单的例子。比如最新版本的 emacs 把 *scratch* 的 auto-save-mode 打开了。如果你不想这个缓 冲区的自动保存的文件名散布得到处都是,可以想办法让这个缓冲区的自动保存 文件放到指定的目录中。刚好 make-auto-save-file-name 是在上面这个列表里 的,但是不幸的是在函数定义里 make-auto-save-file-name 里不对不关联文件 的缓冲区使用 handler(我觉得是一个 bug 呀),继续往下看,发现生成保存文 件名是使用了 expand-file-name 函数,所以解决办法就产生了:\n(defun my-scratch-auto-save-file-name (operation \u0026amp;rest args) (if (and (eq operation \u0026#39;expand-file-name) (string= (car args) \u0026#34;#*scratch*#\u0026#34;)) (expand-file-name (concat \u0026#34;~/.emacs.d/backup/\u0026#34; (car args))) (let ((inhibit-file-name-handlers (cons \u0026#39;my-scratch-auto-save-file-name (and (eq inhibit-file-name-operation operation) inhibit-file-name-handlers))) (inhibit-file-name-operation operation)) (apply operation args)))) 函数列表 (find-file filename \u0026amp;optional wildcards) (find-file-noselect filename \u0026amp;optional nowarn rawfile wildcards) (set-visited-file-name filename \u0026amp;optional no-query along-with-file) (get-file-buffer filename) (find-buffer-visiting filename \u0026amp;optional predicate) (save-buffer \u0026amp;optional args) (insert-file-contents filename \u0026amp;optional visit beg end replace) (insert-file-contents-literally filename \u0026amp;optional visit beg end replace) (write-region start end filename \u0026amp;optional append visit lockname mustbenew) (file-exists-p filename) (file-readable-p filename) (file-writable-p filename) (file-executable-p filename) (file-modes filename) (file-regular-p filename) (file-directory-p filename) (file-symlink-p filename) (file-truename filename) (file-attributes filename \u0026amp;optional id-format) (rename-file file newname \u0026amp;optional ok-if-already-exists) (copy-file file newname \u0026amp;optional ok-if-already-exists keep-time preserve-uid-gid) (delete-file filename) (make-directory dir \u0026amp;optional parents) (delete-directory directory) (set-file-modes filename mode) (file-name-directory filename) (file-name-nondirectory filename) (file-name-sans-extension filename) (file-name-sans-versions name \u0026amp;optional keep-backup-version) (file-name-absolute-p filename) (expand-file-name name \u0026amp;optional default-directory) (file-relative-name filename \u0026amp;optional directory) (file-name-as-directory file) (directory-file-name directory) (convert-standard-filename filename) (make-temp-file prefix \u0026amp;optional dir-flag suffix) (make-temp-name prefix) (directory-files directory \u0026amp;optional full match nosort) (dired-files-attributes dir) 问题解答 提取头文件中函数名 这是我写的一个版本,主要是函数声明的正则表达式不好写,函数是很简单的。 从这个例子也可以看出它错误的把那个 typedef void 当成函数声明了。如果你 知道更好的正则表达式,请告诉我一下。\n(defvar header-regexp-list \u0026#39;((\u0026#34;^\\\\(?:\\\\(?:g_const_return\\\\|extern\\\\|const\\\\)\\\\s-+\\\\)?[a-za-z][_a-za-z0-9]*\\ \\\\(?:\\\\s-*[*]*[ \\t\\n]+\\\\|\\\\s-+[*]*\\\\)\\\\([a-za-z][_a-za-z0-9]*\\\\)\\\\s-*(\u0026#34; . 1) (\u0026#34;^\\\\s-*#\\\\s-*define\\\\s-+\\\\([a-za-z][_a-za-z0-9]*\\\\)\u0026#34; . 1))) (defun parse-c-header (file) \u0026#34;extract function name and declaration position using `header-regexp-list\u0026#39;.\u0026#34; (interactive \u0026#34;fheader file: \\np\u0026#34;) (let (info) (with-temp-buffer (insert-file-contents file) (dolist (re header-regexp-list) (goto-char (point-min)) (while (re-search-forward (car re) nil t) (push (cons (match-string (cdr re)) (line-beginning-position)) info)))) info)) (parse-c-header \u0026#34;/usr/include/glib-2.0/gmodule.h\u0026#34;) ;; =\u0026gt; ;; ((\u0026#34;g_module_name\u0026#34; . 1788) ;; (\u0026#34;g_module_open\u0026#34; . 1747) ;; (\u0026#34;g_module_export\u0026#34; . 1396) ;; (\u0026#34;g_module_export\u0026#34; . 1317) ;; (\u0026#34;g_module_import\u0026#34; . 1261) ;; (\u0026#34;g_module_build_path\u0026#34; . 3462) ;; (\u0026#34;g_module_name\u0026#34; . 2764) ;; (\u0026#34;g_module_symbol\u0026#34; . 2570) ;; (\u0026#34;g_module_error\u0026#34; . 2445) ;; (\u0026#34;g_module_make_resident\u0026#34; . 2329) ;; (\u0026#34;g_module_close\u0026#34; . 2190) ;; (\u0026#34;g_module_open\u0026#34; . 2021) ;; (\u0026#34;g_module_supported\u0026#34; . 1894) ;; (\u0026#34;void\u0026#34; . 1673)) 模拟 chmod 的函数 这是一个改变单个文件模式的 chmod 版本。递归版本的就自己作一个练习吧。最 好不要直接调用这个函数,因为每次调用都要解析一次 mode 参数,想一个只解 析一次的方法吧。\n(defun chmod (mode file) \u0026#34;a elisp function to simulate command chmod. note that the command chmod can accept mode match `[ugoa]*([-+=]([rwxxst]*|[ugo]))+\u0026#39;, but this version only can process mode match `[ugoa]*[-+=]([rwx]*|[ugo])\u0026#39;. \u0026#34; (cond ((integerp mode) (if (\u0026gt; mode #o777) (error \u0026#34;unknown mode option: %d\u0026#34; mode))) ((string-match \u0026#34;^[0-7]\\\\{3\\\\}$\u0026#34; mode) (setq mode (string-to-number mode 8))) ((string-match \u0026#34;^\\\\([ugoa]*\\\\)\\\\([-+=]\\\\)\\\\([rwx]*\\\\|[ugo]\\\\)$\u0026#34; mode) (let ((users (append (match-string 1 mode) nil)) (mask-func (string-to-char (match-string 2 mode))) (bits (append (match-string 3 mode) nil)) (oldmode (file-modes file)) (user-list \u0026#39;((?a . #o777) (?u . #o700) (?g . #o070) (?o . #o007))) mask) (when bits (setq bits (* (cond ((= (car bits) ?u) (lsh (logand oldmode #o700) -6)) ((= (car bits) ?g) (lsh (logand oldmode #o070) -3)) ((= (car bits) ?o) (logand oldmode #o007)) (t (+ (if (member ?r bits) 4 0) (if (member ?w bits) 2 0) (if (member ?x bits) 1 0)))) #o111)) (if users (setq mask (apply \u0026#39;logior (delq nil (mapcar (lambda (u) (assoc-default u user-list)) users)))) (setq mask #o777)) (setq mode (cond ((= mask-func ?\\=) (logior (logand bits mask) (logand oldmode (logxor mask #o777)))) ((= mask-func ?\\+) (logior oldmode (logand bits mask))) (t (logand oldmode (logxor (logand bits mask) #o777)))))))) (t (error \u0026#34;unknow mode option: %s\u0026#34; mode))) (set-file-modes file mode)) 列出目录中所有文件 为了让这个函数更类似 directory-files 函数,我把参数设置为和它一样的:\n(defun my-directory-all-files (dir \u0026amp;optional full match nosort) (apply \u0026#39;append (delq nil (mapcar (lambda (file) (if (and (not (string-match \u0026#34;^[.]+$\u0026#34; (file-name-nondirectory file))) (file-directory-p (expand-file-name file dir))) (if full (my-directory-all-files file full match nosort) (mapcar (lambda (f) (concat (file-name-as-directory file) f)) (my-directory-all-files (expand-file-name file dir) full match nosort))) (if (string-match match file) (list file)))) (directory-files dir full nil nosort))))) 操作对象之四 \u0026ndash; 文本 文本的插入删除,查找替换操作已经在缓冲区一节中讲过了。这一节主要介绍文 本属性。\n如果使用过其它图形界面的文本组件进行编程,它们对于文本的高亮一般都是采 用给对应文本贴上相应标签的方法。emacs 的处理方法也是类似的,但是相比之 下,要强大的多。在 emacs 里,在不同位置上的每个字符都可以有一个属性列表。 这个属性列表和符号的属性列表很相似,都是由一个名字和值构成的对组成。名 字和值都可以是一个 lisp 对象,但是通常名字都是一个符号,这样可以用这个 符号来查找相应的属性值。复制文本通常都会复制相应的字符的文本属性,但是 也可以用相应的函数只复制文本字符串,比如 substring-no-properties、 insert-buffer-substring-no-properties、buffer-substring-no-properties。\n产生一个带属性的字符串可以用 propertize 函数\n(propertize \u0026#34;abc\u0026#34; \u0026#39;face \u0026#39;bold) ; =\u0026gt; #(\u0026#34;abc\u0026#34; 0 3 (face bold)) 如果你在一个 text-mode 的缓冲区内用 m-x eval-expression 用 insert 函数 插入前面这个字符串,就会发现插入的文本已经是粗体字了。之所以不能在 *scratch* 产生这种效果,是因为通常我们是开启了 font-lock-mode,在 font-lock-mode 里,文本的 face 属性是实时计算出来的。 在插入文本之后,它的 face 属性已经很快地被改变了。你可以在关闭 font-lock-mode 后再测试一次应该是可以看到 *scratch* 里也是可以用这种方法插入带 face 属性的文本的。\n虽然文本属性的名字可以是任意的,但是一些名字是有特殊含义的。\n属性名 含义 category 值必须是一个符号,这个符号的属性将作为这个字符的属性 face 控制文本的字体和颜色 font-lock-face 和 face 相似,可以作为 font-lock-mode 中静态文本的 face mouse-face 当鼠标停在文本上时的文本 face fontified 记录是否使用 font lock 标记了 face display 改变文本的显示方式,比如高、低、长短、宽窄,或者用图片代替 help-echo 鼠标停在文本上时显示的文字 keymap 光标或者鼠标在文本上时使用的按键映射 local-map 和 keymap 类似,通常只使用 keymap syntax-table 字符的语法表 read-only 不能修改文本,通过 stickness 来选择可插入的位置 invisible 不显示在屏幕上 intangible 把文本作为一个整体,光标不能进入 field 一个特殊标记,有相应的函数可以操作带这个标记的文本 cursor (不知道具体用途) pointer 修改鼠标停在文本上时的图像 line-spacing 新的一行的距离 line-height 本行的高度 modification-hooks 修改这个字符时调用的函数 insert-in-front-hooks 与 modification-hooks 相似,在字符前插入调用的函数 insert-behind-hooks 与 modification-hooks 相似,在字符后插入调用的函数 point-entered 当光标进入时调用的函数 point-left 当光标离开时调用的函数 composition 将多个字符显示为一个字形 正是由于 emacs 的文本有如此丰富的属性,使得 emacs 里的文字才变得多彩, 变得人性化。\n查看文本属性 由于字符串和缓冲区都可以有文本属性,所以下面的函数通常不提供特定参数就是检 查当前缓冲区的文本属性,如果提供文本对象,则是操作对应的文本属性。\n查看文本对象在某处的文本属性可以用 get-text-property 函数。\n(setq foo (concat \u0026#34;abc\u0026#34; (propertize \u0026#34;cde\u0026#34; \u0026#39;face \u0026#39;bold))) ; =\u0026gt; #(\u0026#34;abccde\u0026#34; 3 6 (face bold)) (get-text-property 3 \u0026#39;face foo) ; =\u0026gt; bold (save-excursion (goto-char (point-min)) (insert foo)) (get-text-property 4 \u0026#39;face) ; =\u0026gt; bold get-char-property 和 get-text-property 相似,但是它是先查找 overlay 的 文本属性。overlay 是缓冲区文字在屏幕上的显示方式,它属于某个缓冲区,具 有起点和终点,也具有文本属性,可以修改缓冲区对应区域上文本的显示方式。\nget-text-property 是查找某个属性的值,用 text-properties-at 可以得到某 个位置上文本的所有属性。\n修改文本属性 put-text-property 可以给文本对象添加一个属性。比如\n(let ((str \u0026#34;abc\u0026#34;)) (put-text-property 0 3 \u0026#39;face \u0026#39;bold str) str) ; =\u0026gt; #(\u0026#34;abc\u0026#34; 0 3 (face bold)) 和 put-text-property 类似,add-text-properties 可以给文本对象添加一系 列的属性。和 add-text-properties 不同,可以用 set-text-properties 直接 设置文本属性列表。你可以用 =(set-text-properties start end nil)= 来除去 某个区间上的文本属性。也可以用 remove-text-properties 和 remove-list-of-text-properties 来除去某个区域的指定文本属性。这两个函 数的属性列表参数只有名字起作用,值是被忽略的。\n(setq foo (propertize \u0026#34;abcdef\u0026#34; \u0026#39;face \u0026#39;bold \u0026#39;pointer \u0026#39;hand)) ;; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 0 6 (pointer hand face bold)) (set-text-properties 0 2 nil foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 6 (pointer hand face bold)) (remove-text-properties 2 4 \u0026#39;(face nil) foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (pointer hand) 4 6 (pointer hand face bold)) (remove-list-of-text-properties 4 6 \u0026#39;(face nil pointer nil) foo) ; =\u0026gt; t foo ; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (pointer hand)) 查找文本属性 文本属性通常都是连成一个区域的,所以查找文本属性的函数是查找属性变化的 位置。这些函数一般都不作移动,只是返回查找到的位置。使用这些函数时最好 使用 limit 参数,这样可以提高效率,因为有时一个属性直到缓冲区末尾也没 有变化,在这些文本中可能就是多余的。\nnext-property-change 查找从当前位置起任意一个文本属性发生改变的位置。 next-single-property-change 查找指定的一个文本属性改变的位置。 next-char-property-change 把 overlay 的文本属性考虑在内查找属性发生改 变的位置。next-single-property-change 类似的查找指定的一个考虑 overlay 后文本属性改变的位置。这四个函数都对应有 previous- 开头的函数,用于查 找当前位置之前文本属性改变的位置\n(setq foo (concat \u0026#34;ab\u0026#34; (propertize \u0026#34;cd\u0026#34; \u0026#39;face \u0026#39;bold) (propertize \u0026#34;ef\u0026#34; \u0026#39;pointer \u0026#39;hand))) ;; =\u0026gt; #(\u0026#34;abcdef\u0026#34; 2 4 (face bold) 4 6 (pointer hand)) (next-property-change 1 foo) ; =\u0026gt; 2 (next-single-property-change 1 \u0026#39;pointer foo) ; =\u0026gt; 4 (previous-property-change 6 foo) ; =\u0026gt; 4 (previous-single-property-change 6 \u0026#39;face foo) ; =\u0026gt; 4 text-property-any 查找区域内第一个指定属性值为给定值的字符位置。 text-property-not-all 和它相反,查找区域内第一个指定属性值不是给定值的 字符位置。\n(text-property-any 0 6 \u0026#39;face \u0026#39;bold foo) ; =\u0026gt; 2 (text-property-any 0 6 \u0026#39;face \u0026#39;underline foo) ; =\u0026gt; nil (text-property-not-all 2 6 \u0026#39;face \u0026#39;bold foo) ; =\u0026gt; 4 (text-property-not-all 2 6 \u0026#39;face \u0026#39;underline foo) ; =\u0026gt; 2 思考题\n写一个命令,可在 text-mode 里用指定模式给选中的文本添加高亮。\n函数列表 (propertize string \u0026amp;rest properties) (get-text-property position prop \u0026amp;optional object) (get-char-property position prop \u0026amp;optional object) (text-properties-at position \u0026amp;optional object) (put-text-property start end property value \u0026amp;optional object) (add-text-properties start end properties \u0026amp;optional object) (set-text-properties start end properties \u0026amp;optional object) (remove-text-properties start end properties \u0026amp;optional object) (remove-list-of-text-properties start end list-of-properties \u0026amp;optional object) (next-property-change position \u0026amp;optional object limit) (next-single-property-change position prop \u0026amp;optional object limit) (next-char-property-change position \u0026amp;optional limit) (next-single-char-property-change position prop \u0026amp;optional object limit) (previous-property-change position \u0026amp;optional object limit) (previous-single-property-change position prop \u0026amp;optional object limit) (previous-char-property-change position \u0026amp;optional limit) (previous-single-char-property-change position prop \u0026amp;optional object limit) (text-property-any start end property value \u0026amp;optional object) (text-property-not-all start end property value \u0026amp;optional object) 问题解答 手工高亮代码 (defun my-fontify-region (beg end mode) (interactive (list (region-beginning) (region-end) (intern (completing-read \u0026#34;which mode to use: \u0026#34; obarray (lambda (s) (and (fboundp s) (string-match \u0026#34;-mode$\u0026#34; (symbol-name s)))) t)))) (let ((buf (current-buffer)) (font-lock-verbose nil) (start 1) face face-list) (set-text-properties beg end \u0026#39;(face nil)) (with-temp-buffer (goto-char (point-min)) (insert-buffer-substring buf beg end) (funcall mode) (font-lock-fontify-buffer) (or (get-text-property start \u0026#39;face) (setq start (next-single-property-change start \u0026#39;face))) (while (and start (\u0026lt; start (point-max))) (setq end (or (next-single-property-change start \u0026#39;face) (point-max)) face (get-text-property start \u0026#39;face)) (and face end (setq face-list (cons (list (1- start) (1- end) face) face-list))) (setq start end))) (when face-list (dolist (f (nreverse face-list)) (put-text-property (+ beg (car f)) (+ beg (cadr f)) \u0026#39;face (nth 2 f)))))) 但是直接从那个临时缓冲区里把整个代码拷贝出来也可以了,但是可能某些情况 下,不好修改当前缓冲区,或者不想把那个模式里其它文本属性拷贝出来,这个 函数还是有用的。当然最主要的用途是演示使用查找和添加文本属性的方法。事 实上这个函数也是我用来高亮 muse 模式里 src 标签内源代码所用的方法。但是 不幸的是 muse 模式里这个函数并不能产生很好的效果,不知道为什么。\n后记 到现在为止,我计划写的 elisp 入门内容已经写完了。如果你都看完看懂这些内 容,我想写一些简单的 elisp 应用应该是没有什么问题了。还有一些比较重要的 内容没有涉及到,我在这列一下,如果你对此有兴趣,可以自己看 elisp manual 里相关章节:\n按键映射 (keymap) 和菜单 minibuffer 和补全 进程 调试 主模式 (major mode) 和从属模式 (minor mode) 定制声明 修正函数 (advising function) 非 ascii 字符 其实看一遍 elisp manual 也是很好的选择。我在写这些文字时就是一边参考 elisp manual 一边写的。写的时候我一直有种不安的感觉,这近 3m 的文字被 我压缩到这么一点点是不是太过份了。在 elisp manual 里一些很重要的说明经 常被我一两句话就带过了,有时根本就没有提到,这会不会让刚学 elisp 的人 误入歧途呢?每每想到这一点,我就想就此停住。但是半途而废总是不好的。所 以我还是决定写完应该写的就好了。其它的再说吧。\n如果你是一个新手,我很想知道你看完这个入门教程的感受。当然如果实在没有 兴趣看,也可以告诉我究竟哪里写的不好。我希望在这份文档上花的时间和精力 没有白费。\n","date":"2023-04-18","permalink":"https://loveminimal.github.io/posts/elisp/","summary":"\u003cp\u003e📕 转载自 \u003ca href=\"http://smacs.github.io/elisp/\"\u003eEmacs Lisp 简明教程 - 水木社区 Emacs 版\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是 叶文彬(水木 ID: happierbee) 写的一份 Emacs Lisp 的教程,深入浅出,非常适合初学者。文档的 TeX 代码及 PDF 文档可在* \u003ca href=\"http://www.newsmth.net/nForum/article/Emacs/58338?s=58338\"\u003e此处下载\u003c/a\u003e* 。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eemacs 的高手不能不会 elisp。但是对于很多人来说 elisp 学习是一个痛苦的历程,至少我是有这样一段经历。因此,我写了这一系列文章,希望能为后来者提供一点捷径。\u003c/p\u003e","title":"emacs lisp"},{"content":"为什么会制作这款 typora 的主题呢?\n📙 virgo , 仓库地址 loveminimal/typora-theme-virgo: a typora theme for relative hugo.\n更新日志 - 2023-04-17 20:48 新增 virgo dark 暗色主题样式 快速开始 现在,它已经上传到了 typora 官方的主题仓库 - virgo ,但是我首次提交 pull request 的时候写错了 homepage 和 download 的链接…… 😅\n我已经提交了新 pull request ,后续官方合并后就没有问题了。幸运的是,它并影响你使用这个主题,毕竟你正在浏览器这个页面。\n官方合并的速度还挺快的,目前 virgo 中的链接已经是正确的了。\n在 偏好设置 / 外观 中点击 打开主题文件夹 ,如下:\n该文件夹下存放着 typora 的主题文件,在文件夹下,执行如下命令:\ngit clone https://github.com/loveminimal/typora-theme-virgo.git 然后,将文件夹 typora-theme-virgo 中的 virgo 文件夹和 virgo.css 、 virgo-dark.css 拷贝一份到当前目录下,重启 typora ,即可选择主题 virgo 、 virgo dark 。\n有点不明白,作者不什么不直接遍历 themes 文件下的所有第一级文件夹,然后指定其中的默认加载样式文件,如此,也方便使用 git 管理啊,省了手动拷贝的这一步。 😶\n为什么需要 之前是在 vscode 中编辑博客文件的,使用体验也很好,基本上没有什么不足之处。那为什么要使用 typora 呢?\n一是, typora 的预览效果十分接近于网页渲染,因为当前的主题就是从站点的主题适配而来的,除了部分借助于 javascript 实现的动态交互,其渲染结果有着 90%+ 的相似性。‘所见即所得’,很不错的体验。\n二是,vscode 中对表格的处理没有 typora 优雅,尤其是中英文混输的时候,光是对齐就让人‘崩溃’,尽管有一些所谓有对齐表格的办法,也是不那么让人满意。\n这是 vscode 中对于 table 的编辑状态,很乱很乱,如下:\n\u0026gt; 这是用 vs 编辑器中的 table\n我们来看看,当前主题中对于 table 的编辑和渲染,如下:\n\u0026gt; typora 下当前主题 virgo 的渲染和编辑\n对比很‘惨烈’! 😈\n最后, typora 对于图片的处理也很优秀,不仅实现了直接从剪切板复制粘贴,最近的更新中还优化了对图片相关操作(改、删)的状态同步。\n其他 typora 内置了几个主题,各有特色,在 themes gallery — typora 中也有不少第三方的精美主题可以选择。总之呢,基本上可以满足各类用户的喜好。\n当前主题目前已经提交 pr 到该主题仓库,具体生效日期不定,其实就算合并成功,你还是得按照 快速开始 中描述的步骤那样使其生效。\n结语 最后,使用了一段时间了,对 typora 做一些简评吧。总体来说,纯文本编辑的话,还是不错的。就个人而言,希望在后续更新中可以改进或开放以下两个方面。\n开放设置项,允许用户快速打开文件的时候,可以选择在当前窗口打开,而不是新开一个窗口。\n可以增强快捷键设置,当前的开放支持太弱了,希望可以增加更多的按键接口,尤其的光标的上下左右移动的映射。编辑的时候,习惯使用 alt + h/j/k/l 来移动了,使用 箭头 的话就不得不移动一下右手……\n","date":"2023-04-04","permalink":"https://loveminimal.github.io/posts/about-typora-theme-virgo/","summary":"\u003cp\u003e为什么会制作这款 Typora 的主题呢?\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://theme.typora.io/theme/Virgo/\"\u003e📙 Virgo\u003c/a\u003e , 仓库地址 \u003ca href=\"https://github.com/loveminimal/typora-theme-virgo\"\u003eloveminimal/typora-theme-virgo: A typora theme for relative hugo.\u003c/a\u003e\u003c/p\u003e","title":"一款 typora 主题"},{"content":"本文不涉及具体的细节,浅谈一下 web 前端的 1 2 3 ……\n前端是什么 在当下的产品开发流程中,大致分为以下几个过程:\n1. 产品设计(需求分析)\r2. 技术选型\r3. 前端负责界面渲染、交互 \u0026amp; 后端负责数据处理\r4. 测试(功能)\r5. 运维部署 这里,我们只关注第 3 点,即具体的开发过程。\n要理解‘什么是前端’,最好回顾一下 web 的发展史。这是世界上第一个网站 http://info.cern.ch/ ,于 1991.8.6 上网,它解释了万维网是什么,如何使用网页浏览器和如何建立一个网页服务器等等。\n你会发现,这个页面就是一份简单的 .html 文件,让我们来看一下它的实现过程。\n- 伯纳斯·李(互联网之父)写了一个 html 文档,\r- 然后把这个文档放在连接着网络的一台电脑上(服务器),\r- 用户电脑通过客户端(通常是浏览器)连网访问它。 看,最初的时候,只有 html ,它就是一个文本文件,没有交互,用户只能浏览。这个时候,也没有什么前后端的概念。开发人员需要做的,就是编写 html 文档,并把它放到可以被访问的服务器上。\n用户或开发人员,感觉单纯的 html 文件在浏览器上显示出来不好看,于是有了 css 来装饰样式(1995),我们进入了 html + css 的时代。\n目前为止,用户还是只能看啊,用户说:“我要交互!我要交互!我要交互!”\n伯纳斯说:“给你表单!给你表单!给你表单!”\n现在用户可以通过表单,输入一些信息(比如登录),点击按钮,来和服务器进行一定的交互了。同样,服务器端也有同样的程序来处理用户提交的表单信息。此时,一切都还是简单的!\n直到有一天,用户又不满意了,大声说:“我要更多的交互!更多!更多!”\n网景公司(火狐浏览器前身)委托 brendan eich 开发了 javascript 语言,用于满足用户的需求。 brendan 只花了 10 天时间,太快了,所以 javascript 有许多的语言缺陷,但又阴差阳错地成了浏览器最钟爱的(内置了的)脚本语言,简直了。有些事,就是那么不讲道理。\n我们现在进入了 html + css + js 的时代!\n你还没说什么是前端呢?\n别着急,就要说了。最初的时候,开发任务不算很重,一个开发人员就可以完成 .html、 .css、 .js 文件及服务器上处理用户请求的程序的编写。\n这时,还是没有区分前端、后端,因为都是一个人写的!\n后来,网页内容越来越丰富,用户的交互越来越多,服务器端处理的请求越来越复杂。于是,程序员 b 对程序员 f 说:“我来处理用户提交的信息,生成展示所需的数据,你负责编写文档(html、css、js 等)文件,把数据插入到 html 中,返回给用户的浏览器来展示。”\n于是乎,程序员 b 只负责处理用户请求,程序员 f 负责页面渲染,提高了效率。慢慢地,也就有了专职于后端(back end)和专职于前端(front end)的方向。\n现在,我们明白了,所谓‘前端’,主要就是负责展示部分,最终的交付便是用于用户端浏览器可以识别的页面文件。\n近些年来,前端发展可谓是如火如荼,页面的交互也越来越复杂,各种框架层出不穷,让人眼花缭乱。但它的本质,从来就没有改变过。最终, 前端的核心,就是生成用户浏览器可以渲染的页面文件。\n重点来了,一是浏览器,二是页面文件。\n浏览器 说起浏览器,大家肯定都不陌生,如 chrome、 edge、 firefox、 ie ……\n浏览器只认识 html !\n如果,深入了解,就会发现浏览器实在是一个复杂的软件,好在我们只需要应用它。更多关于浏览器的渲染细节,可以参考 浏览器 这篇文章。\n对于一位前端开发人员来说,现在是幸福的,因为有了浏览器开发工具(一般现代浏览器通过 f12 打开),你可查看页面的请求过程和渲染细节,很方便地开发和调试项目页面。\n页面文件 再厉害的框架,最终都要编译打包成 html + css + js , 因为浏览器只认这个组合!\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;页面文件\u0026lt;/title\u0026gt; …… \u0026lt;link href=\u0026#34;/path/to/style.css\u0026#34; /\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;关于页面文件\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;description\u0026#34;\u0026gt; \u0026lt;p\u0026gt;再厉害的框架,最终都要编译打包成 `html + css + js` 组合。\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;……\u0026lt;/p\u0026gt; …… \u0026lt;/div\u0026gt; …… \u0026lt;script src=\u0026#34;/path/to/main.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 一个 .html 文件,通过 \u0026lt;link\u0026gt; 标签引入 .css 文件,通过 \u0026lt;script\u0026gt; 标签引入 .js 文件,这就是页面文件的基本结构。\nhtml html ,全称‘超文本标记语言’(hyperext markup language) ,是一种用于创建网页的标准标记语言。\n为什么需要‘标记’呢?\n你直接对浏览器讲中文,它是听不懂的!你必须跟它讲 html 语言,它才能懂你要干什么。\nhtml 语言很简单,你只需要用相应的标签来包裹你想要浏览器渲染的内容,就可以了。比如,段落标签 \u0026lt;p\u0026gt; ,分割线标签 \u0026lt;hr\u0026gt;,标题标签 \u0026lt;title\u0026gt; ,引入样式文件的标签 \u0026lt;link\u0026gt; ,引入脚本文件的标签 \u0026lt;script\u0026gt; 等等。\n在实际学习的过程中,有一些标签很常用,有一些标签几乎不用,边学边用即可!\ncss css,全称‘层叠样式表’(cascading style sheets),是一种用来为结构化文档(如 html)添加样式(字体、间距和颜色等)的计算机语言。\ncss 中最重要的一个概念就是‘选择器’了,即选中你要添加样式的页面元素! ……\njavascript 前端领域, javascript 可谓是绝对的主角(后续简称 js),它是目前主流浏览器(所有)指定的唯一脚本语言。\n最初,它只是为了满足用户日益俱增的交互需求,现在也是,只不过更加的强大和复杂。但本质上,仍然是服务于 html 的,这一点从未改变。\njs 的核心语法很简单,如变量、函数及日常应用过程中常用到的数组、对象等数据结构。\n缺陷很多,但不影响 js 登大雅之堂!可以这样说,精通 js 的过程,就是一场学习如何‘踩坑’的旅程,开个玩笑,其实现在好多了。\n框架 js 是通过什么来指挥浏览器的呢?\n浏览器提供了一系列的接口 api ,与 js 最紧密的便是 dom 了。最初的时候,开发人员也是这样做的,通过 html 编写好页面内容,通过 css 来添加样式,通过 js 操作浏览器提供的 api 接口来操作 dom 。\ndom (document object model) 文档对象模型,是浏览器解析 .html 文件后生成的一个树形结构,用于渲染页面。\n简单的页面,这样做很容易。\n问题就在于,现在的页面越来越不简单了……\n于是框架就上场了!看,有‘需求’,就有‘市场’!\njquery 与其说 jquery 是一个框架,不如说,它是一个函数库。本质上来说,它是对原生 js 的一种封装,用来更方便地操作 dom 。对,你仍然需要操作 dom !\n现在用的不多了,在使用中了解即可。\nvue \u0026amp; react 这两个前端框架,是目前最热门、最流行的前端框架了。除了,实现原理的某些部分有些不同外,其余应用都是大同小异。学习了其中一种,再学习另一种,也会很快。\n这里,我们以 vue 为主,某些方面对比 react 来说。不得不说,vue 对于初学者是更加友好的,性能也很不错!国内来说,流行程度更胜一筹。\n首先,需要明白,vue 本身并没有为 html + css + js 引入什么新的东西,你所编写的 vue 相关的模板文件 .vue、 样式文件 .scss、 逻辑文件 .js ,最终经过编译打包后,还是 html + css + js 这一套组合!\n也就是说,你最终,放在服务器上,供用户访问的页面文件,始终都是 html !\n学习 vue ,究竟学什么? 模板语法!!!\n是的,和你学习 html ,没什么太大的区别,正如你学习 html 是为了让浏览器知道你的说什么,学习 vue 的模板语法也是为了让 vue 相关的编译器、打包器知道你要它们干什么!仅此而已!\n你按着它规定的语法来编写 .vue、 .scss、 .js ,对应的编译器、打包器就知道如何把你编写的内容转成 html + css + js 供浏览器渲染绘制页面。\n我们不止一次提到了编译器和打包器,它们是什么呢?有什么作用?\n编译器\n拿 vue 来说,你编写的是组件是 app.vue ,浏览器是不认识的,经过编译器编译就成了 *.js ,就可以引入到 *.html 中供浏览器解析了。可以认为编译器就是一个‘加工厂’,进去的是 .vue ,出来的是 .js 。\n看,编译器并不是什么神奇的东东,它本质上来说,就是个‘翻译机’。\n编译器不需要应用开发者参与,它是由框架实现的,开发者只需要按着框架给出的‘写法’,编译器就能读懂,并正确转译。\n打包器\n打包器呢?为什么需要打包器?因为随着前端内容越来越复杂,把所有的逻辑写在一个 .js 文件中对于程序员来说是不友好的。比较典型的方式,就是通过模块化编程来避开这一点。也就是说,我们会把交互逻辑分散到不同的 .js 文件中,方便编写和复用。如此,在一个项目中,我们使用的资源分分散在项目的不同层级之中,有 .js ,有 .css 等等。最终使用的时候,打包器会根据它们之间的依赖关系,将这些资源再合并到一起,引入到 .html 中,供浏览器渲染。\n是的,打包器就像一个‘拼接手’,按照资源(模块)的依赖关系,把零碎的‘资源’合成一个整体。\n当前,比较流行的打包器仍然是 webpack ,你需要学习并了解它,同样的,边学习,边使用,在使用中学习。\n另外,你可能也需要了解一下 rollup ,它也是一种打包工具,最新的 vue3 推荐的脚手架 vite (项目初始化工具)就是集成了 rollup 。\n同样的,所有的打包工具,实现方式可能各有不同,但其最终目的和思想,都是大同小异的。\n其他 模块和组件 在 模块化编程 中,我们具体说明过模块相关的概念,可以参考一下。\n什么是模块?什么是组件?\n很多时候,这两个概念的划分不是很清楚,甚至是通用的。比如,一个 .vue 文件,可以称为是一个组件,一个完全的 vue 应用其实就是许多组件组成的‘组件树’。\n模块,多是缩写的 .js 文件,每个模块实现了不同的功能,可以通过相应的方式引入别的模块,及暴露自身供外部模块使用的功能。它是一个封装的概念,比如模块 a 引入了模块 b(当然也可不引入),并包含 1、 2、 3 三个功能,模块 a 可以选择只暴露 1、 3 两个功能给外部使用。\n在编写 vue 组件的过程中,我们常会引用到不同的功能模块,比如,我们引入一个时间模块,用来格式化当前的时间,引入一个排序模块,用来给某个数组进行排序等等。\n预编译器 什么是预编译器?顾名思义,它也是一种编译器,一般用于资源打包前,比如将 .scss 样式文件编译成 .css 文件。 dart 就是目前使用最多的一种 scss 的预编译器,只需要在打包工具中,配置好,打包过程中,就会自动完成这个转译的过程。\n扩展一下,就会发现,程序这个东东经常做这种转来转去的东西。一方面为了程序员编写方便,维护方便,另一方面为了编译器、计算机能理解执行。\n一度连 .html 文件都有相应的编译器,如 jade ,现在不怎么使用了。\n后端是什么 在前后端分离之后,后端方面主要就是接受用户在前端发出的请求,分析处理,该请求数据库的请求数据库,该拒绝响应的拒绝响应,把相应的处理结果返回给前端,前端使用这些数据进行页面渲染或其它处理。\n有一点需要注意的是,无论是后端的服务包,还是前端的项目包,都是需要部署在服务器端的,用户只是通过浏览器访问它们。\n当然,后端开发也经历了一系列的变革,有了不少好用的框架工具,比如 java 领域集大成的 spring 家族框架。\n关于 node node 让 js 也可以在服务器端大展拳脚,它与用在浏览器中的 js 有什么区别呢?其实,语言本身没有什么很大的区别,毕竟基本上都遵守 ecmascript 标准。它们主要是宿主(运行环境)不同,nodejs 的宿主是 v8 ,前端 js 的宿主则是浏览器,仅些而已。\n内容看着不少,其实,刚开始的时候,只需要学习一下 npm 包管理方面的知识就可以了。多数时候,只需要使用它安装项目所需要的第三方工具包。\n学习路线 - html、 css、 js 的基本使用\r- npm 的相关概念\r- vuejs 的模板语法及结构\r- webpack 的了解 结语 以上内容,只是一个大概的、过程性的描述,并没有牵扯到具体的开发细节。但是,理解需要学习什么,为什么学习,也是一件重要的事情。我们对于不熟悉的事物,多数时候有些过于‘敬畏’,这是正常的,一旦揭开了它的面纱,就会发现,一切不过如此。\n","date":"2023-03-28","permalink":"https://loveminimal.github.io/posts/web-front-end/","summary":"\u003cp\u003e本文不涉及具体的细节,浅谈一下 Web 前端的 1 2 3 ……\u003c/p\u003e","title":"前端浅谈"},{"content":"在 《一场“疲惫”的主题制作之旅》 中,已经有了不少博客相关的碎碎念。这里,主要用来浅谈一下当前站点博客系统的 =搭建、编辑及部署 相关的系列流程。\n当前站点,使用 hugo 静态博客生成系统 驱动,部署在个人服务器上。事实上,你可以把生成的站点项目部署在任何可以被访问的地方(比如 github page),它本质上是一个包含了若干 .html 文件及相关静态资源的文件夹。\n更新日志 - 2023-04-05 21:31 添加新的日常编辑方式 - 使用 typora \u0026gt; 更新日志仅是为了记录每篇文章的更新过程\n准备篇 ……\n搭建篇 ……\n编辑篇 这里,我们以当前文章的创建及编辑过程为例。\n创建文章 = 怎么说呢?这个章节写的过于太细节了……好像…… 😅\n我们可以使用以下命令来创建文章:\nhugo new posts/how-do-i-blog/index.md\t# 推荐 # 或 hugo new posts/how-do-i-blog.md\t# 不推荐 这里我们使用第一条命令,该命令会自动生成如下目录层级下 .md 文件。\ncontent\r├── posts\r│ ├── how-do-i-blog\r│ │ ├── imgs\r│ │ │ └── 1aa09c580e674b09e82c722a3689d280012f2ae6e1700e924deeef558347d91a.png\r│ │ └── index.md 为什么不直接使用 how-do-i-blog.md ,而使用 how-do-i-blog/index.md ?\n正如上述目录层级中所反映的,如此方便我们把当前文件所需要的资源(如图片 imgs )都放在当前文章的层级下,方便管理。\n这样做还有额外的好处,我们将在后面 插入图片 的部分进一步说明。\n为什么我们直接手动创建 .md 文件呢?\n事实上,你完全可以手动创建它,但手动创建出的 index.md 文件是空白的,而通过上述命令创建出的 index.md 文件则会包含类似以下内容文件头:\n---\rtitle: \u0026#34;how do i blog\u0026#34;\rdate: 2023-03-06t09:40:02+08:00\rdraft: false\rcategories: [_misc]\rtags: []\rcard: false\rweight: 0\r--- 它们包含了一些 hugo 生成文章时要使用到的信息,我们可以把 title 修改为自己喜欢的文章标题,并为其设置分类和标签等。以下,是我们修改之后的文章头信息:\n---\rtitle: \u0026#34;我是怎么写博客的\u0026#34;\rdate: 2023-03-06t09:40:02+08:00\rdraft: false\rcategories: [关于]\rtags: []\rcard: false\rweight: 0\r--- 我们这里把文章名称改为了 我是怎么写博客的 ,并把它分类到 关于 类别中。\n这些头信息是怎么生成的?在哪里配置它们?\n它位于站点根目录下的 /archetypes/default.md 中,该文件默不存在。一般会启用你所使用主题下的 themes/virgo/archetypes/default.md 文件。\n.\r├── archetypes\r│ ├── default.md 以下是 loveminimal/hugo-theme-virgo 主题中的 default.md 内容:\n---\rtitle: \u0026#34;{{ replace .name \u0026#34;-\u0026#34; \u0026#34; \u0026#34; | title }}\u0026#34;\rdate: {{ .date }}\rdraft: false\rcategories: [_misc]\rtags: []\rcard: false\rweight: 0\r--- 你可以修改它,或者在站点根目录下,创建新的 default.md 文件,并修改为自己喜欢的初始配置。\n插入图片 在文章中插入图片是一个相对高频的操作。在第三方的博客平台中,一般来说直接复制图片并粘贴到要插入的位置就可以了,很方便。而编辑 .md 文件,插入图片就稍微麻烦一些。\n我们通过 ![图片名称](地址链接) 在文章中插入图片,默认情况下,你需要经过:\n1. 搜索图片\r2. 另存图片到本地\r3. 编辑 `![图片名称](地址链接)` 引用\r…… 很繁琐!\n而且还不能控制图片的‘显示’尺寸,需要插入图片数量过多的时候,简直就是一种折磨了。\n有没有一种更好的方式来插入图片呢?\n很幸运,有!\n我平时是使用 vscode 来管理站点内容和编辑 .md 文件的,其中有一款插件很好地解决了这个问题。\n它提供了丰富的自定义设置选项,这里主要用到以下几种:\nmarkdown-image › base: file name format ,设置为 ${hash} ,当然有其它各种格式可选组合; markdown-image › base: image width ,设置为 400 ,默认宽度设为 400px ; markdown-image › local: path ,设置为 ./imgs ,生成的图片放在当前文章同级目录下的 imgs 文件夹中。 这也是上文中我们推荐使用 hugo new posts/how-do-i-blog/index.md 命令来生成文章的原因之一。\n使用该插件,你只需要复制所需要图片(本地或网络图片),并通过其提供的粘贴方式(右键选择)插入到位置即可。如此,你的 .md 文件中,就会插如下内容:\n\u0026lt;img alt=\u0026#34;picture 3\u0026#34; src=\u0026#34;imgs/30737f6467ed6269eed8911b8a915f47b9fed706b8f892efd3271d9b6a76181c.png\u0026#34; width=\u0026#34;400\u0026#34; /\u0026gt; 它会被渲染成下面这张图片,是不是很方便!\n它的原理是什么?\n它会读取你剪切板中刚刚复制的图片数据,在你粘贴的时候,重新生成一份拷贝,并在 .md 文件中,插入对应的图片格式,并引用。真的很方便!🎉\n语法增强 我们在 hugo-theme-virgo 主题中,对 markdown 的语法提供了一些增强功能 - 下划线、文字高亮、标注、折叠板 ,你要以在 《关于 virgo 需要知道的一些事》:标记语法增强 文章中了解它。\n当然,如果你使用其它主题,这些增强的样式是无法生效的,幸运的是,它在其他主题中依然得到渲染 - 使用斜体显示,你并不会丢失你想表达的内容。\n使用 typora 编辑 = 现在的编辑操作就是使用 typora 完成的。 😄\n之前的 vscode 使用的不是很爽吗?为什么切换为 typora 了呢?\nvscode 确实很爽,到目前为止,我也经常使用它。切换到 typora 的原因也很简单,家里的电脑性能不行,新的主机配置还在纠结中……\n更多原因可以查看 一款 typora 主题 中的描述。\nps: vscode 真的是一款非常优秀的编辑器,插件丰富且优质,可扩展性强,可轻可重。\n使用 typora 的感觉怎么样?\n很好!如 一款 typora 主题 中所描述的那样,配合自己制作的主题(由站点样式适配),基本上实现了所见即所得的编辑。另外,typora 本身集成了许多 markdown 相关的快捷键,很直观也很好用,尤其是当你专注于编写内容的时候。\n它还有‘专注模式’和‘打字机模式’,很舒心。\n其内置的图片插入功能也不错,基本上和 插入图片 章节中的实现是相同的,原理没有探究。怎么说呢,typora 上的相对来说,更加符合日常的编辑逻辑,还贴心地增加了可以缩放图片的选项,最最重要的还是‘所见即所得’,你可以实时看到你的图片。\n部署篇 当你想部署你的站点内容到托管平台时,你可能会经过以下步骤:\n1. 执行 `hugo` 命令,生成站点内容,默认放在站点根目录的 `public` 文件夹中;\r2. 复制内容包,上传到托管平台;\r…… 如果,只操作一次的话,不是很复杂,但如果,你的内容更新比较频繁,那就有些烦扰了。内容包的部署方式有很多,各有优缺。\n脚本部署 我们这里,使用脚本部署,一次性配置之后,每次部署只需要执行一条简单的命令即可。分享出来,供大家参考使用。\n我们的站点目录如下所示:\n.\r├── config.toml\r├── content\r│ ├── about\r│ ├── archive.md\r│ ├── _index.md\r│ ├── nav\r│ ├── posts\r│ └── search.md\r├── package.json\r├── readme.md\r├── resources\r│ └── _gen\r├── scripts\r│ ├── deploy.sh\r│ └── gitee.sh 其中, scripts/deploy.sh 便是我们定义的部署脚本,其内容如下:\n#!/bin/sh # ------------------- # deploy posts to `loveminimal.github.io` # ------------------- if [ -d \u0026#34;public\u0026#34; ] then # 如果你是部署到 github ,并绑定了域名,那你可能需要启用该行,以 # 保证其正确的指向 # rm -rf \u0026#34;public/cname\u0026#34; \u0026amp;\u0026amp; cp -r \u0026#34;cname\u0026#34; \u0026#34;public/\u0026#34; # 拷贝内容包 public 到一个临时文件夹 .temp ,并 # 用 git 初始化管理该它 cp -r \u0026#34;public\u0026#34; \u0026#34;../.temp\u0026#34; cd \u0026#34;../.temp\u0026#34; pwd git init git add . git commit -m \u0026#34;posts update.\u0026#34; # 添加远程库,引得我们使用的是个人服务器的仓库地址,如果 # 你是托管在 github 上,那么连接的对应的远程库即可 - \u0026lt;your_username\u0026gt;.github.io # 如果你是在 github ‘政治正确’后创建的库,其默认分支为 main, 那你 # 需要 master:main 而不是 master git remote add origin jack@ovirgo.com:/home/jack/.repo/site.git git push -f origin master # git remote add origin https://github.com/loveminimal/loveminimal.github.io.git # git push -f origin master:main # 清除临时文件夹 cd .. rm -rf \u0026#34;.temp\u0026#34; # 返回站点目录 cd \u0026#34;site\u0026#34; fi 如上所示,只需要在站点根目录下,运行 source scripts/deploy.sh 就可以静待站点部署完成了。\n初看,上述脚本初看可能有些混乱,尤其是对初学者来说。但其实,如果你愿意,理论上你需要:\n更改远程仓库地址; 更改站点目录,即可。 也许你会遇到各种各样的问题,但你也会从中收获到不少的乐趣,不是嘛~~\n命令简化 使用 source scripts/deploy.sh 还是有点太复杂了?有如下限制:\n需要进行到站点根目录才可以运行它; 部署后,站点内的内容包并没得到清理。 哈,你只需要多加几条命令即可,如下:\ncd ~/appdata/roaming/site \u0026amp;\u0026amp; rm -rf public \u0026amp;\u0026amp; hugo \u0026amp;\u0026amp; source scripts/deploy.sh \u0026amp;\u0026amp; rm -rf public 如此,无论当前你在那一种路径,都会:\n自动进入站点根目录(此处是 site); 删除站点下旧的 public (如果有的话); 根据当前内容生成新的 public 内容包; 执行部署脚本,发布到对应托管平台; 清理掉生成的 public 内容包。 emm\u0026hellip; 还是长啊,每次都要键入这么长,太麻烦了,怎么办?\n那么你就需要了解一些关于 bash alias 方面的知识了,如下所示,在当前用户家目录下,创建 .bash_aliases 文件(若无),并添加如下内容:\nalias ssd=\u0026#34;cd ~/appdata/roaming/site \u0026amp;\u0026amp; rm -rf public \u0026amp;\u0026amp; hugo \u0026amp;\u0026amp; source scripts/deploy.sh \u0026amp;\u0026amp; rm -rf public\u0026#34; 保存后,在用户家目录下,运行 source .bashrc 命令使 .bash_aliases 中的别名生效。\nok,现在,当你想部署站点的时候,你只需要运行键入 ssd ,回车即可完成部署。\n‘用户家目录’是什么?\n在 windows 下,有两个家目录:\n用户家目录,如 c:\\users\\jack ,一些软件的默认配置会放在该目录下; 用户漫游家目录,如 c:\\users\\jack\\appdata\\roaming ,另一些软件的配置又会放在这个目录下。 😅 微软的东西真的有点混乱哈。\n如果,你使用的是 gnu/linux 系统,那么家目录就只有一个喽,如 /home/jack 。\n","date":"2023-03-06","permalink":"https://loveminimal.github.io/posts/how-do-i-blog/","summary":"\u003cp\u003e在 \u003ca href=\"../a-theme-making-journey\"\u003e《一场“疲惫”的主题制作之旅》\u003c/a\u003e 中,已经有了不少博客相关的碎碎念。这里,主要用来浅谈一下当前站点博客系统的 \u003cem\u003e=搭建、编辑及部署\u003c/em\u003e 相关的系列流程。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/how-do-i-blog/imgs/1aa09c580e674b09e82c722a3689d280012f2ae6e1700e924deeef558347d91a.png\" width=\"300\" /\u003e \r\n\u003cp\u003e当前站点,使用 \u003ca href=\"https://gohugo.io/\"\u003eHugo 静态博客生成系统\u003c/a\u003e 驱动,部署在个人服务器上。事实上,你可以把生成的站点项目部署在任何可以被访问的地方(比如 \u003ccode\u003eGithub Page\u003c/code\u003e),它本质上是一个包含了若干 \u003ccode\u003e.html\u003c/code\u003e 文件及相关静态资源的文件夹。\u003c/p\u003e","title":"我是怎么写博客的"},{"content":"emoji(えもじ 绘文字),就是表情符号,来自日语词汇“絵文字”(假名为“えもじ”,读音即 emoji)。最早是由栗穰崇于 1999 年创作,并在日本网络及手机用户中流行。自苹果公司发布的 ios5 输入法中加入 emoji 后,这种表情符号开始席卷全球,现已被大多数计算机系统所兼容的 unicode 编码采纳,得以普遍运用。\n💡 loveminimal/emojing: emojing - github\n现代浏览器对 emoji 的支持越来越广泛,并且 emoji 也很有趣!\n先前一直使用 emoji 表情符号大全 ,本来布局很紧凑,某天打开就变大了…… 这就很🤕,自己撰一个,以便使用。\n这里,我们实现一个简单的复制 emoji 的页面 🎉 ➭ emojing 。\n用法 它本身就是一个工具页,你可以很方便地使用它 emojing 。\n配置 config.js :\nexport default { en: false } 默认为中文界面,如果你想设置为英文,设置 en: true 即可。\n预览 记录 我们用到了插件 clipboard.js 和 toastify ,并参考完善了 emoji 表情大全_武恩赐的博客 的 emoji 表情集合。\n","date":"2022-11-15","permalink":"https://loveminimal.github.io/posts/emojing/","summary":"\u003cp\u003eEmoji(えもじ 绘文字),就是表情符号,来自日语词汇“絵文字”(假名为“えもじ”,读音即 emoji)。最早是由栗穰崇于 1999 年创作,并在日本网络及手机用户中流行。自苹果公司发布的 ios5 输入法中加入 emoji 后,这种表情符号开始席卷全球,现已被大多数计算机系统所兼容的 Unicode 编码采纳,得以普遍运用。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/loveminimal/emojing\"\u003e💡 loveminimal/emojing: Emojing - GitHub\u003c/a\u003e\u003c/p\u003e","title":"emojing"},{"content":" 🔔 本文摘录自 如何维护更新日志 \u0026ndash; 更新日志绝对不应该是 git 日志的堆砌物\n简介 version 1.0.0\n# changelog\rall notable changes to this project will be documented in this file.\rthe format is based on [keep a changelog](https://keepachangelog.com/en/1.0.0/),\rand this project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).\r## [unreleased]\r## [1.0.0] - 2017-06-20\r### added\r- new visual identity by [@tylerfortune8](https://github.com/tylerfortune8).\r- version navigation.\r- links to latest released version in previous versions.\r- \u0026#34;why keep a changelog?\u0026#34; section.\r- \u0026#34;who needs a changelog?\u0026#34; section.\r- \u0026#34;how do i make a changelog?\u0026#34; section.\r- \u0026#34;frequently asked questions\u0026#34; section.\r- new \u0026#34;guiding principles\u0026#34; sub-section to \u0026#34;how do i make a changelog?\u0026#34;.\r- simplified and traditional chinese translations from [@tianshuo](https://github.com/tianshuo).\r- german translation from [@mpbzh](https://github.com/mpbzh) \u0026amp; [@art4](https://github.com/art4).\r- italian translation from [@azkidenz](https://github.com/azkidenz).\r- swedish translation from [@magol](https://github.com/magol).\r- turkish translation from [@karalamalar](https://github.com/karalamalar).\r- french translation from [@zapashcanon](https://github.com/zapashcanon).\r- brazilian portugese translation from [@webysther](https://github.com/webysther).\r- polish translation from [@amielucha](https://github.com/amielucha) \u0026amp; [@m-aciek](https://github.com/m-aciek).\r- russian translation from [@aishek](https://github.com/aishek).\r- czech translation from [@h4vry](https://github.com/h4vry).\r- slovak translation from [@jkostolansky](https://github.com/jkostolansky).\r- korean translation from [@pierceh89](https://github.com/pierceh89).\r- croatian translation from [@porx](https://github.com/porx).\r- persian translation from [@hameds](https://github.com/hameds).\r- ukrainian translation from [@osadchyi-s](https://github.com/osadchyi-s).\r### changed\r- start using \u0026#34;changelog\u0026#34; over \u0026#34;change log\u0026#34; since it\u0026#39;s the common usage.\r- start versioning based on the current english version at 0.3.0 to help\rtranslation authors keep things up-to-date.\r- rewrite \u0026#34;what makes unicorns cry?\u0026#34; section.\r- rewrite \u0026#34;ignoring deprecations\u0026#34; sub-section to clarify the ideal\rscenario.\r- improve \u0026#34;commit log diffs\u0026#34; sub-section to further argument against\rthem.\r- merge \u0026#34;why can’t people just use a git log diff?\u0026#34; with \u0026#34;commit log\rdiffs\u0026#34;\r- fix typos in simplified chinese and traditional chinese translations.\r- fix typos in brazilian portuguese translation.\r- fix typos in turkish translation.\r- fix typos in czech translation.\r- fix typos in swedish translation.\r- improve phrasing in french translation.\r- fix phrasing and spelling in german translation.\r### removed\r- section about \u0026#34;changelog\u0026#34; vs \u0026#34;changelog\u0026#34;.\r## [0.3.0] - 2015-12-03\r### added\r- ru translation from [@aishek](https://github.com/aishek).\r- pt-br translation from [@tallesl](https://github.com/tallesl).\r- es-es translation from [@zeliosariex](https://github.com/zeliosariex).\r## [0.2.0] - 2015-10-06\r### changed\r- remove exclusionary mentions of \u0026#34;open source\u0026#34; since this project can\rbenefit both \u0026#34;open\u0026#34; and \u0026#34;closed\u0026#34; source projects equally.\r## [0.1.0] - 2015-10-06\r### added\r- answer \u0026#34;should you ever rewrite a change log?\u0026#34;.\r### changed\r- improve argument against commit logs.\r- start following [semver](https://semver.org) properly.\r## [0.0.8] - 2015-02-17\r### changed\r- update year to match in every readme example.\r- reluctantly stop making fun of brits only, since most of the world\rwrites dates in a strange way.\r### fixed\r- fix typos in recent readme changes.\r- update outdated unreleased diff link.\r## [0.0.7] - 2015-02-16\r### added\r- link, and make it obvious that date format is iso 8601.\r### changed\r- clarified the section on \u0026#34;is there a standard change log format?\u0026#34;.\r### fixed\r- fix markdown links to tag comparison url with footnote-style links.\r## [0.0.6] - 2014-12-12\r### added\r- readme section on \u0026#34;yanked\u0026#34; releases.\r## [0.0.5] - 2014-08-09\r### added\r- markdown links to version tags on release headings.\r- unreleased section to gather unreleased changes and encourage note\rkeeping prior to releases.\r## [0.0.4] - 2014-08-09\r### added\r- better explanation of the difference between the file (\u0026#34;changelog\u0026#34;)\rand its function \u0026#34;the change log\u0026#34;.\r### changed\r- refer to a \u0026#34;change log\u0026#34; instead of a \u0026#34;changelog\u0026#34; throughout the site\rto differentiate between the file and the purpose of the file — the\rlogging of changes.\r### removed\r- remove empty sections from changelog, they occupy too much space and\rcreate too much noise in the file. people will have to assume that the\rmissing sections were intentionally left out because they contained no\rnotable changes.\r## [0.0.3] - 2014-08-09\r### added\r- \u0026#34;why should i care?\u0026#34; section mentioning the changelog podcast.\r## [0.0.2] - 2014-07-10\r### added\r- explanation of the recommended reverse chronological release ordering.\r## [0.0.1] - 2014-05-31\r### added\r- this changelog file to hopefully serve as an evolving example of a\rstandardized open source project changelog.\r- cname file to enable github pages custom domain\r- readme now contains answers to common questions about changelogs\r- good examples and basic guidelines, including proper date formatting.\r- counter-examples: \u0026#34;what makes unicorns cry?\u0026#34;\r[unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...head\r[1.0.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...v1.0.0\r[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0\r[0.2.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.1.0...v0.2.0\r[0.1.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.8...v0.1.0\r[0.0.8]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.7...v0.0.8\r[0.0.7]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.6...v0.0.7\r[0.0.6]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.5...v0.0.6\r[0.0.5]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.4...v0.0.5\r[0.0.4]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.3...v0.0.4\r[0.0.3]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.2...v0.0.3\r[0.0.2]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.1...v0.0.2\r[0.0.1]: https://github.com/olivierlacan/keep-a-changelog/releases/tag/v0.0.1 更新日志是什么?\n更新日志(change log)是一个由人工编辑,以时间为倒序的列表, 以记录一个项目中所有版本的显著变动。\n为何要提供更新日志?\n为了让用户和开发人员更简单明确的知晓项目在不同版本之间有哪些显著变动。\n哪些人需要更新日志?\n人人需要更新日志。无论是消费者还是开发者,软件的最终用户都关心软件所包含什么。 当软件有所变动时,大家希望知道改动是为何、以及如何进行的。\n怎样制作高质量的更新日志? 指导原则:\n记住日志是写给人的,而非机器 每个版本都应该有独立的入口 同类改动应该分组放置 版本与章节应该相互对应 新版本在前,旧版本在后 应包括每个版本的发布日期 注明是否遵守 语义化版本格式 变动类型 备注 added 新添加的功能 changed 对现有功能的变更 deprecated 已经不建议使用,准备很快移除的功能 removed 已经移除的功能 fixed 对 bug 的修复 security 对安全的改进 \u0026gt; 表:变动类型\n如何减少维护更新日志的精力? 在文档最上方提供 unreleased 区块以记录即将发布的更新内容。\n这样有两大意义:\n大家可以知道在未来版本中可能会有哪些变更 在发布新版本时,可以直接将 unreleased 区块中的内容移动至新发布版本的描述区块就可以了 有很糟糕的更新日志吗? 当然有,下面就是一些糟糕的方式。\n使用 git 日志\n使用 git 日志作为更新日志是个非常糟糕的方式:git 日志充满各种无意义的信息, 如合并提交、语焉不详的提交标题、文档更新等。\n提交的目的是记录源码的演化。 一些项目会清理提交记录,一些则不会。\n更新日志的目的则是记录重要的变更以供最终受众阅读,而记录范围通常涵盖多次提交。\n无视即将弃用功能\n当从一个版本升级至另一个时,人们应清楚(尽管痛苦)的知道哪些部分将出现问题。 应该允许先升级至一个列出哪些功能将会被弃用的版本,待去掉那些不再支持的部分后, 再升级至把那些弃用功能真正移除的版本。\n即使其他什么都不做,也要在更新日志中列出 derecations,removals 以及其他重大变动。\n易混淆的日期格式\n在美国,人们将月份写在日期的开头 (06-02-2012 对应 2012 年 6 月 2 日), 与此同时世界上其他地方的很多人将至写作 2 june 2012,并拥有不同发音。 2012-06-02 从大到小的排列符合逻辑,并不与其他日期格式相混淆,而且还 符合 iso 标准。因此,推荐在更新日志中采用使用此种日期格式。\n还有更多内容。请通过 发布问题 或发布 pull 请求帮助我收集更多异常模式。\nfaq 是否有一个标准化的更新日志格式?\n并没有。虽然 gnu 提供了更新日志样式指引,以及那个仅有两段长的 gnu news 文件“指南”, 但两者均远远不够。\n此项目意在提供一个 更好的更新日志惯例 所有点子都来自于在开源社区中对优秀实例的观察与记录。\n对于所有建设性批评、讨论及建议,我们都非常 欢迎。\n更新日志文件应被如何命名?\n可以叫做 changelog.md。 一些项目也使用 history、news 或 releases。\n当然,你可以认为更新日志的名字并不是什么要紧事,但是为什么要为难那些仅仅是想看到都有哪些重大变更的最终用户呢?\n对于 github 发布呢?\n这是个非常好的倡议。releases 可通过手动添加发布日志或将带 有注释的 git 标签信息抓取后转换的方式,将简单的 git 标签(如一个叫 v1.0.0 的标签) 转换为信息丰富的发布日志。\ngithub 发布会创建一个非便携、仅可在 github 环境下显示的更新日志。尽管会花费更 多时间,但将之处理成更新日志格式是完全可能的。\n现行版本的 github 发布不像哪些典型的大写文件 (readme, contributing, etc.),仍可以认为是不利于最终用户探索的。 另一个小问题则是界面并不提供不同版本间 commit 日志的链接。\n更新日志可以被自动识别吗?\n非常困难,因为有各种不同的文件格式和命名。\nvandamme 是一个 ruby 程序,由 gemnasium 团队制作,可以解析多种 (但绝对不是全部)开源库的更新日志。\n那些后来撤下的版本怎么办?\n因为各种安全/重大 bug 原因被撤下的版本被标记 \u0026lsquo;yanked\u0026rsquo;。 这些版本一般不出现在更新日志里,但建议他们出现。 显示方式应该是: ## 0.0.5 - 2014-12-13 [yanked] 。\n[yanked] 的标签应该非常醒目。人们应该非常容易就可以注意到他。 并且被方括号所包围也使其更易被程序识别。\n是否可以重写更新日志?\n当然可以。总会有多种多样的原因需要我们去改进更新日志。 对于那些有着未维护更新日志的开源项目,我会定期打开 pull 请求以加入缺失的发布信息。\n另外,很有可能你发现自己忘记记录一个重大功能更新。这种情况下显然你应该去重写更新日志。\n如何贡献?\n本文档并非真理。而是我深思熟虑后的建议,以及我收集的信息与典例。\n我希望我们的社区可以对此达成一致。我相信讨论的过程与最终结果一样重要。\n所以欢迎 贡献.\n访谈 我在 更新日志播客 上讲述了为何维护者与贡献者应关心更新日志, 以及支持我进行此项目的诸多因素。\n","date":"2022-11-02","permalink":"https://loveminimal.github.io/posts/keep-changelog/","summary":"\u003cimg alt=\"picture 14\" src=\"/posts/keep-changelog/imgs/de78a20f0efae9c4f2c0432200dfe721f1007dc1c9af179b9c33d842f20d1e7b.png\" width=\"300\" /\u003e \r\n\u003cp\u003e🔔 本文摘录自 \u003ca href=\"https://keepachangelog.com/zh-CN/1.0.0/\"\u003e如何维护更新日志 \u0026ndash; 更新日志绝对不应该是 git 日志的堆砌物\u003c/a\u003e\u003c/p\u003e","title":"如何维护更新日志"},{"content":" i.e.semantic versioning\n🔔 本文摘录自 语义化版本 2.0.0 | semantic versioning - semver.org\n= emm\u0026hellip; 直接借用文章中的一段话 \u0026ndash; 为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。*\n摘要 版本格式: 主版本号.次版本号.修订号 ,版本号递增规则如下:\n主版本号:当你做了不兼容的 api 修改, 次版本号:当你做了向下兼容的功能性新增, 修订号:当你做了向下兼容的问题修正。 先行版本号及版本编译信息可以加到“主版本号。次版本号。修订号”的后面,作为延伸。\n简介 在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。\n= 在维护陈旧的历史项目时尤为让人烦扰……*\n在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。\n作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。 为了让这套理论运作,你必须先有定义好的公共 api。 这可能包括文档或代码的强制要求。无论如何,这套 api 的清楚明了是十分重要的。一旦你定义了公共 api,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式: x.y.z(主版本号。次版本号。修订号) 修复问题但不影响 api 时,递增修订号;api 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。\n我称这套系统为 _“语义化的版本控制” ,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。\n语义化版本控制规范(semver) 以下关键词 must、must not、required、shall、shall not、should、should not、 recommended、may、optional 依照 rfc 2119 的叙述解读。\n使用语义化版本控制的软件必须(must)定义公共 api。 该 api 可以在代码中被定义或出现于严谨的文档内。无论何种形式都应该力求精确且完整。 标准的版本号必须(must)采用 x.y.z 的格式,其中 x、y 和 z 为非负的整数,且禁止(must not)在数字前方补零。x 是主版本号、y 是次版本号、而 z 为修订号。每个元素必须(must)以数值来递增。例如: 1.9.1 -\u0026gt; 1.10.0 -\u0026gt; 1.11.0。 标记版本号的软件发行后,禁止(must not)改变该版本软件的内容。任何修改都必须(must)以新版本发行。 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 api 不应该被视为稳定版。 1.0.0 的版本号用于界定公共 api 的形成。 这一版本之后所有的版本号更新都基于公共 api 及其修改内容。 修订号 z(x.y.z | x \u0026gt; 0)必须(must)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。 次版本号 y(x.y.z | x \u0026gt; 0)必须(must)在有向下兼容的新功能出现时递增。在任何公共 api 的功能被标记为弃用时也必须(must)递增。也可以(may)在内部程序有大量新功能或改进被加入时递增,其中可以(may)包括修订级别的改变。每当次版本号递增时,修订号必须(must)归零。 主版本号 x(x.y.z | x \u0026gt; 0)必须(must)在有任何不兼容的修改被加入公共 api 时递增。其中可以(may)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(must)归零。 先行版本号可以(may)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(must)由 ascii 字母数字和连接号 [0-9a-za-z-] 组成,且禁止(must not)留白。数字型的标识符禁止(must not)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、 1.0.0-alpha.1、 1.0.0-0.3.7、 1.0.0-x.7.z.92。 版本编译信息可以(may)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(must)由 ascii 字母数字和连接号 [0-9a-za-z-] 组成,且禁止(must not)留白。当判断版本的优先层级时,版本编译信息可(should)被忽略。因此当两个版本只有在版本编译信息有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、 1.0.0+20130313144700、 1.0.0-beta+exp.sha.5114f85。 版本的优先层级指的是不同版本在排序时如何比较。 判断优先层级时,必须(must)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。 由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较。例如:1.0.0 \u0026lt; 2.0.0 \u0026lt; 2.1.0 \u0026lt; 2.1.1。 当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。 有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(must)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定: 只有数字的标识符以数值高低比较。 有字母或连接号时则逐字以 ascii 的排序来比较。 数字的标识符比非数字的标识符优先层级低。 若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。例如:1.0.0-alpha \u0026lt; 1.0.0-alpha.1 \u0026lt; 1.0.0-alpha.beta \u0026lt; 1.0.0-beta \u0026lt; 1.0.0-beta.2 \u0026lt; 1.0.0-beta.11 \u0026lt; 1.0.0-rc.1 \u0026lt; 1.0.0。 合法语义化版本的巴科斯范式语法 巴科斯范式 以美国人巴科斯 (backus) 和丹麦人诺尔 (naur) 的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯-诺尔形式 (backus-naur form)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。\rbnf 表示语法规则的方式为:非终结符用尖括号括起。每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以 “::=” 分开。具有相同左部的规则可以共用一个左部,各右部之间以直竖 “|” 隔开。 = 规范!规范!还是 tmd 规范!*\n\u0026lt;valid semver\u0026gt; ::= \u0026lt;version core\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r\u0026lt;version core\u0026gt; ::= \u0026lt;major\u0026gt; \u0026#34;.\u0026#34; \u0026lt;minor\u0026gt; \u0026#34;.\u0026#34; \u0026lt;patch\u0026gt;\r…… \u0026lt;valid semver\u0026gt; ::= \u0026lt;version core\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r| \u0026lt;version core\u0026gt; \u0026#34;-\u0026#34; \u0026lt;pre-release\u0026gt; \u0026#34;+\u0026#34; \u0026lt;build\u0026gt;\r\u0026lt;version core\u0026gt; ::= \u0026lt;major\u0026gt; \u0026#34;.\u0026#34; \u0026lt;minor\u0026gt; \u0026#34;.\u0026#34; \u0026lt;patch\u0026gt;\r\u0026lt;major\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;minor\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;patch\u0026gt; ::= \u0026lt;numeric identifier\u0026gt;\r\u0026lt;pre-release\u0026gt; ::= \u0026lt;dot-separated pre-release identifiers\u0026gt;\r\u0026lt;dot-separated pre-release identifiers\u0026gt; ::= \u0026lt;pre-release identifier\u0026gt;\r| \u0026lt;pre-release identifier\u0026gt; \u0026#34;.\u0026#34; \u0026lt;dot-separated pre-release identifiers\u0026gt;\r\u0026lt;build\u0026gt; ::= \u0026lt;dot-separated build identifiers\u0026gt;\r\u0026lt;dot-separated build identifiers\u0026gt; ::= \u0026lt;build identifier\u0026gt;\r| \u0026lt;build identifier\u0026gt; \u0026#34;.\u0026#34; \u0026lt;dot-separated build identifiers\u0026gt;\r\u0026lt;pre-release identifier\u0026gt; ::= \u0026lt;alphanumeric identifier\u0026gt;\r| \u0026lt;numeric identifier\u0026gt;\r\u0026lt;build identifier\u0026gt; ::= \u0026lt;alphanumeric identifier\u0026gt;\r| \u0026lt;digits\u0026gt;\r\u0026lt;alphanumeric identifier\u0026gt; ::= \u0026lt;non-digit\u0026gt;\r| \u0026lt;non-digit\u0026gt; \u0026lt;identifier characters\u0026gt;\r| \u0026lt;identifier characters\u0026gt; \u0026lt;non-digit\u0026gt;\r| \u0026lt;identifier characters\u0026gt; \u0026lt;non-digit\u0026gt; \u0026lt;identifier characters\u0026gt;\r\u0026lt;numeric identifier\u0026gt; ::= \u0026#34;0\u0026#34;\r| \u0026lt;positive digit\u0026gt;\r| \u0026lt;positive digit\u0026gt; \u0026lt;digits\u0026gt;\r\u0026lt;identifier characters\u0026gt; ::= \u0026lt;identifier character\u0026gt;\r| \u0026lt;identifier character\u0026gt; \u0026lt;identifier characters\u0026gt;\r\u0026lt;identifier character\u0026gt; ::= \u0026lt;digit\u0026gt;\r| \u0026lt;non-digit\u0026gt;\r\u0026lt;non-digit\u0026gt; ::= \u0026lt;letter\u0026gt;\r| \u0026#34;-\u0026#34;\r\u0026lt;digits\u0026gt; ::= \u0026lt;digit\u0026gt;\r| \u0026lt;digit\u0026gt; \u0026lt;digits\u0026gt;\r\u0026lt;digit\u0026gt; ::= \u0026#34;0\u0026#34;\r| \u0026lt;positive digit\u0026gt;\r\u0026lt;positive digit\u0026gt; ::= \u0026#34;1\u0026#34; | \u0026#34;2\u0026#34; | \u0026#34;3\u0026#34; | \u0026#34;4\u0026#34; | \u0026#34;5\u0026#34; | \u0026#34;6\u0026#34; | \u0026#34;7\u0026#34; | \u0026#34;8\u0026#34; | \u0026#34;9\u0026#34;\r\u0026lt;letter\u0026gt; ::= \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34; | \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;g\u0026#34; | \u0026#34;h\u0026#34; | \u0026#34;i\u0026#34; | \u0026#34;j\u0026#34;\r| \u0026#34;k\u0026#34; | \u0026#34;l\u0026#34; | \u0026#34;m\u0026#34; | \u0026#34;n\u0026#34; | \u0026#34;o\u0026#34; | \u0026#34;p\u0026#34; | \u0026#34;q\u0026#34; | \u0026#34;r\u0026#34; | \u0026#34;s\u0026#34; | \u0026#34;t\u0026#34;\r| \u0026#34;u\u0026#34; | \u0026#34;v\u0026#34; | \u0026#34;w\u0026#34; | \u0026#34;x\u0026#34; | \u0026#34;y\u0026#34; | \u0026#34;z\u0026#34; | \u0026#34;a\u0026#34; | \u0026#34;b\u0026#34; | \u0026#34;c\u0026#34; | \u0026#34;d\u0026#34;\r| \u0026#34;e\u0026#34; | \u0026#34;f\u0026#34; | \u0026#34;g\u0026#34; | \u0026#34;h\u0026#34; | \u0026#34;i\u0026#34; | \u0026#34;j\u0026#34; | \u0026#34;k\u0026#34; | \u0026#34;l\u0026#34; | \u0026#34;m\u0026#34; | \u0026#34;n\u0026#34;\r| \u0026#34;o\u0026#34; | \u0026#34;p\u0026#34; | \u0026#34;q\u0026#34; | \u0026#34;r\u0026#34; | \u0026#34;s\u0026#34; | \u0026#34;t\u0026#34; | \u0026#34;u\u0026#34; | \u0026#34;v\u0026#34; | \u0026#34;w\u0026#34; | \u0026#34;x\u0026#34;\r| \u0026#34;y\u0026#34; | \u0026#34;z\u0026#34; = 版本驱动开发 edd ?有点意思……*\n为什么要使用语义化的版本控制? 这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。 问题在于只是“近似”还不够。 如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。\n举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函数库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能,你可以放心地指定依赖于梯子的版本号大于等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。\n作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。\n如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函数库正在使用它并遵循这些规则就可以了。请在你的 readme 文件中保留此页链接,让别人也知道这些规则并从中受益。\nfaq 在 0.y.z 初始开发阶段,我该如何进行版本控制?\n最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。\n如何判断发布 1.0.0 版本的时机?\n当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 api 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。\n这不会阻碍快速开发和迭代吗?\n主版本号为零的时候就是为了做快速开发。如果你每天都在改变 api,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。\n对于公共 api,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?\n这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。\n为整个公共 api 写文档太费事了!\n为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。保持项目高效的一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 api 有良好规范的坚持,可以让每个人及每件事都运行顺畅。\n万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?\n一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。\n= git 也是这种思想,每一次修改都是一次新增!*\n如果我更新了自己的依赖但没有改变公共 api 该怎么办?\n由于没有影响到公共 api,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。\n如果我变更了公共 api 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)\n自行做最佳的判断。如果你有庞大的使用者群在依照公共 api 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。\n我该如何处理即将弃用的功能?\n弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 api 时,你应该做两件事:(1)更新你的文档让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 api。\n语义化版本对于版本的字符串长度是否有限制呢?\n没有,请自行做适当的判断。举例来说,长到 255 个字符的版本已过度夸张。再者,特定的系统对于字符串长度可能会有他们自己的限制。\n“v1.2.3” 是一个语义化版本号吗?\n“v1.2.3” 并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。比如: git tag v1.2.3 -m \u0026quot;release version 1.2.3\u0026quot; 中,“v1.2.3” 表示标签名称,而 “1.2.3” 是语义化版本号。\n是否有推荐的正则表达式用以检查语义化版本号的正确性?\n有两个推荐的正则表达式。第一个用于支持按组名称提取的语言(pcre[perl 兼容正则表达式,比如 perl、php 和 r]、python 和 go)。\n参见:https://regex101.com/r/ly7o1x/3/\n^(?p\u0026lt;major\u0026gt;0|[1-9]\\d*)\\.(?p\u0026lt;minor\u0026gt;0|[1-9]\\d*)\\.(?p\u0026lt;patch\u0026gt;0|[1-9]\\d*)(?:-(?p\u0026lt;prerelease\u0026gt;(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*))*))?(?:\\+(?p\u0026lt;buildmetadata\u0026gt;[0-9a-za-z-]+(?:\\.[0-9a-za-z-]+)*))?$ 第二个用于支持按编号提取的语言(与第一个对应的提取项按顺序分别为:major、minor、patch、prerelease、buildmetadata)。主要包括 ecma script(javascript)、pcre(perl 兼容正则表达式,比如 perl、php 和 r)、python 和 go。\n参见:https://regex101.com/r/vkijkf/1/\n^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-za-z-][0-9a-za-z-]*))*))?(?:\\+([0-9a-za-z-]+(?:\\.[0-9a-za-z-]+)*))?$ 关于 语义化版本控制的规范是由 gravatars 创办者兼 github 共同创办者 tom preston-werner 所建立。\n如果您有任何建议,请到 github 上提出您的问题。\n许可证 知识共享 署名 3.0 (cc by 3.0)\n","date":"2022-11-02","permalink":"https://loveminimal.github.io/posts/semantic-versioning/","summary":"\u003cimg alt=\"picture 10\" src=\"/posts/semantic-versioning/imgs/f9ff34a8e93cc860f20282e5587cafd221ef73f0d25b926452bec4b7d1dfcb96.png\" width=\"300\" /\u003e \r\n\u003cp\u003ei.e.Semantic Versioning\u003c/p\u003e\n\u003cp\u003e🔔 本文摘录自 \u003ca href=\"https://semver.org/lang/zh-CN/\"\u003e语义化版本 2.0.0 | Semantic Versioning - semver.org\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= Emm\u0026hellip; 直接借用文章中的一段话 \u0026ndash; 为供他人使用的软件编写适当的文档,是你作为一名专业开发者应尽的职责。*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"语义化版本"},{"content":"在 《gtd 管理系统》 和 《极简主义生活》 中,我们已经聊过类似的内容,然而“个人管理”终究是个没有止境的话题……\n\u0026gt; 这里不妨抛弃‘结构’,想到哪儿,说到哪儿……\n为什么需要‘个人管理’ 一万年太久,只争朝夕。\n时间看似有很多,却是不经用的很,而且,生活往往如一团乱麻,理不清,扯不顺。稍不留意,哪天顾镜自盼,也许就青丝霜雪了。\n你可以懒,但不能懒不自知,更不应知懒而不欲改之,毕竟,是你的人生不是?且大概率只有一次,不可回溯,就算可以轮回,你又有什么资格透支下辈子的人生呢?自欺欺人,可不是一件值得让人‘骄傲’的事。聪明如你,自不会如此愚蠢的,对吧?\n虽说‘难得糊涂’,却不应‘如此糊涂’,更不应‘一直糊涂’,保持清醒,了解自己。\n‘理清生活’的首要之举便在于探索收集生活中的方方面面,对其有过去及现在有一个基本的了解和掌握,当然,这不是一蹴而就的事情,也完全没有必要毕其功于一役,稍加思考就会发现,它本就贯穿你的一生,因其本就是生活。\n世事纷繁,人生苦短,‘善假物’者多少会轻松些,过程本身也会多些乐趣,毕竟我们不是‘苦行僧’,不用证那‘宏愿道’。\n切记,成功无捷径,必须得有的话,只会是 \u0026ndash; 戒骄戒躁,实事求是 !\n如何管理 如何管理呢?日程表?事务巨细,尽皆预排?这可是一个大工程,基本上是很难坚持下来的,它本身对身心就是一个不小的消耗。相信,关注个人管理的,多少都有过类似的心中历程。费了大气力写的计划,不多日,便只能束之以高阁。叹兮,悲兮,奈若何?\n尽收所有‘悬而未决’之事!何为‘悬而未决’?乱你心者,搅你意者,困你心者,扰你念者!一句话就是,所有,你感觉要解决的或将要解决的,一网打尽,统统先记在‘小本本’上。\n然后,从上而下,逐次分析,是否需要采取行动,不需要行动的该怎么办,需要行动的又当如何做。分了门类,进一步组织管理它们,大而化小,小而具象,行之检之,周而往复。(具体步骤,详见 gtd)\n这里的‘行、检、周往’是很重要的,精进很始于此。生活,本就不应是简单地重复,只要保持向上之心,并身体力行,许多‘看似的后退’,其实质都是‘曲折的前进’。\n=全面收集,认真分析,细致管理,定期回顾,踏实行动,实是缺一不可!\n少即是多 在不断地管理过程中,自然而然地就会发现,我们的生活‘太满了’,以至于显得‘拥挤’,变得‘混乱’。多而无序,杂乱无章,许多‘方便’反而成了‘负担’,以致让人积重难行,疲惫不堪。\n生活,应该“减负”了!\n极简主义生活方式得到了不少人的倾心,我个人也很喜欢这种理念,‘少即是多’,确保有限的选择对处女座简直是一种救赎。当然,我感觉,要追求‘适度的少’,而不是‘绝对的少’,太‘少’了,真的不够用了,反而失去了‘极简主义’本身服务于生活的初心。不能为了‘简’而简,一切最终要回归到生活本身,以人为本。\n适度的‘少’!少于何处?\n其实很简单,从生活的日常开始。如果你经常熬夜,那就要注意了,你肯定有很多需要‘少’的地方,比如,少刷或不刷短视频,少看或不看头条‘震惊榜’,少…… 如此,熬夜‘少’了,睡眠自然‘多’了,精力也就‘多’了,做事情更有效率,收获当然可预期地增‘多’……\n舍得舍得,少即是多哦 ~\n提到极简主义,就很难不涉及到“断、舍、离”,就我理解来看,没必要太纠结这三字分是何意,实则是同出而异名,谓之一物。它的内涵是什么呢?是放下包袱,是停止内耗,是平衡取舍,以达到与自己身心的和解。\n以此,愈精简,愈益于收集、分析和管理,良好的感觉又反馈自身,继而实现更深层次的精简。循环往复,平衡便在其中。\n具体的‘术’ 我们已经确定了基本的宗旨和理念 \u0026ndash; 尽收‘悬而未决’,以至‘少即是多’,如何采取具体的行动呢,有没有‘万能药’拿来一试?\n有,也没有!\n所谓‘有’,是指其理念本身就是一种绝佳的指导;所谓‘没有’,是指从来存在什么‘一剑破万法’,凡事皆需要“具体问题,具体分析”,不可有思想上的怠惰。任何行之有效的具体方法,皆可拿来学习、借鉴和模仿,目的在于体会、理解其真义,但切忌一味地‘照猫画虎’,不求甚解。\n正所谓,“一切具象,皆为虚妄”, 本就没有“可道之常道,可名之常名”。\n如此,‘术’何以求?‘术’乃自生!\n在求‘道’的过程中,真正合适本身的‘术’自然就诞生了。当然,这并不是提倡什么不假外求,固步自封。正相反,‘道’存于心,求之在内、外,老祖宗自古就讲求一个“天人合一”,便是此理。\n未结之语 以上各种,难免‘言不及意’,哈,你不能要求一个初窥门径的人能说的清楚,道的明白,不过是抛砖引玉之举,探讨二三事罢了。\n","date":"2022-10-17","permalink":"https://loveminimal.github.io/posts/personal-management/","summary":"\u003cp\u003e在 \u003ca href=\"../get-things-done/\"\u003e《GTD 管理系统》\u003c/a\u003e 和 \u003ca href=\"../minimalist-lifestyle/\"\u003e《极简主义生活》\u003c/a\u003e 中,我们已经聊过类似的内容,然而“个人管理”终究是个没有止境的话题……\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 这里不妨抛弃‘结构’,想到哪儿,说到哪儿……\u003c/code\u003e\u003c/p\u003e","title":"再谈“个人管理”"},{"content":"近来,准备组装一台 pc 主机,做个记录 ~\n🖥 2022 年电脑配置推荐完整榜单(通用篇)\n🖥 cpu篇:2023年3月台式电脑cpu一文搞定\n说真的,采购对于‘处女座’来说真心不是一件简单的事情,很容易就会陷入到‘内耗’的境地……\n采购单 amd 系 配件 型号 价格 备注 主板 华硕 x570 1599 m-atx 中型主板,供电和散热能力是衡量主板好坏的核心标准 cpu amd5800x3d 2399 要关注 cpu 的接口规格,以便于选取合适的主板 显卡 华硕 n 卡 3060ti 3999 独显 硬盘 三星 1t 980pro 899 m.2 nvme 内存条 金士顿 3200 频 699 ddr4 16*2 电源 航嘉 850w 679 散热 华硕 360 水冷 1299 机箱 爱国者 k 269 atx \u0026gt; m-atx \u0026gt; itx 显示器 三星 27 英寸 2k 1579 已购 总价 12742 intel 系 配件 型号 价格 备注 主板 华硕 x570 1599 m-atx 中型主板,供电和散热能力是衡量主板好坏的核心标准 cpu i5-13600k ¥2599 2599 i7-13700k ¥2999 显卡 华硕 n 卡 3060ti 3999 独显 硬盘 三星 1t 980pro 899 m.2 nvme 内存条 金士顿 3200 频 699 ddr4 16*2 电源 航嘉 850w 679 散热 华硕 360 水冷 1299 机箱 爱国者 k 269 atx \u0026gt; m-atx \u0026gt; itx 显示器 三星 27 英寸 2k 1579 已购 总价 12742 备选配件 配件 型号 备注 支架 显卡支架用于防止显卡卡座垂直变形 散热器 用于给 cpu 散热,绝大多数 cpu 有配套风冷,水冷另外买 机箱扇 用于机箱散热,部分机箱有带,很多带有灯效 当前主机 当前使用 2016 年购入的小米笔记本,外接 2k 显示屏,播放 1080p 分辨率视频都会卡顿……\n配件 型号 备注 cpu i5-6500 显卡 gt 940mx “烂大街”的轻薄本显卡 内存 8g 不够用哦 硬盘 m.2 256gb 可扩展,预留了一个 ssd 卡位 \u0026gt; 2016(首代)小米笔记本 air 13.3 英寸 银色\n配件 cpu cpu 型号解读:教你 cpu 型号后缀怎么看?cpu 型号后面的字母和数字区别是什么?\ncpu ,是一台主机的‘心脏’ ❤️!\n","date":"2022-10-14","permalink":"https://loveminimal.github.io/posts/hi-pc/","summary":"\u003cp\u003e近来,准备组装一台 PC 主机,做个记录 ~\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/83636026\"\u003e🖥 2022 年电脑配置推荐完整榜单(通用篇)\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/425492516\"\u003e🖥 CPU篇:2023年3月台式电脑CPU一文搞定\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e说真的,采购对于‘处女座’来说真心不是一件简单的事情,很容易就会陷入到‘内耗’的境地……\u003c/p\u003e","title":"嗨,pc"},{"content":"感谢小伙伴 whatacold 的创意 a bookmarklet for copying a link as an org-mode link - whatacold\u0026rsquo;s space - whatacold.io ,可以方便地复制当前页面的地址并格式化 .md 链接格式,当然,也可以格式化成 .org 链接格式。\n* 配置链接类型 在源码中的 copy.js 中,2 ~ 10 行,你可以看到如下代码片段。\nlet md = `[${(document.title || document.location.hostname)}](${document.location.href})` let org = `[[${document.location.href}][${(document.title || document.location.hostname)}]]` // ----------------------------------------------------------- // 🛠️ 链接类型设置 // org - org mode link, eg. [[https://ovirgo.com/ship/][ship]] // md - markdown link, eg. [ship](https://ovirgo.com/ship/) let type = md // ----------------------------------------------------------- 这里,我们提供了 .md 和 .org 两种文件的链接格式,并通过 type 来设置它。此处,默认 type 为 md ,如果,你经常编辑的是 .org 文件,只需要把 type 设为 org 即可。\n为什么我们这里不提供一个直接在插件设置中的配置选项?\n首先,通常情况下,频繁地切换编辑 .org 和 .md 文件的场景很少,它们有各自的粘性用户;其次,尽可能的减少操作步骤,不想赋予插件图标过多的功能交互;最后,因为是本地导入,你完全可以拷贝一份文件夹,分别配置,导入成两个插件。\n快速使用 浏览器插件方式 loveminimal/copy-link: a browser extension to copy current url as a markdown link etc.\n下载 copy link 📥 到本地,解压缩后,打开浏览器的‘扩展程序’,打开开发者模式,加载已解压的扩展程序,选择你解压后的文件夹,即可添加插件到浏览器。\n点击图标,或使用 ctrl + b 即可使用该功能。\n标签方式 在浏览器的‘书签管理器’中添加新书签,内容如下图所示:\n以下为压缩并添加前缀之后的代码,复制粘贴至上图剪头处即可。\njavascript:function copy(){const e=document.queryselector(\u0026#34;#btn\u0026#34;),t=document.queryselector(\u0026#34;#ipt\u0026#34;),o=document.queryselector(\u0026#34;#cont\u0026#34;);t.select(),document.execcommand(\u0026#34;copy\u0026#34;)?(document.execcommand(\u0026#34;copy\u0026#34;),console.log(\u0026#34;copy success\u0026#34;),e.innertext=\u0026#34;🎉 success\u0026#34;,e.style.background=\u0026#34;#67c23a\u0026#34;,e.style.color=\u0026#34;#fff\u0026#34;):(console.log(\u0026#34;copy failed\u0026#34;),e.innertext=\u0026#34;❌ faild\u0026#34;,e.style.background=\u0026#34;#f56c6c\u0026#34;,e.style.color=\u0026#34;#fff\u0026#34;),settimeout(()=\u0026gt;{document.body.removechild(o)},1500)}!function(){let e=\u0026#34;[\u0026#34;+(document.title||document.location.hostname)+\u0026#34;](\u0026#34;+document.location.href+\u0026#34;)\u0026#34;,t=document.createelement(\u0026#34;div\u0026#34;);t.id=\u0026#34;btn\u0026#34;,t.innertext=\u0026#34;🥳 copy\u0026#34;,t.style=\u0026#34;background: #e6a23c; box-shadow: 1px 1px 3px #333; width: 120px; height: 40px; text-align: center; line-height: 40px; border-radius: 4px; color: #333; cursor: pointer; font-weight: 700; font-family: segoe script, courier new;font-size: 16px;\u0026#34;,t.addeventlistener(\u0026#34;click\u0026#34;,copy);let o=document.createelement(\u0026#34;input\u0026#34;);o.value=e,o.id=\u0026#34;ipt\u0026#34;,o.style=\u0026#34;opacity: 0;\u0026#34;,o.select();let c=document.createelement(\u0026#34;div\u0026#34;);c.id=\u0026#34;cont\u0026#34;,c.style=\u0026#34;position: fixed; right: 32px; top: 16px; width: 100px;z-index: 10000;\u0026#34;,c.appendchild(t),c.appendchild(o),document.body.appendchild(c)}(); 完成后,点击标签后,会在当前网页左上角弹出复制按钮;\n点击复制后,按钮会更新状态及样式,并在 1.5s 自动消失。\n当然,也可能失败,但希望你永远都不会看到它。\n源码解析 请访问 copy-link/copy.js at master · loveminimal/copy-link/copy.js 。\n📌 结语\njust for fun 🎉\n","date":"2022-09-30","permalink":"https://loveminimal.github.io/posts/a-simple-bookmark-copying/","summary":"\u003cp\u003e感谢小伙伴 Whatacold 的创意 \u003ca href=\"https://whatacold.io/blog/2022-08-08-org-link-bookmarklet/\"\u003eA Bookmarklet for Copying a Link as an Org-mode Link - whatacold\u0026rsquo;s space - whatacold.io\u003c/a\u003e ,可以方便地复制当前页面的地址并格式化 \u003ccode\u003e.md\u003c/code\u003e 链接格式,当然,也可以格式化成 \u003ccode\u003e.org\u003c/code\u003e 链接格式。\u003c/p\u003e","title":"a simple bookmark copying"},{"content":"在 canvas 中,我们介绍了画布的基本概念和使用方式,现在,我们来用它实现一个基本的手写板 - 你可以在 tablet 查看源码 。\n\u0026gt; 原生 js 实现一下喽 ~\n\u0026lt;!-- 画布 --\u0026gt; \u0026lt;canvas id=\u0026#34;cvs\u0026#34; width=\u0026#34;600\u0026#34; height=\u0026#34;300\u0026#34; ontouchstart=\u0026#34;touchstart(event)\u0026#34; ontouchmove=\u0026#34;touchmove(event)\u0026#34; ontouchend=\u0026#34;touchend(event)\u0026#34; onmousedown=\u0026#34;mousedown(event)\u0026#34; onmousemove=\u0026#34;mousemove(event)\u0026#34; onmouseup=\u0026#34;mouseup(event)\u0026#34; \u0026gt; \u0026lt;/canvas\u0026gt; 一般来说,手写板什么的多在移动设备(触摸)上使用,只涉及 touch*** 相关事件,当然,如上所示,pc 端使用 mouse*** 事件模拟即可。\n下面,让我们看一下具体实现吧(以 mouse*** 事件为例)。\n基本思路 手写板应用的核心,就是使用 canvas 实时绘制路径(path),我们先来简单回顾一下这方面的知识,如下:\nbeginpath()\r- 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径\rmoveto(x, y)\r- 把画笔移动到指定的坐标 (x, y),相当于设置路径的起始点坐标\rlineto(x, y)\r- 添加一个新点,然后创建从该点到画面中最后指定点的线条\rclosepath()\r- 闭合路径之后,图形绘制命令又重新指向到上下文中\rstroke()\r- 通过线条来绘制图形轮廓 通过以上绘制路径的方法,我们使用 mousestart 结合 beginpath() 和 moveto(x, y) 方法,开始绘制路径,并在 mousemove 事件触发的过程中结合 lineto(x, y) 和 stroke() 实时绘制路径。\n核心解析 1. 先准备一下吧\nlet cvs = document.queryselector(\u0026#39;#cvs\u0026#39;);\t// 获取画布 let ctx = cvs.getcontext(\u0026#39;2d\u0026#39;); // 上下文 2. 看看相关的事件\nfunction mousedown(e) { drawstart(e.pagex - cvs.offsetleft, e.pagey - cvs.offsettop); } // function drawstart(x, y) { // document.body.classlist.add(\u0026#39;body-fix\u0026#39;);\t// 书写时禁止页面滚动 // ctx.beginpath(); // ctx.moveto(x, y); // } mousedown 和 mousemove 事件中,我们可以方便获取鼠标指针相对于其第一个父级元素(带有 position 属性)的相对位置 (x, y),如果没有,就是相对于 body 了(本例中即是如此)。\nfunction mousemove(e) { if (e.buttons === 1) {\t// 鼠标左键按下时 drawmove(e.pagex - cvs.offsetleft, e.pagey - cvs.offsettop); } } // function drawmove(x, y) { // ctx.lineto(x, y); // ctx.stroke(); // } 实时坐标的获取同上,此外需要注意的是,这里我们限制了 =仅当鼠标左键按下时 才会绘制路径,否则,绘制出来的路径只会是个鬼画符。当然,如果你是在触摸设备中使用 touch*** 事件,则不存在这个问题。\n不同浏览器对于鼠标事件的监听指示可能有所不同,chrome 中,当 e.buttons 为 1 时,表示左键是按下状态,如果你想做兼容,请查看相关文档。\nfunction mouseup(e) { drawend(); } // function drawend() { // // ctx.closepath() // document.body.classlist.remove(\u0026#39;body-fix\u0026#39;);\t// 书写完成恢复页面滚动 // } 你可能已经注意到了,在 mousedown 和 mouseup 中,我们针对 body 元素做了一些类别修改 - 添加/删除 body-fix ,它有什么作用呢?\n.body-fix { overflow: hidden; } 很简单,就是为了防止在书写签名时页面滚动,导致你写不成字 ~ 当然,别忘记在 mouseup 时,移除该类,否则,你就滚动不了页面喽。\n辅助功能 1. 生成签名\n// 生成签名 function expcvs() { let src = cvs.todataurl(\u0026#39;image/png\u0026#39;, 1); /* * canvas.todataurl(type, encoderoptions) * 返回: * - 该方法返回一串 uri 字符串(canvas 中图像数据的 base64 编码) * * 参数: * - type:图像格式,默认为\u0026#34;image/png\u0026#34; * - encoderoptions:数值为 0 ~ 1,表示图片质量,仅在 type 为 \u0026#34;image/jpeg\u0026#34; 或 \u0026#34;image/webp\u0026#34; 时有效 * * 其他: * png 默认生成图片无背景,jpeg 默认生成图片为黑色背景 * 如果需要白色背景,可以在绘制前先绘制背景: * ctx.fillstyle = \u0026#39;#fff\u0026#39;; * ctx.fillrect(0, 0, canvas.width, canvas.height); */ console.log(src); img.src = src; } canvas 可以方便地生成图片格式(base64)文件 - 通过 todataurl 方法,如此,我们就可以方便的传递数据或将其作为图片标签的 src 属性使用。\n这里注意,做为签名来说,我们通常需要生成背景透明的图像,所以默认即为 png 格式的。\n2. 清除签名\n// 清除签名 function clrcvs() { ctx.clearrect(0, 0, 600, 300); img.src = \u0026#39;\u0026#39;; } 很简单,直接使用 clearrect 清空一个画布就可以了。\n3. 选择颜色\n// 选择签名颜色 function selectcolor(e) { console.log(e.target.value); ctx.strokestyle = e.target.value; } 可选功能,用来设置画笔颜色,当然,还有其他设置项,你完全可以按需添加。\n结语 emm\u0026hellip; 基本原理,就是讲的这些,具体项目中实现可能会稍有改变,但难不到你的,对吧 🥳\n参考链接 canvas 生成一张图片在后,图片背景颜色默认是黑色 怎么改成其他颜色呢? - 知乎 - www.zhihu.com 解决 canvas 转 base64/jpeg 时透明区域变成黑色背景的方法_html5_网页制作_脚本之家 - www.jb51.net szimek/signature_pad: html5 canvas based smooth signature drawing - github.com h5 前端实现移动端手写 canvas 签名(支持横竖屏,自定义图片旋转角度)_canvas 手写签名横屏 h5 canvas 签名板 - 简书 - www.jianshu.com ","date":"2022-09-28","permalink":"https://loveminimal.github.io/posts/tablet/","summary":"\u003cp\u003e在 \u003ca href=\"../canvas/\"\u003eCanvas\u003c/a\u003e 中,我们介绍了画布的基本概念和使用方式,现在,我们来用它实现一个基本的手写板 - 你可以在 \u003ca href=\"https://github.com/loveminimal/tablet\"\u003eTablet\u003c/a\u003e 查看源码 。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 原生 JS 实现一下喽 ~\u003c/code\u003e\u003c/p\u003e","title":"手写板"},{"content":"\u0026gt; 近来要开发一个手写板程序,顺便来系统回顾一下 canvas 这个东东……\n🔔 本篇摘录自 《学习 html5 canvas 这一篇文章就够了》 ,写的真不错。\n\u0026lt;canvas\u0026gt; 是 html5 新增的一个可以使用脚本(通常为 javascript)在其中绘制图像的 html 元素。\n\u0026gt; 上图就是使用 canvas 绘制出来的\ncanvas 是由 html 代码配合高度和宽度属性而定义出的可绘制区域。javascript 代码可以访问该区域,类似于其他通用的二维 api,通过一套完整的绘图函数来动态生成图形。\n基本使用 1. \u0026lt;canvas\u0026gt; 元素\n\u0026lt;canvas id=\u0026#34;tutorial\u0026#34; width=\u0026#34;300\u0026#34; height=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;canvas\u0026gt; 标签只有两个可选的属性 width 和 height ,如果省略,则默认 width 为 300 ,height 为 150 ,单位是 px 。也可以使用 css 属性来设置宽高,但是如宽高属性和初始比例不一致,会出现扭曲(强烈不推荐 ❌)。\n2. 渲染上下文 context\n\u0026lt;canvas\u0026gt; 会创建一个固定大小的画布,会公开一个或多个渲染上下文(画笔),使用渲染上下文来绘制和处理要展示的内容。\n我们重点研究 2d 渲染上下文。如何获取它呢?\nvar canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if(!canvas.getcontext) return;\t// 检测支持性 var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;);\t// 获得 2d 上下文对象 绘制形状 1. 栅格(grid)和坐标空间\n如下图所示,canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角,坐标为 (0,0) 。所有元素的位置都相对于原点来定位。所以图中蓝色方形左上角的坐标为距离左边(x 轴)x 像素,距离上边(y 轴)y 像素,坐标为 (x,y)。\n后面我们会涉及到坐标原点的平移、网格的旋转以及缩放等。\n2. 绘制矩形\n\u0026lt;canvas\u0026gt; 只支持一种原生的图形绘制:矩形。\n\u0026gt; 所有其他图形都至少需要生成一种路径 (path)。\ncanvas 提供了有三种方法绘制矩形:\n// 绘制一个填充的矩形\r1. fillrect(x, y, width, height)\r// 绘制一个矩形的边框\r2. strokerect(x, y, width, height)\r// 清除指定的矩形区域,然后这块区域会变的完全透明\r3. clearrect(x, y, widh, height) 这 3 个方法具有相同的参数。其中, x, y 指矩形左上角的坐标, width, height 指绘制的矩形的宽高。\n上 🌰 ,如下:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if(!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillrect(10, 10, 100, 50);\t//绘制矩形,填充的默认颜色为黑色 ctx.strokerect(10, 70, 100, 50);\t//绘制矩形边框 ctx.clearrect(15, 15, 50, 25); } draw(); 绘制路径 图形的基本元素是路径。\n路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。\n一个路径,甚至一个子路径,都是闭合的。\n使用路径绘制图形需要一些额外的步骤:\n创建路径起始点; 调用绘制方法去绘制出路径; 把路径封闭; 一旦路径生成,通过描边或填充路径区域来渲染图形。 下面是需要用到的方法:\nbeginpath()\r- 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径\rmoveto(x, y)\r- 把画笔移动到指定的坐标 (x, y),相当于设置路径的起始点坐标\rlineto(x, y)\r- 添加一个新点,然后创建从该点到画面中最后指定点的线条\rclosepath()\r- 闭合路径之后,图形绘制命令又重新指向到上下文中\rstroke()\r- 通过线条来绘制图形轮廓\rfill()\r- 通过填充路径的内容区域生成实心的图形 1. 绘制线段\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath();\t// 新建一条 path ctx.moveto(50, 50);\t// 把画笔移动到指定的坐标 ctx.lineto(200, 50);\t// 绘制一条从当前位置到指定坐标 (200, 50) 的直线 // 闭合路径 // 会拉一条从当前点到 path 起始点的直线, // 如果当前点与起始点重合,则什么都不做 ctx.closepath(); ctx.stroke(); //绘制路径 } draw(); 2. 绘制三角形边框\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); ctx.lineto(200, 50); ctx.lineto(200, 200); ctx.closepath();\t//虽然我们只绘制了两条线段,但是 closepath 会自动闭合,仍然是一个 3 角形 ctx.stroke();\t//描边,stroke 不会自动 closepath() } draw(); 3. 填充三角形\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); ctx.lineto(200, 50); ctx.lineto(200, 200); ctx.fill();\t//填充闭合区域,如果 path 没有闭合,则 fill() 会自动闭合路径。 } draw(); 4. 绘制圆弧\n有两个方法可以绘制圆弧:\n1. arc(x, y, r, startangle, endangle, anticlockwise)\r- 以 (x, y) 为圆心,以 r 为半径,从 startangle 弧度开始到 endangle 弧度结束,\r- anticlosewise 是布尔值,true 表示逆时针,false 表示顺时针(默认是顺时针)\r* 这里的度数都是弧度,0 弧度是指 x 轴正向\r* radias = (math.pi/180)*degrees // 角度转换成弧度\r2. arcto(x1, y1, x2, y2, radius)\r- 根据给定的控制点和半径画一段圆弧,最后再以直线连接两个控制点\r* 这个方法可以这样理解,绘制的弧形是由两条切线所决定\r* - 第 1 条切线:起始点和控制点 1 决定的直线\r* - 第 2 条切线:控制点 1 和控制点 2 决定的直线\r* - 其实绘制的圆弧就是与这两条直线相切的圆弧 来看几个示例 🍩\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.arc(50, 50, 40, 0, math.pi / 2, false); ctx.stroke(); ctx.beginpath(); ctx.arc(150, 50, 40, 0, -math.pi / 2, true); ctx.closepath(); ctx.stroke(); ctx.beginpath(); ctx.arc(50, 150, 40, -math.pi / 2, math.pi / 2, false); ctx.fill(); ctx.beginpath(); ctx.arc(150, 150, 40, 0, math.pi, false); ctx.fill(); } draw(); 再来看一下关于 arcto 的方法示例如下:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(50, 50); //参数 1、2:控制点 1 坐标 参数 3、4:控制点 2 坐标 参数 5:圆弧半径 ctx.arcto(200, 50, 200, 200, 100); ctx.lineto(200, 200) ctx.stroke(); ctx.beginpath(); ctx.rect(50, 50, 10, 10); ctx.rect(200, 50, 10, 10) ctx.rect(200, 200, 10, 10) ctx.fill() } draw(); 5. 绘制贝塞尔曲线\n贝塞尔曲线 (bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。\n一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。\n贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具如 photoshop 等。在 flash4 中还没有完整的曲线工具,而在 flash5 里面已经提供出贝塞尔曲线工具。\n贝塞尔曲线于 1962,由法国工程师皮埃尔·贝塞尔(pierre bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由 paul de casteljau 于 1959 年运用 de casteljau 演算法开发,以稳定数值的方法求出贝兹曲线。\n_一次贝塞尔曲线其实是一条直线\n_二次贝塞尔曲线\n_三次贝塞尔曲线\n好的,我们已初步了解了 =贝塞尔曲线 是什么东东,那么如何绘制它呢?\n// 二次贝塞尔曲线\rquadraticcurveto(cp1x, cp1y, x, y)\r- 参数 1 和 2:控制点坐标\r- 参数 3 和 4:结束点坐标\r// 三次贝塞尔曲线\rbeziercurveto(cp1x, cp1y, cp2x, cp2y, x, y)\r- 参数 1 和 2:控制点 1 的坐标\r- 参数 3 和 4:控制点 2 的坐标\r- 参数 5 和 6:结束点的坐标 像下面这样:\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.moveto(10, 200); // 起始点 var cp1x = 40, cp1y = 100; // 控制点 var x = 200, y = 200; // 结束点 //绘制二次贝塞尔曲线 ctx.quadraticcurveto(cp1x, cp1y, x, y); ctx.stroke(); ctx.beginpath(); ctx.rect(10, 200, 10, 10); ctx.rect(cp1x, cp1y, 10, 10); ctx.rect(x, y, 10, 10); ctx.fill(); ctx.beginpath(); ctx.moveto(40, 200); // 起始点 var cp1x = 20, cp1y = 100; // 控制点 1 var cp2x = 100, cp2y = 120; // 控制点 2 var x = 200, y = 200; // 结束点 //绘制三次贝塞尔曲线 ctx.beziercurveto(cp1x, cp1y, cp2x, cp2y, x, y); ctx.stroke(); ctx.beginpath(); ctx.rect(40, 200, 10, 10); ctx.rect(cp1x, cp1y, 10, 10); ctx.rect(cp2x, cp2y, 10, 10); ctx.rect(x, y, 10, 10); ctx.fill(); } draw(); 添加样式和颜色 在前面的绘制矩形章节中,只用到了默认的线条和颜色。\n如果想要给图形上色,有两个重要的属性可以做到:\n1. fillstyle = color // 设置图形的填充颜色\r2. strokestyle = color // 设置图形轮廓的颜色\r备注:\r- color 可以是表示 css 颜色值的字符串、渐变对象或者图案对象\r- 默认情况下,线条和填充颜色都是黑色\r- 一旦您设置了 strokestyle 或者 fillstyle 的值,那么这个新值就会成为新绘制的图形的默认值,\r- 如果你要给每个图形上不同的颜色,你需要重新设置 fillstyle 或 strokestyle 的值 1. fillstyle\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); for (var i = 0; i \u0026lt; 6; i++){ for (var j = 0; j \u0026lt; 6; j++){ ctx.fillstyle = \u0026#39;rgb(\u0026#39; + math.floor(255 - 42.5 * i) + \u0026#39;,\u0026#39; + math.floor(255 - 42.5 * j) + \u0026#39;,0)\u0026#39;; ctx.fillrect(j * 50, i * 50, 50, 50); } } } draw(); 2. strokestyle\nfunction draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); for (var i = 0; i \u0026lt; 6; i++){ for (var j = 0; j \u0026lt; 6; j++){ ctx.strokestyle = `rgb( ${randomint(0, 255)}, ${randomint(0, 255)} )`; ctx.strokerect(j * 50, i * 50, 40, 40); } } } draw(); /** * 返回随机的 [from, to] 之间的整数(包括 from,也包括 to) */ function randomint(from, to){ return parseint(math.random() * (to - from + 1) + from); } 3. transparency (透明度)\nglobalalpha = transparencyvalue\r- 这个属性影响到 canvas 里所有图形的透明度\r- 有效的值范围是 0.0 (完全透明)到 1.0(完全不透明),默认是 1.0\r- globalalpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效,\r- 不过,我认为使用 rgba() 设置透明度更加好一些 4. line style\n_1. 线宽\nlinewidth = value\r关于 value :\r- 只能是正值,默认是 1.0\r- 起始点和终点的连线为中心,上下各占线宽的一半 ctx.beginpath(); ctx.moveto(10, 10); ctx.lineto(100, 10); ctx.linewidth = 10; ctx.stroke(); ctx.beginpath(); ctx.moveto(110, 10); ctx.lineto(160, 10) ctx.linewidth = 20; ctx.stroke() _2. 线条末端样式\nlinecap = type\r关于 type :\r- butt:线段末端以方形结束\r- round:线段末端以圆形结束\r- square:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域 var linecaps = [\u0026#34;butt\u0026#34;, \u0026#34;round\u0026#34;, \u0026#34;square\u0026#34;]; for (var i = 0; i \u0026lt; 3; i++){ ctx.beginpath(); ctx.moveto(20 + 30 * i, 30); ctx.lineto(20 + 30 * i, 100); ctx.linewidth = 20; ctx.linecap = linecaps[i]; ctx.stroke(); } ctx.beginpath(); ctx.moveto(0, 30); ctx.lineto(300, 30); ctx.moveto(0, 100); ctx.lineto(300, 100) ctx.strokestyle = \u0026#34;red\u0026#34;; ctx.linewidth = 1; ctx.stroke(); _3. 同一个 path 内,设定线条与线条间接合处的样式\nlinejoin = type\r关于 type :\r- round :通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度\r- bevel :在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角\r- miter(默认) :通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域 function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); var linejoin = [\u0026#39;round\u0026#39;, \u0026#39;bevel\u0026#39;, \u0026#39;miter\u0026#39;]; ctx.linewidth = 20; for (var i = 0; i \u0026lt; linejoin.length; i++){ ctx.linejoin = linejoin[i]; ctx.beginpath(); ctx.moveto(50, 50 + i * 50); ctx.lineto(100, 100 + i * 50); ctx.lineto(150, 50 + i * 50); ctx.lineto(200, 100 + i * 50); ctx.lineto(250, 50 + i * 50); ctx.stroke(); } } draw(); 4. 虚线\n用 setlinedash 方法和 linedashoffset 属性来制定虚线样式,如下:\nsetlinedash 方法接受一个数组,来指定线段与间隙的交替; linedashoffset 属性设置起始偏移量。 function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.setlinedash([20, 5]); // [实线长度,间隙长度] ctx.linedashoffset = -0; ctx.strokerect(50, 50, 210, 210); } draw(); // getlinedash(): 返回一个包含当前虚线样式,长度为非负偶数的数组 绘制文本 canvas 提供了两种方法来渲染文本:\nfilltext(text, x, y [, maxwidth])\r- 在指定的 (x,y) 位置填充指定的文本,绘制的最大宽度是可选的\rstroketext(text, x, y [, maxwidth])\r- 在指定的 (x,y) 位置绘制文本边框,绘制的最大宽度是可选的 var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.font = \u0026#34;100px sans-serif\u0026#34; ctx.filltext(\u0026#34;天若有情\u0026#34;, 10, 100); ctx.stroketext(\u0026#34;天若有情\u0026#34;, 10, 200) } draw(); 我们都可以给文本添加哪些样式呢?\n1. 绘制文本的样式\rfont = value\r- 这个字符串使用和 css font 属性相同的语法\r- 默认的字体是 10px sans-serif\r2. 文本对齐选项\rtextalign = value\r- 可选的值包括:start, end, left, right or center\r- 默认值是 start。\r3. 基线对齐选项\rtextbaseline = value\r- 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom\r- 默认值是 alphabetic\r4. 文本方向\rdirection = value\r- 可能的值包括:ltr, rtl, inherit\r- 默认值是 inherit 绘制图片 我们也可以在 canvas 上直接绘制图片。\n1. 由零开始创建图片\nvar img = new image(); // 创建 img 元素 img.src = \u0026#39;myimage.png\u0026#39;; // 设置图片源地址 img.onload = function(){ // 参数 1:要绘制的 img // 参数 2、3:绘制的 img 在 canvas 中的坐标 ctx.drawimage(img, 0, 0) } // 关于 onload // - 考虑到图片是从网络加载,如果 drawimage 的时候图片还没有完全加载完成,则什么都不做,个别浏览器会抛异常, // - 所以我们应该保证在 img 绘制完成之后再 drawimage 2. 绘制 img 标签元素中的图片\nimg 可以 new 也可以来源于我们页面的 \u0026lt;img\u0026gt; 标签。\n\u0026lt;img src=\u0026#34;./美女。jpg\u0026#34; alt=\u0026#34;\u0026#34; width=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;canvas id=\u0026#34;tutorial\u0026#34; width=\u0026#34;600\u0026#34; height=\u0026#34;400\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); var img = document.queryselector(\u0026#34;img\u0026#34;); ctx.drawimage(img, 0, 0); } document.queryselector(\u0026#34;img\u0026#34;).onclick = function (){ draw(); } \u0026lt;/script\u0026gt; \u0026gt; 上图就是页面中的 \u0026lt;img\u0026gt; 标签\n3. 缩放图片\ndrawimage() 也可以再添加两个参数:\ndrawimage(image, x, y, width, height)\r- width 和 height,这两个参数用来控制 当像 canvas 画入时应该缩放的大小 4. 切片\ndrawimage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight)\r- 第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用\r其他 8 个参数:\r- 前 4 个是定义图像源的切片位置和大小\r- 后 4 个则是定义切片的目标显示位置和大小 状态的保存和恢复 canvas 的状态就是当前画面应用的所有样式和变形的一个快照,状态的保存和恢复是绘制复杂图形时必不可少的操作。save() 和 restore() 方法是用来保存和恢复 canvas 状态的,都没有参数。\n1. save()\ncanvas 状态存储在栈中,每当 save() 方法被调用后,当前的状态就被推送到栈中保存。\n\u0026gt; canvas 都有哪些状态呢?\n一个绘画状态包括:\n当前应用的变形(即移动,旋转和缩放); strokestyle, fillstyle, globalalpha, linewidth, linecap, linejoin, miterlimit, shadowoffsetx, shadowoffsety, shadowblur, shadowcolor, globalcompositeoperation 的值; 当前的裁切路径(clipping path)。 可以调用任意多次 save 方法(类似数组的 push())。\n2. restore()\n每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复(类似数组的 pop())。\n上个例子吧 🍩\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillrect(0, 0, 150, 150); // 使用默认设置绘制一个矩形 ctx.save(); // 保存默认状态 ctx.fillstyle = \u0026#39;red\u0026#39; // 在原有配置基础上对颜色做改变 ctx.fillrect(15, 15, 120, 120); // 使用新的设置绘制一个矩形 ctx.save(); // 保存当前状态 ctx.fillstyle = \u0026#39;#fff\u0026#39; // 再次改变颜色配置 ctx.fillrect(30, 30, 90, 90); // 使用新的配置绘制一个矩形 ctx.restore(); // 重新加载之前的颜色状态 ctx.fillrect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形 ctx.restore(); // 加载默认颜色配置 ctx.fillrect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形 } draw(); 变形 1. translate\ntranslate(x, y)\r- 用来移动 canvas 的原点到指定的位置\r- 接受两个参数,x 是左右偏移量,y 是上下偏移量 \u0026gt; 注意:translate 移动的是 canvas 的坐标原点(坐标变换)。\n在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore() 方法比手动恢复原先的状态要简单得多。又如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.save(); //保存坐原点平移之前的状态 ctx.translate(100, 100); ctx.strokerect(0, 0, 100, 100); ctx.restore(); //恢复到最初状态 ctx.translate(220, 220); ctx.fillrect(0, 0, 100, 100) } draw(); 2. rotate\nrotate(angle)\r- 旋转坐标轴, 旋转的中心是坐标原点\r- 这个方法只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值 var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillstyle = \u0026#34;red\u0026#34;; ctx.save(); ctx.translate(100, 100); ctx.rotate(math.pi / 180 * 45); ctx.fillstyle = \u0026#34;blue\u0026#34;; ctx.fillrect(0, 0, 100, 100); ctx.restore(); ctx.save(); ctx.translate(0, 0); ctx.fillrect(0, 0, 50, 50) ctx.restore(); } draw(); 3. scale\n我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。\nscale(x, y)\r- scale 方法接受两个参数。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值\r- 值比 1.0 小表示缩 小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有 默认情况下,canvas 的 1 单位就是 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。\n4. transform(变形矩阵)\ntransform(a, b, c, d, e, f)\r- a (m11): horizontal scaling.\r- b (m12): horizontal skewing.\r- c (m21): vertical skewing.\r- d (m22): vertical scaling.\r- e (dx): horizontal moving.\r- f (dy): vertical moving. var ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.transform(1, 1, 0, 1, 0, 0); ctx.fillrect(0, 0, 100, 100); } draw(); 合成 在前面的所有例子中、,我们总是将一个图形画在另一个之上,对于其他更多的情况,仅仅这样是远远不够的。比如,对合成的图形来说,绘制顺序会有限制。不过,我们可以利用 globalcompositeoperation 属性来改变这种状况。\nglobalcompositeoperation = type\r其中 type 值有 13 个,如下:\r- source-over (默认值), source-in, source-out, source-atop\r- destination-over, destination-in, destination-out, destination-atop\r- lighter, darken, lighten\r- xor, copy 下面我们分别来看一下这些值的表示。\n1. source-over\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.fillstyle = \u0026#34;blue\u0026#34;; ctx.fillrect(0, 0, 200, 200); ctx.globalcompositeoperation = \u0026#34;source-over\u0026#34;; //全局合成操作 ctx.fillstyle = \u0026#34;red\u0026#34;; ctx.fillrect(100, 100, 200, 200); } draw(); 注:下面的展示中,蓝色是原有的,红色是新的。\n这是默认设置,新图像会覆盖在原有图像。\n2. source-in\n仅仅会出现新图像与原来图像重叠的部分,其他区域都变成透明的。(包括其他的老图像区域也会透明)\n3. source-out\n仅仅显示新图像与老图像没有重叠的部分,其余部分全部透明。(老图像也不显示)\n4. source-atop\n新图像仅仅显示与老图像重叠区域。(老图像仍然可以显示)\n5. destination-over\n新图像会在老图像的下面。\n6. destination-in\n仅仅新老图像重叠部分的老图像被显示,其他区域全部透明。\n7. destination-out\n仅仅老图像与新图像没有重叠的部分。 (注意显示的是老图像的部分区域)\n8. destination-atop\n老图像仅仅仅仅显示重叠部分,新图像会显示在老图像的下面。\n9. lighter\n新老图像都显示,但是重叠区域的颜色做加处理。\n10. darken\n保留重叠部分最黑的像素。(每个颜色位进行比较,得到最小的)\nblue: #0000ff\rred: #ff0000\r// 所以重叠部分的颜色:#000000 11. lighten\n保证重叠部分最量的像素。(每个颜色位进行比较,得到最大的)\nblue: #0000ff\rred: #ff0000\r// 所以重叠部分的颜色:#ff00ff 12. xor\n重叠部分会变成透明。\n13. copy\n只有新图像会被保留,其余的全部被清除(边透明)。\n裁剪路径 clip()\r- 把已经创建的路径转换成裁剪路径 裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。\n注意:clip() 只能遮罩在这个方法 =调用之后 绘制的图像,如果是 clip() 方法调用之前绘制的图像,则无法实现遮罩。\nvar ctx; function draw(){ var canvas = document.getelementbyid(\u0026#39;tutorial1\u0026#39;); if (!canvas.getcontext) return; var ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); ctx.beginpath(); ctx.arc(20,20, 100, 0, math.pi * 2); ctx.clip(); ctx.fillstyle = \u0026#34;pink\u0026#34;; ctx.fillrect(20, 20, 100,100); } draw(); 动画 动画的基本步骤:\n清空 canvas 再绘制每一帧动画之前,需要清空所有。清空所有最简单的做法就是 clearrect() 方法; 保存 canvas 状态 如果在绘制的过程中会更改 canvas 的状态(颜色、移动了坐标原点等), 又在绘制每一帧时都是原始状态的话,则最好保存下 canvas 的状态; 绘制动画图形这一步才是真正的绘制动画帧; 恢复 canvas 状态如果你前面保存了 canvas 状态,则应该在绘制完成一帧之后恢复 canvas 状态。 控制动画\n我们可用通过 canvas 的方法或者自定义的方法把图像绘制到 canvas 上。正常情况,我们能看到绘制的结果是在脚本执行结束之后。例如,我们不可能在一个 for 循环内部完成动画。\n也就是,为了执行动画,我们需要一些可以定时执行重绘的方法。\n一般用到下面三个方法:\nsetinterval() settimeout() requestanimationframe() let sun; let earth; let moon; let ctx; init(); function init() { sun = new image(); earth = new image(); moon = new image(); sun.src = \u0026#34;sun.png\u0026#34;; earth.src = \u0026#34;earth.png\u0026#34;; moon.src = \u0026#34;moon.png\u0026#34;; let canvas = document.queryselector(\u0026#34;#solar\u0026#34;); ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); sun.onload = function (){ draw() } } function draw() { ctx.clearrect(0, 0, 300, 300); // 清空所有内容 // 绘制太阳 ctx.drawimage(sun, 0, 0, 300, 300); ctx.save(); ctx.translate(150, 150); //绘制 earth 轨道 ctx.beginpath(); ctx.strokestyle = \u0026#34;rgba(255,255,0,0.5)\u0026#34;; ctx.arc(0, 0, 100, 0, 2 * math.pi) ctx.stroke() let time = new date(); //绘制地球 ctx.rotate( 2 * math.pi / 60 * time.getseconds() + 2 * math.pi / 60000 * time.getmilliseconds() ); ctx.translate(100, 0); ctx.drawimage(earth, -12, -12); //绘制月球轨道 ctx.beginpath(); ctx.strokestyle = \u0026#34;rgba(255,255,255,.3)\u0026#34;; ctx.arc(0, 0, 40, 0, 2 * math.pi); ctx.stroke(); //绘制月球 ctx.rotate( 2 * math.pi / 6 * time.getseconds() + 2 * math.pi / 6000 * time.getmilliseconds() ); ctx.translate(40, 0); ctx.drawimage(moon, -3.5, -3.5); ctx.restore(); requestanimationframe(draw); } emm\u0026hellip; 不错,再来一个吧 🍪\n// \u0026lt;canvas id=\u0026#34;solar\u0026#34; width=\u0026#34;300\u0026#34; height=\u0026#34;300\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; init(); function init(){ let canvas = document.queryselector(\u0026#34;#solar\u0026#34;); let ctx = canvas.getcontext(\u0026#34;2d\u0026#34;); draw(ctx); } function draw(ctx){ requestanimationframe(function step(){ drawdial(ctx); //绘制表盘 drawallhands(ctx); //绘制时分秒针 requestanimationframe(step); }); } /*绘制时分秒针*/ function drawallhands(ctx){ let time = new date(); let s = time.getseconds(); let m = time.getminutes(); let h = time.gethours(); let pi = math.pi; let secondangle = pi / 180 * 6 * s; //计算出来 s 针的弧度 let minuteangle = pi / 180 * 6 * m + secondangle / 60; //计算出来分针的弧度 let hourangle = pi / 180 * 30 * h + minuteangle / 12; //计算出来时针的弧度 drawhand(hourangle, 60, 6, \u0026#34;red\u0026#34;, ctx); //绘制时针 drawhand(minuteangle, 106, 4, \u0026#34;green\u0026#34;, ctx); //绘制分针 drawhand(secondangle, 129, 2, \u0026#34;blue\u0026#34;, ctx); //绘制秒针 } /*绘制时针、或分针、或秒针 * 参数 1:要绘制的针的角度 * 参数 2:要绘制的针的长度 * 参数 3:要绘制的针的宽度 * 参数 4:要绘制的针的颜色 * 参数 4:ctx * */ function drawhand(angle, len, width, color, ctx){ ctx.save(); ctx.translate(150, 150); //把坐标轴的远点平移到原来的中心 ctx.rotate(-math.pi / 2 + angle); //旋转坐标轴。 x 轴就是针的角度 ctx.beginpath(); ctx.moveto(-4, 0); ctx.lineto(len, 0); // 沿着 x 轴绘制针 ctx.linewidth = width; ctx.strokestyle = color; ctx.linecap = \u0026#34;round\u0026#34;; ctx.stroke(); ctx.closepath(); ctx.restore(); } /*绘制表盘*/ function drawdial(ctx){ let pi = math.pi; ctx.clearrect(0, 0, 300, 300); //清除所有内容 ctx.save(); ctx.translate(150, 150); //一定坐标原点到原来的中心 ctx.beginpath(); ctx.arc(0, 0, 148, 0, 2 * pi); //绘制圆周 ctx.stroke(); ctx.closepath(); for (let i = 0; i \u0026lt; 60; i++){ //绘制刻度。 ctx.save(); ctx.rotate(-pi / 2 + i * pi / 30); //旋转坐标轴。坐标轴 x 的正方形从 向上开始算起 ctx.beginpath(); ctx.moveto(110, 0); ctx.lineto(140, 0); ctx.linewidth = i % 5 ? 2 : 4; ctx.strokestyle = i % 5 ? \u0026#34;blue\u0026#34; : \u0026#34;red\u0026#34;; ctx.stroke(); ctx.closepath(); ctx.restore(); } ctx.restore(); } 结语 canvas 画布的功能还是挺强大的,go go go!\n","date":"2022-09-23","permalink":"https://loveminimal.github.io/posts/canvas/","summary":"\u003cp\u003e\u003ccode\u003e\u0026gt; 近来要开发一个手写板程序,顺便来系统回顾一下 Canvas 这个东东……\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e🔔 本篇摘录自 \u003ca href=\"https://www.runoob.com/w3cnote/html5-canvas-intro.html\"\u003e《学习 HTML5 Canvas 这一篇文章就够了》\u003c/a\u003e ,写的真不错。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026lt;canvas\u0026gt;\u003c/code\u003e 是 HTML5 新增的一个可以使用脚本(通常为 JavaScript)在其中绘制图像的 HTML 元素。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/canvas/imgs/1232422ad2c81ef221b8d3cf3938b9cc48fda66fdd7418d5c400728d82a2c162.png\" width=\"500\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; 上图就是使用 canvas 绘制出来的\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eCanvas 是由 HTML 代码配合高度和宽度属性而定义出的可绘制区域。JavaScript 代码可以访问该区域,类似于其他通用的二维 API,通过一套完整的绘图函数来动态生成图形。\u003c/p\u003e","title":"canvas"},{"content":"📔 官方文档\n具体安装及引入细节,请直接参考官方文档。\nreact 是一个用于构建用户界面的 javascript 库,你可以用它给简单的 html 页面增加一点交互,也可以开始一个完全由 react 驱动的复杂应用。\n\u0026gt; 对的,它只是一个 ui 库而已 !!!\n简单的就不说了,直接来看一下 react 团队推荐的创建 spa (单页面,single page app)的工具链 - create react app 。\n要创建项目,请执行:\nnpx create-react-app my-app\rcd my-app\rnpm start create react app 不会处理后端逻辑或操纵数据库;它只是创建一个前端构建流水线(build pipeline),所以你可以使用它来配合任何你想使用的后端。它在内部使用 babel 和 webpack,但你无需了解它们的任何细节。当然,关于它,你肯定想了解更多,请参考 用户指南 。\n如果你倾向于从头开始打造你自己的 javascript 工具链,可以 查看这个指南,它重新创建了一些 create react app 的功能。\n核心概念 老规矩,上 \u0026quot;hello world\u0026quot; 😂\n// \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; const element = \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;; reactdom.render( element, document.getelementbyid(\u0026#39;root\u0026#39;) ); 它将在页面上展示一个 “hello, world!” 的标题。不要着急,马上你就明白它的工作原理了!\njsx 简介 再观察一下上面的例子,这是什么?\nconst element = \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;; \u0026gt; 怎么把 dom 元素直接赋给了一个变量 ❓\n这个有趣的标签语法既不是字符串也不是 html。它被称为 jsx,是一个 javascript 的语法扩展。\njsx 可以生成 react “元素”,它其实一个表达式,在编译(通过 babel)之后,会被转为普通 javascript 函数(react.createelement())调用,并且对其取值后得到 javascript 对象。\njsx 的语法格式十分简单!上 🌰\nconst name = \u0026#39;josh perez\u0026#39;; const element = ( \u0026lt;h1\u0026gt; hello, {name} \u0026lt;/h1\u0026gt; ); const element = \u0026lt;img src={user.avatarurl}\u0026gt;\u0026lt;/img\u0026gt;; 在 jsx 语法中,你可以在大括号内放置任何有效的 javascript 表达式。\n只需要注意:\n尽量将内容包裹在括号中,以避免多行书写时遇到自动插入分号陷阱; 在属性中嵌入 javascript 表达式时,不要在大括号外面加上引号; 使用 camelcase(小驼峰命名)来定义属性的名称,而不使用 html 属性名称的命名约定。 ok,这就是 jsx ,再来一个例子看看它的具体转译过程!\n// 我们用 jsx 是这样写的 const element = ( \u0026lt;h1 classname=\u0026#34;greeting\u0026#34;\u0026gt; hello, world! \u0026lt;/h1\u0026gt; ); // 被 babel 转译为 react.crateelement() 调用 const element = react.createelement( \u0026#39;h1\u0026#39;, {classname: \u0026#39;greeting\u0026#39;}, \u0026#39;hello, world!\u0026#39; ); // react.createelement() 会预先进行一些检查,实际上创建了如下对象, // 这些对象被称为 “react 元素”, // react 通过读取这些对象,然后使用它们来构建 dom 以及保持随时更新 // 注意:这是简化过的结构 const element = { type: \u0026#39;h1\u0026#39;, props: { classname: \u0026#39;greeting\u0026#39;, children: \u0026#39;hello, world!\u0026#39; } }; 是的,jsx 就是这么简单 ❗\n元素渲染 在上一节中,我们已经多次提到了 =react “元素” ,它究竟是什么呢?\n元素描述了你在屏幕上想看到的内容。如 element 就是一个 react 元素:\nconst element = \u0026lt;h1\u0026gt;hello, world\u0026lt;/h1\u0026gt;; 与浏览器的 dom 元素不同,react 元素是创建开销极小的普通对象(详见上节)。react dom 会负责更新 dom 来与 react 元素保持一致。\n\u0026gt; 那 react dom 到底是如何渲染 react 元素为 dom 的呢 ❓\n只需要把它们传入 reactdom.render() 就可以了(该元素会被自动渲染到根 dom 节点中)!\n需要注意的是, react 元素是不可变对象! 一旦被创建,你就无法更改它的子元素或者属性。\n如何更新 ui 呢?\n根据我们已有的知识,更新 ui 唯一的方式是创建一个全新的元素,并将其传入 reactdom.render()。react dom 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 dom 达到预期的状态。\nfunction tick() { const element = ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;it is {new date().tolocaletimestring()}.\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); reactdom.render(element, document.getelementbyid(\u0026#39;root\u0026#39;)); } // 每秒都创建一个新元素,并传入 reactdom.render() setinterval(tick, 1000); 当然,在实践中,我们并不会那么蠢,大多数 react 应用只会调用一次 reactdom.render(),后续我们将学习如何封装一个有状态的组件。\n组件 \u0026amp; props 组件允许你将 ui 拆分为独立可复用的代码片段,并对每个片段进行独立构思。\n组件,从概念上类似于 javascript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 react 元素。\n在 react 中,有两种组件形式:函数组件和类组件,如下:\n// 函数组件 function welcome(props) { return \u0026lt;h1\u0026gt;hello, {props.name}\u0026lt;/h1\u0026gt;; } // 类组件 class welcome extends react.component { render() { return \u0026lt;h1\u0026gt;hello, {this.props.name}\u0026lt;/h1\u0026gt;; } } 上述两个组件在 react 里是等效的。 它们返回的都是 react 元素哦!\n= 在实际应用中,函数式组件明显更受欢迎,也更符合直觉,再加上现在有了 hook,所以你懂得 ……*\n例如,这段代码会在页面上渲染 “hello, sara”:\nfunction welcome(props) { return \u0026lt;h1\u0026gt;hello, {props.name}\u0026lt;/h1\u0026gt;; } const element = \u0026lt;welcome name=\u0026#34;sara\u0026#34; /\u0026gt;; reactdom.render( element, document.getelementbyid(\u0026#39;root\u0026#39;) ); 让我们来回顾一下这个例子中发生了什么:\n我们调用 reactdom.render() 函数,并传入 \u0026lt;welcome name=\u0026quot;sara\u0026quot; /\u0026gt; 作为参数; react 调用 welcome 组件,并将 {name: 'sara'} 作为 props 传入; welcome 组件将 \u0026lt;h1\u0026gt;hello, sara\u0026lt;/h1\u0026gt; 元素作为返回值; react dom 将 dom 高效地更新为 \u0026lt;h1\u0026gt;hello, sara\u0026lt;/h1\u0026gt;。 注意: 组件名称必须以大写字母开头 !!!(react 会将以小写字母开头的组件视为原生 dom 标签)\n组件可以在其输出中引用其他组件(=组件组合)。有时候,将组件拆分为更小的组件也是很不错的选择(=组件提取)。\n所有 react 组件都必须像纯函数一样保护它们的 props 不被更改。\n= 其实,props 很简单,就把它理解为一个只读的函数入参就行了!函数,你足够了解的,对吧?*\nprops 是不可变的,但应用程序的 ui 是动态的,并会伴随着时间的推移而变化,emm\u0026hellip; 😟\n放心!在下一章节中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state 允许 react 组件随用户操作、网络响应或者其他变化而动态更改输出内容。\nstate \u0026amp; 生命周期 在元素渲染章节中,我们只了解了一种更新 ui 界面的方法,通过调用 reactdom.render() 来修改我们想要渲染的元素。\n我们也说了,那种方法有点蠢 🤣! 在本章节中,我们将学习如何封装真正可复用的组件。\nstate 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。\n下面,让我们看一个完整的 clock 组件(请留意注释内容):\nclass clock extends react.component { // 构造函数 - 用来初始化的 constructor(props) { // 将 props 传递到父类的构造函数中 ❓ // class 组件应该始终使用 props 参数来调用父类的构造函数 super(props); // 在构造函数中为 this.state 赋初值 this.state = { data: new date() }; // 将生命周期方法添加到 class 中 // ^ 组件挂载 componentdidmount() { // 尽管 this.props 和 this.state 是 react 本身设置的,且都拥有特殊的含义, // 但是其实你可以向 class 中随意添加不参与数据流(比如计时器 id)的额外字段 this.timerid = setinterval(() =\u0026gt; this.tick(), 1000 ); } // ^ 组件卸载 componentwillunmount() { clearinterval(this.timerid); } tick() { // 使用 this.setstate() 来时刻更新组件 state this.setstate({ date: new date() }); } render() { return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;it is {this.state.date.tolocaletimestring()}.\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; ); } } } reactdom.render( \u0026lt;clock /\u0026gt;, document.getelementbyid(\u0026#39;root\u0026#39;) ); 让我们来快速概括一下发生了什么和这些方法的调用顺序:\n当 \u0026lt;clock /\u0026gt; 被传给 reactdom.render() 的时候,react 会调用 clock 组件的构造函数。因为 clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state 。我们会在之后更新 state ; 之后 react 会调用组件的 render() 方法。这就是 react 确定该在页面上展示什么的方式。然后 react 更新 dom 来匹配 clock 渲染的输出; 当 clock 的输出被插入到 dom 中后,react 就会调用 componentdidmount() 生命周期方法。在这个方法中,clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法; 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,clock 组件会通过调用 setstate() 来计划进行一次 ui 更新。得益于 setstate() 的调用,react 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。react 也会相应的更新 dom; 一旦 clock 组件从 dom 中被移除,react 就会调用 componentwillunmount() 生命周期方法,这样计时器就停止了。 是的!state 就是一个组件的核心!!! 下面我们来看一下,如何正确的使用它!\n// 🅰️ 不要直接修改 state,应该使用 setstate() // 构造函数是唯一可以给 this.state 赋值的地方: this.state.comment = \u0026#39;hello\u0026#39;;\t// ❌ this.setstate({comment: \u0026#39;hello\u0026#39;});\t// ✔️ // 🅱️ state 的更新可能是异步的,不要依赖他们的值来更新下一个状态 this.setstate({ counter: this.state.counter + this.props.increment, });\t// 可能 ❌ // 要解决这个问题,可以让 setstate() 接收一个函数而不是一个对象, // 这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数 this.setstate((state, props) =\u0026gt; ({ counter: state.counter + props.increment }));\t// ✔️ 数据是向下流动的!\n不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。\n组件可以选择把它的 state 作为 props 向下传递到它的子组件中。\n事件处理 react 元素的事件处理和 dom 元素的很相似,但是有一点语法上的不同:\nreact 事件的命名采用小驼峰式(camelcase),而不是纯小写; 使用 jsx 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串; 在 react 中另一个不同点是你不能通过返回 false 的方式阻止默认行为,你必须显式的使用 preventdefault 。 function actionlink() { function handleclick(e) { // 在这里,e 是一个合成事件,react 根据 w3c 规范来定义这些合成事件 e.preventdefault();\t// 显式的使用 ✔️ console.log(\u0026#39;the link was clicked.\u0026#39;); } return ( \u0026lt;a href=\u0026#34;#\u0026#34; onclick={handleclick}\u0026gt;\t// 注意,大括号外不要加引号 click me \u0026lt;/a\u0026gt; ); } 另外,当你使用 es6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。如下:\nclass toggle extends react.component { constructor(props) { super(props); this.state = {istoggleon: true}; // 为了在回调中使用 `this`,这个绑定是必不可少的,否则 // 当你调用 onclick={this.handleclick} 这个事件函数回调的时候, // `this` 的值为 `undefined` ,会报错 this.handleclick = this.handleclick.bind(this); // bind 太麻烦 ?试试下面这个等效写法 - class fields 语法 // create react app 默认启用此语法 // 此语法确保 `handleclick` 内的 `this` 已被绑定 // 注意:这是 *实验性* 语法 // handleclick = () =\u0026gt; { // console.log(\u0026#39;this is:\u0026#39;, this); // } } handleclick() { this.setstate(state =\u0026gt; ({ istoggleon: !state.istoggleon })); } render() { return ( \u0026lt;button onclick={this.handleclick}\u0026gt; {this.state.istoggleon ? \u0026#39;on\u0026#39; : \u0026#39;off\u0026#39;} \u0026lt;/button\u0026gt; ); } } reactdom.render( \u0026lt;toggle /\u0026gt;, document.getelementbyid(\u0026#39;root\u0026#39;) ); 你必须谨慎对待 jsx 回调函数中的 this,在 javascript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleclick 并把它传入了 onclick,当你调用这个函数的时候 this 的值为 undefined。\n这并不是 react 特有的行为,这其实与 javascript 函数工作原理有关。\n= emm\u0026hellip; this 可以说是 javascript 永远的痛了,好在应用起来并不算太难!*\n在事件处理中,除了 this 的绑定之外,还有一个需要注意的地方 - 向事件处理程序传递参数。\n在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 id,以下两种方式都可以向事件处理函数传递参数:\n\u0026lt;button onclick={(e) =\u0026gt; this.deleterow(id, e)}\u0026gt;delete row\u0026lt;/button\u0026gt; \u0026lt;button onclick={this.deleterow.bind(this, id)}\u0026gt;delete row\u0026lt;/button\u0026gt; 上述两种方式是等价的,分别通过箭头函数和 function.prototype.bind 来实现。\n在这两种情况下,react 的事件对象 e 会被作为第二个参数传递。 如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。\n条件渲染 这里,就不多讲了,只要记住 jsx 最终会被转成一个 javascript 对象,条件渲染也就是 if 或者条件运算符那点事了。\n在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。在组件的 render 方法中返回 null 并不会影响组件的生命周期。\n想要了解更多,直接阅读 条件渲染 。\n列表 \u0026amp; key 同上,略!\n唯一需要注意的是 key ,它是什么?\nkey 帮助 react 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。\n元素的 key 只有放在就近的数组上下文中才有意义。\n= 所谓列表,就是利用一些迭代数据,组装出可用子元素集合,然后把它们放在应该放的父元素中就可以了。*\n详见 列表 \u0026amp; key 。\n表单 主要是弄清楚 “受控组件” 和 “非受控组件” 的概念,就可以喽。详见 表单 。\n状态提升 = 抽象和共享,永远不变的真理!\n通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。\n在 react 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。\n更多详见 状态提升。\n组合 vs 继承 详见 组合 vs 继承 – react - react.docschina.org。\nreact 哲学 我们认为,react 是用 javascript 构建快速响应的大型 web 应用程序的首选方式。\n= emm\u0026hellip; vue:我才是,angular:你们都是弟弟!\nok,上心法 ❤️。\n第一步:将设计好的 ui 划分为组件层级\n第二步:用 react 创建一个静态版本\n第三步:确定 ui state 的 =最小(且完整)表示\n通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:\n该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。 该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。 你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。 第四步:确定 state 放置的位置\n第五步:添加反向数据流\n= 基础的核心概念并不多(毕竟就是一个 ui 库嘛),但其思想非常好,官方文档也相当 ok ,可以不定期地多看几遍。\nhook = 不着急,先过几遍再说这个,很简单的!*\n相关技术栈 当然,你可以选择从零开始,但更好的选择是使用官方提供的脚架 - create react app 。\n样式 react 对样式如何定义并没有明确态度;如果存在疑惑,比较好的方式是和平时一样,在一个单独的 *.css 文件定义你的样式,并且通过 classname 指定它们。\nreact 并没有原生提供 css 封装方案!!!\nreact 本身的设计原则决定了其不会提供原生的 css 封装方案,或者说 css 封装并不是 react 框架本身的关注点。因此 ,react 社区从很早的时候就开始寻找相关替代办法。\n- css 模块化(css modules)\r这种做法非常类似 angular 与 vue 对样式的封装方案,其核心是以 css 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。对于大多数 react 项目来说,这种方案已经足够用了。\r- 基于共识的人工维护的方法论,如 bem\r这种方法的缺点是会为团队带来很大的挑战,对于全局和局部规划选择器的命名,团队对于这种方法需要有共识,即使熟练使用的情况下,在使用中依然有着较高的思维负担和维护成本。\r- shadow dom\r借助 direflow.io 等工具,我们可以将 react 组件输出为 web component,借助 shadow dom 实现组件的 css 样式封装。这是一种解决办法,不过基本很少有项目选择这样做。\r- css-in-js _1. scss\n好吧,相信你的项目是由 create react app (cra) 生成的,如果你想使用 scss ,只需要安装 dart-sass 库即可,像下面这样:\nnpm i sass --save-dev 感谢 node-sass 退出历史舞台,但感谢作者的贡献 😅!\nok,安装之后,就可以把 *.scss 文件作为一个模块引入了,如:\nimport example from \u0026#39;./example.scss\u0026#39;; _2. css in js\n注意此功能并不是 react 的一部分,而是由第三方库提供。\n“css-in-js” 是指一种模式,其中 css 由 javascript 生成而不是在外部文件中定义。在 此处 阅读 css-in-js 库之间的对比。\n= css in js 的本质就是写行内样式 style ❓❗*\nconst style = { \u0026#39;color\u0026#39;: \u0026#39;red\u0026#39;, \u0026#39;fontsize\u0026#39;: \u0026#39;46px\u0026#39; }; const clickhandler = () =\u0026gt; alert(\u0026#39;hi\u0026#39;); reactdom.render( \u0026lt;h1 style={style} onclick={clickhandler}\u0026gt; hello, world! \u0026lt;/h1\u0026gt;, document.getelementbyid(\u0026#39;example\u0026#39;) ); 当然,大项目,这样直接写是非常不明智的,好在有懒人包 🥳!\n目前比较流行的两个解决方案是 styled-components 和 emotion 。\n相关参考:\ncss-in-js:一个充满争议的技术方案 - 知乎 - zhuanlan.zhihu.com _3. css modules 🏆(首推)\n这种做法非常类似 angular 与 vue 对样式的封装方案,其核心是以 css 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。对于大多数 react 项目来说,这种方案已经足够用了。\n由于一般的脚手架都默认集成了 css modules,比如 react 官方的脚手架:create-react-app,已经将 css modules 集成进来了,我们可以直接使用。\n如何使用呢?\n详见 在 react 中使用 css modules。\n路由 = hmmm\u0026hellip; 页面路由,大大的有用!\nreact-router-dom 使用指南(最新 v6.0.1) - 知乎 - zhuanlan.zhihu.com introduction · react router 中文手册 - uprogrammer.cn ","date":"2022-09-19","permalink":"https://loveminimal.github.io/posts/react/","summary":"\u003cp\u003e\u003ca href=\"%5Bhttps://%5D(https://react.docschina.org/docs/getting-started.html)\"\u003e📔 官方文档\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e具体安装及引入细节,请直接参考官方文档。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eReact 是一个用于构建用户界面的 JavaScript 库,你可以用它给简单的 HTML 页面增加一点交互,也可以开始一个完全由 React 驱动的复杂应用。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u0026gt; 对的,它只是一个 UI 库而已 !!!\u003c/code\u003e\u003c/p\u003e","title":"react"},{"content":"该主题已上传到官方主题面,跳转了解 hugo-theme-virgo ,当然也可以直接访问 github 仓库 。\n更新日志 - 2023-03-23 14:58 增加 utterances 评论插件\r- 2023-03-22 16:08 更新主页快捷联系选项,增加知乎和简书\r- 2023-03-21 17:58 更新导航页面文件结构 - `nav.md` 或 `nav/index.md`\r- 2023-03-19 00:05 优化导航页面快速跳转 快速开始 首先,下载该主题。\ncd your_site_dir\t# 本地站点目录 git clone https://github.com/loveminimal/hugo-theme-virgo.git themes/virgo # or cd your_site_dir git submodule add https://github.com/loveminimal/hugo-theme-virgo.git themes/virgo 💡 使用哪种方式呢?如果你有这个疑问,那么就选择第一种。\n然后,更新你站点的 config.toml 内容,如下(后续可按需修改):\nbaseurl = \u0026#39;https://ovirgo.com/\u0026#39; languagecode = \u0026#39;zh-cn\u0026#39; title = \u0026#39;virgo\u0026#39; # 启用主题 theme = \u0026#39;virgo\u0026#39; hascjklanguage = true summarylength = 80 paginate = 11 enablegitinfo = true [params] author = \u0026#39;jack\u0026#39; slogan = \u0026#39;life should be interesting.\u0026#39; # \u0026#39;life is just a joker.\u0026#39; # 页面语言,默认中文 en = false # 英文首页标题,默认 \u0026#39;virgo\u0026#39; hometitleen = \u0026#39;virgo\u0026#39; # 中文首页标题,默认 ‘一晌贪欢’ hometitlezh = \u0026#39;一晌贪欢\u0026#39; # 激活暗色模式, # 由于静态页面的限制,我们使用浏览器本地存储来记忆该状态, # 如果设置为 `true` 后,默认不是暗色模式,清除浏览器缓存后刷新页面即可 dark = false # 文章列表页单列显示 issinglecolumnofpostlist = true # 是否显示相邻页链接 isshowprevnextlink = true # 激活页面加载时的过渡动画 hasactiveanimate = true # 激活 cool 模式,酷爽但是消耗资源也更多, # 如果想更换页面背景,只需要将图片命名为 `default.jpg` 后,置于 `/static/imgs/bg` 文件夹中即可, # 浏览器有缓存,更换后强制页面刷新(快捷键为 ctrl+shift+r)一下即可 hasactivecool = true # 展开/折叠代码块,默认不折叠, # 设置为 `true` ,则默认折叠所有代码块, # 提示,在移动设备中,系统设置为永久折叠代码块 # (该项设置不重要,完全是个人喜好) hasfoldallcodeblocks = false # 如下导航链接,你应该创建对应的 `.md` 文件,以生成对应的页面 # ----------------------------------- # nav - nav.md or nav/index.md # search - search.md or search/index.md # archive - archive.md or archive/index.md # wiki - posts/wiki.md or wiki/index.md # about - about.md or about/index.md # ----------------------------------- # 菜单选项定制,使用 `00、01、23` 等进行选项顺序调整 # 🐶🎉👀💡👓🐌 [params.menu] [params.menu.00] active = true path = \u0026#39;/nav\u0026#39; en = \u0026#39;nav\u0026#39; zh = \u0026#39;导航\u0026#39; icon = \u0026#39;🎯\u0026#39; [params.menu.11] active = true path = \u0026#39;/search\u0026#39; en = \u0026#39;search\u0026#39; zh = \u0026#39;搜索\u0026#39; icon = \u0026#39;🔎\u0026#39; [params.menu.22] active = true path = \u0026#39;/posts\u0026#39; en = \u0026#39;posts\u0026#39; zh = \u0026#39;文章\u0026#39; icon = \u0026#39;📜\u0026#39; [params.menu.33] active = true path = \u0026#39;/archive\u0026#39; en = \u0026#39;archive\u0026#39; zh = \u0026#39;归档\u0026#39; icon = \u0026#39;🎉\u0026#39; [params.menu.44] active = true path = \u0026#39;/posts/wiki\u0026#39; en = \u0026#39;wiki\u0026#39; zh = \u0026#39;百科\u0026#39; icon = \u0026#39;🚀\u0026#39; [params.menu.55] active = true path = \u0026#39;/about\u0026#39; en = \u0026#39;about\u0026#39; zh = \u0026#39;关于\u0026#39; icon = \u0026#39;🐌\u0026#39; # 首页图片/文字 [params.img] # 如果你不想显示图片,想显示一段话,只需要 # 设置 `noimgbutwords` 为 true 即可 notimgbutwords = false # 内置了 `girl.jpg, wukong.jpg, and tux.jpg, cat.svg ……`,当然你可以 # 把自己喜欢图片放在 `static/imgs/` 目录中,并在 `src` 引用它, # 你还可以通过 `width` 调整引入图片的显示大小, # 如果,将 `width` 设置为 \u0026#39;\u0026#39; 或 0 , # 将默认使用图片自身分辨率尺寸 src = \u0026#39;cat.svg\u0026#39; width = 0 # words = \u0026#34;stay hungry, stay foolish. \u0026lt;br\u0026gt;your time is limited, so don\u0026#39;t waste it living someone else\u0026#39;s life. \u0026lt;br\u0026gt;have the courage to follow your heart and intuition. they somehow already know what you truly want to become. everything else is secondary. \u0026lt;br\u0026gt;-- steve jobs.\u0026#34; # words = \u0026#34;多少事,从来急;\u0026lt;br\u0026gt;天地转,光阴迫。\u0026lt;br\u0026gt;一万年太久,只争朝夕。\u0026lt;br\u0026gt;-- 教员\u0026#34; words = \u0026#34;“照顾好自己的身体和情绪,\u0026lt;br\u0026gt;这场人生,\u0026lt;br\u0026gt;你就赢了一大半,\u0026lt;br\u0026gt;其余的其余,\u0026lt;br\u0026gt;人生自有安排。”\u0026#34; [params.contact] icp = \u0026#39;豫 icp 备 2022002918 号-1\u0026#39; # 备案号,如果你不想显示,设置为 \u0026#39;\u0026#39; 空即可 icplink = \u0026#39;//beian.miit.gov.cn\u0026#39; # 备案链接 weibo = \u0026#39;6867589681\u0026#39; # e.g. https://weibo.com/u/6867589681 wechat = \u0026#39;imgs/bg/wechat-public.jpg\u0026#39; # 微信二维码地址 # zhihu = \u0026#39;loveminimal\u0026#39; # e.g. https://www.zhihu.com/people/loveminimal # jianshu = \u0026#39;eebcc2974936\u0026#39; # e.g. https://www.jianshu.com/u/eebcc2974936 email = \u0026#39;loveminimal@outlook.com\u0026#39; github = \u0026#39;loveminimal\u0026#39; # e.g. https://github.com/loveminimal bilibili = \u0026#39;11608450\u0026#39; # e.g. https://space.bilibili.com/11608450 # twitter = \u0026#39;loveminimal163\u0026#39; # e.g. https://twitter.com/loveminimal163 # facebook = \u0026#39;loveminimal\u0026#39; # e.g. https://facebook.com/loveminimal # instagram = \u0026#39;loveminimal163\u0026#39; # e.g. https://www.instagram.com/loveminimal163 # youtube = \u0026#39;uckwibwe3rztdambs0gjngka\u0026#39; # e.g. https://www.youtube.com/channel/uckwibwe3rztdambs0gjngka # telegram = \u0026#39;loveminimal\u0026#39; # e.g. https://web.telegram.org/k/#@loveminimal color = \u0026#39;#696969\u0026#39; # 图标颜色,默认为浅灰色 slogan = \u0026#39;find me via : )\u0026#39; # 联系标语,不想显示,可以置空 # 在开发环境下(http://localhost:1313/),不再启用评论插件, # 如果想在开发环境下启用它,修改服务端口即可,如下 # hugo server -p=1314 [params.utterances] active = true # 是否启用评论插件 repo = \u0026#34;loveminimal/comment\u0026#34; # 输入你的仓库名称 issueterm = \u0026#34;pathname\u0026#34; theme = \u0026#34;github-light\u0026#34; crossorigin = \u0026#34;anonymous\u0026#34; # 以下为 markdown 解析擎的一些设置, # 建议保持不变 [markup] [markup.asciidocext] preservetoc = true [markup.highlight] # 代码块显示风格、行号显示 style = \u0026#34;github\u0026#34; linenos = false [markup.tableofcontents] endlevel = 3 ordered = false startlevel = 2 [markup.goldmark] [markup.goldmark.renderer] unsafe = true 现在,你就可以运行 hugo server -d 开始你的折腾之旅了。\n导航页 如果,你想使用导航页并正确显示,那么就应该严格按照下面这种格式搭建你的 nav.md 文件结构:\n--- title: \u0026#34;nav\u0026#34; draft: false --- \u0026lt;div class=\u0026#34;nav\u0026#34;\u0026gt; ## 🌞 *分类 one* - [mineitem one](/) - [mineitem two](/archive) - [mineitem three](https://nav-item-three.com) ## 🔨 *分类 two* - [toolitem three](https://nav-item-three.com) ## 📑 *分类 three* - [docitem one](/) - [docitem two](/archive) - [docitem three](https://nav-item-three.com) \u0026lt;/div\u0026gt; ## 🔖 *标签 bookmarks* \u0026lt;div class=\u0026#34;bookmark\u0026#34;\u0026gt; - bookmark item one https://bookmark-item-one.com - bookmark item two https://bookmark-item-two.com - bookmark item three https://bookmark-item-three.com \u0026lt;/div\u0026gt; 为什么要做格式方面的限制呢?\n众所周知,markdown 对 table 的支持很一般,鉴于导航页的内容主要是外链和书签,使用列表管理是最方便的。另外,我们会使用 js 进行内容项的统计,所以就需要使用者遵守格式,不然可能页面显示可能会不正常。\n标记语法增强 \u0026gt; 使用 js 对 markdown 做出的一些增强性修改\n不止一次吐槽过 markdown 虽然是纯文本性质的,但是其某些标记语法真的是让人不敢恭维,直观性和表现力都是一般。不过,从另一个方面来说,本来就是轻量级的标记语言,不可能承载太多。\n本来想直接修改 markdown 引擎来实现,研究了一下,还要颇费一番工夫。鉴于仅满足于个人使用,用一些曲线方式使用 js 来实现反而更加简单些。\n此处就记录一下针对 hugo-theme-virgo 做的一些魔改。\n行内格式 markdown 中的行内格式有以下几种:\n语法 效果 转译 html 标签 **加粗** 加粗 \u0026lt;strong\u0026gt;加粗\u0026lt;/strong\u0026gt; *斜体* 斜体 \u0026lt;em\u0026gt;斜体\u0026lt;/em\u0026gt; ~~删除线~~ 删除线 \u0026lt;del\u0026gt;删除线\u0026lt;/del\u0026gt; ` 行内代码 行内代码 \u0026lt;code\u0026gt;行内代码\u0026lt;/code\u0026gt; \u0026mdash; 下划线 \u0026lt;u\u0026gt;下划线\u0026lt;/u\u0026gt; 是的,markdown 中没有下划线的标记语法。\n本来想用行内代码的标记格式做魔改,鉴于博文中出现行内的代码的概率较高,遍历起来相对更耗性能(虽然并没有多少),故决定选择 *斜体* 语法标记,其使用频率不多,且其对应的 org mode 中可以直接显为粗体显示。\n新增语法 效果 *_下划线* _下划线 *=高亮* =高亮 | *-高亮* | -高亮 | ❌\n| *=吐槽系* | =吐槽系 | ❌\n如此,我们便增加了 _下划线 和 =高亮 两种语法标识了。另外,在文章中,尤其是一些摘录和转载的文章中,我们需要做一些随笔,之前我们是使用 \u0026lt;div class=\u0026quot;oh-essay\u0026quot;\u0026gt;...\u0026lt;/div\u0026gt; 这种标签插入,如上表,我们也对其做了语法标识。\n如上,所示,更改了一些语法标记,因为有的 markdown 引擎中使用 ==高亮== 来高亮文本,我们这里就用 *=高亮* 来表示,以做到在观感上统一。\n另外,我们不再使用 *=吐槽系* 来表示个人在摘录或编辑中的个人想法展示,主要是由于在不支持当前语法标记的主题中,它只能以斜体展示,不容易和正文内容作区分。我们使用 \u0026gt;= 吐槽系 或 \u0026gt; = 吐槽系 来表示,如此在不支持的情况下,可以解析为引用样式,便于区分。\n之前我们做了一些 snippet 进行 html 标签的插入,以实现以上效果,但是这就限定在了某些编辑器中,些许背离了纯文本输入的理念,以上小小的增强,使得我们可以任何文本编辑器中进行方便的文本输入。\n\u0026gt;= 好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅\r\u0026gt; =好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅\r\u0026gt; = 好吧,尽管它们只能在 `hugo-theme-virgo` 中才有效果 😅 上述语法标记是等价的,会被解析为如下样式:\n= 好吧,尽管它们只能在 hugo-theme-virgo 中才有效果 😅\n代码块折叠 在 markdown 中,包裹代码块很方便。但有时候在博文中,我们可能引入较多的代码片段,这会导致正文内容的间断,所以,允许其进行折叠,可以在 config.tom 中,使用 hasfoldallcodeblocks: true 进行初始化。\n既然已经可以折叠了,这里我们不妨用它再做一个更通用的折叠板(默认折叠),原理也很简单,利用 lang 判别。\n如果其为 _lang 这种格式,则表示轻量级代码折叠,效果如下:\n这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。这是一个代码层级的折叠,其不会换行。 可以看到,上面的内容并没有换行,如果它的代码内容,自然是没有问题的,但对于一个通用的折叠板来说,我们希望其可以自动换行,很简单,使用 __lang 即可,如下:\n会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽!会自动换行喽! wiki 图片链接语法渲染 最近使用 obsidian ,其使用的链接及图片格式为 wiki 语法,如下:\n名称 描述 链接 [[link]] 链接(带描述) [[link | desc]] 图片 ![[path/to/img]] 图片(带尺寸) ![[path/to/img | 200]] hugo 默认的 markdown 引擎是不支持渲染这种语法的,我们这里做了一下增强,现在你可以畅快地使用 obsidian 来编辑你的博客了。\n= 图片和链接,好像 wiki 的这种语法写起来更加简洁。其实,还是使用 \u0026lt;img\u0026gt; 标签的通用性更好些,不过许多软件的即时渲染又不支持,就很伤。\n*为什么开始使用 obsidian ?\n它的双向链接功能不错,使用了几天,整个编辑体验是要比 typora 好的,后者其实也不弱,但扩展性不好,开发者方面迭代也好慢。比如,其默认 快速打开 文件是在新的窗口(想要在当前窗口打开),而且无法设置,邮件也发了,issue 也提了,久久没有回应,这就让人很无奈。相对来说,vscode 和 obsidian 这种具有第三方插件扩展的,不满意的话可以自己修改,就很不错,emacs 和 vim 更是灵活扩展方面的翘楚。\n站点编辑 你可以在 《我是怎么写博客的》 了解到本站内容的日常编写流程。\n","date":"2022-09-13","permalink":"https://loveminimal.github.io/posts/about-virgo/","summary":"\u003cp\u003e该主题已上传到官方主题面,跳转了解 \u003ca href=\"https://themes.gohugo.io/themes/hugo-theme-virgo/\"\u003ehugo-theme-virgo\u003c/a\u003e ,当然也可以直接访问 \u003ca href=\"https://github.com/loveminimal/hugo-theme-virgo\"\u003eGithub 仓库\u003c/a\u003e 。\u003c/p\u003e","title":"♫ 关于 virgo 需要知道的一些事"},{"content":" 好吧,内容不少,为了后续插入图片之后 ,页面太大,我们这里拆分到两个页面中。\n26. 图形用户界面 i.e. graphical user interfaces\n(。・∀・)ノ゙嗨 我是 carrie anne 欢迎收看计算机科学速成课。\n我们上集最后 ,谈了苹果在 1984 年发布的 macintosh ,这是普通人可以买到的 第一台带图形用户界面的计算机,还带一个鼠标。那时的计算机全是命令行, 图形界面是个革命性进展,不必记住或猜正确的命令,图形界面直接显示了,你可以做什么,只要在屏幕上找选项就行了。这是一个\u0026quot;选择并点击\u0026quot;的界面,突然间计算机更直观了。不只是爱好者或科学家能用计算机 ,任何人都可以用计算机解决问题。\n人们认为是 macintosh 把图形用户界面(gui)变成主流,但实际上图形界面是数十年研究的成果。前几集,我们讨论了早期的交互式图形程序,比如 sketchpad 和太空战争 都是 1962 年制作的,但都是一次性项目,不是整合良好的体验,现代图形界面的先驱 可以说是 道格拉斯·恩格尔巴特。\n让我们进入思想泡泡!\n二战期间 恩格尔巴特 驻扎在菲律宾做雷达操作员,他读了 万尼瓦尔·布什 的 memex 文章,这些文章启发了他。当他海军服役结束时,他回到学校 1955 年在 ucb 取得博士学位,他沉溺于新兴的计算机领域,他在 1962 年一份开创性报告中 汇集了各种想法,报告名为:\u0026ldquo;增强人类智力\u0026rdquo;。恩格尔巴特认为,人类面临的问题 比解决问题的能力增长得更快,因此,找到增强智力的方法 似乎是必要且值得一做的目标。他构想计算机不仅做自动化工作,也可以成为未来知识型员工 应对复杂问题的工具。\n伊凡·苏泽兰 的\u0026quot;几何画板\u0026quot; 进一步启发了 恩格尔巴特,他决定动手把愿景变为现实 开始招募团队来做 on-line system ,他意识到如果只有键盘 ,对他想搭建的程序来说是不够的。用他的话说:\u0026ldquo;我们设想人们用计算机辅助工作站来增强工作,用户需要和屏幕上的信息互动,用某种设备在屏幕上移动 [光标]\u0026quot;。\n1964 年,和同事比尔·英格利希的共同努力下,他创造了第一个计算机鼠标,尾部有一根线,看起来很像老鼠 因此\u0026quot;鼠标\u0026quot;这个名字沿用了下来。\n谢了思想泡泡!\n1968 年 恩格尔巴特 在\u0026quot;秋季计算机联合会议\u0026quot;展示了他的系统 ,这次演示 被视为如今所有演示的祖先,演示有 90 分钟 展现了现代计算机的许多功能:包括 位图图像、视频会议、文字处理和实时协作编辑文件,还有现代图形界面的原型 - 比如鼠标和多窗口, 不过窗口不能重叠,远远先于那个时代。\n就像其它\u0026quot;跨时代\u0026quot;的产品一样,它最终失败了,至少商业上是这样,但它对当时的计算机研究者影响巨大,恩格尔巴特 因此获得 1997 年图灵奖。\n政府资金在 1970 年代初开始减少,我们在两集前说过(第 24 集:冷战和消费主义)。那时,恩格尔巴特团队里的许多人,包括比尔·英格利希去了施乐公司新成立的\u0026quot;帕洛阿尔托研究中心\u0026rdquo;,更为人熟知的名字是 xerox parc 。他们在这里开发了第一台带真正 gui 的计算机:施乐奥托 , 于 1973 年完成。为了让计算机易于使用,需要的不只是花哨的图形,还要借助一些人们已经熟悉的概念,让人们不用培训 就能很快明白如何使用。施乐的答案是将 2d 屏幕当作\u0026quot;桌面\u0026quot;,就像桌面上放很多文件一样,用户可以打开多个程序 每个程序都在一个框里,叫\u0026quot;窗口\u0026quot;,就像桌上的文件一样。窗口可以重叠,挡住后面的东西,还有桌面配件,比如计算器和时钟。用户可以把配件在屏幕上四处移动,它不是现实桌面的完美复制,而是用桌面这种隐喻,因此叫\u0026quot;桌面隐喻\u0026quot;。有很多方法来设计界面, 但 alto 团队用窗口,图标,菜单和指针来做 - 因此叫 wimp 界面。如今大部分图形界面都用这个,它还提供了一套基本部件,可复用的基本元素, 比如按钮,打勾框,滑动条和标签页,这些也来自现实世界,让人们有熟悉感,gui 程序就是这些小组件组成的。\n让我们试着写一个简单例子。\n首先,我们必须告诉操作系统 为程序创建一个窗口\r我们通过 gui api 实现 需要指定窗口的名字和大小\r假设是 500×500 像素\r现在再加一些小组件,一个文本框和一个按钮\r创建它们需要一些参数\r首先要指定出现在哪个窗口 因为程序可以有多个窗口\r还要指定默认文字窗口中的 x,y 位置 以及宽度和高度\r好,现在我们有个 看起来像 gui 程序的东西\r但它还没有功能\r如果点 roll 按钮,什么也不会发生\r在之前的例子中,代码是从上到下执行的\r但 gui 是 \u0026#34;事件驱动编程\u0026#34;\r代码可以在任意时间执行 以响应事件\r这里是用户触发事件 比如点击按钮,选一个菜单项,或滚动窗口\r或一只猫踩过键盘\r就会一次触发好多事件!\r假设当用户点 roll 按钮\r我们产生 1 到 20 之间的随机数\r然后在文本框中,显示这个数字\r我们可以写一个函数来做\r我们还可以让它变有趣些,假设随机数是 20 就把背景颜色变成血红色!\r最后,把代码与\u0026#34;事件\u0026#34;相连 每次点按钮时 都触发代码\r那么,要设置事件触发时 由哪个函数来处理\r我们可以在初始化函数中,加一行代码来实现\r我们要处理的,是\u0026#34;点击\u0026#34;事件 然后函数会处理这个事件\r现在完成了 可以点按钮点上一整天 每次都会执行 rolld20 函数,这就是程序背后的原理。在编辑器里点 粗体 ,或菜单里选 关机 ,一个处理该事件的函数会触发,希望不会随机到 20,啊!!!\n好,现在回到施乐奥托!\n大约制作了 2000 台奥托有的在施乐公司内部用,有的送给大学实验室,从来没有商业出售过,然而,parc 团队不断完善硬件和软件,最终于 1981 年发布了 施乐之星系统,施乐之星扩展了\u0026quot;桌面隐喻\u0026quot;。现在文件看起来就像一张纸 ,还可以存在文件夹里,这些都可以放桌面上,或数字文件柜里,这样来隐喻底层的文件系统。\n从用户角度来看,是一层新抽象!\n施乐卖的是印刷机 但在文本和图形制作工具领域也有领先,例如,他们首先使用了 \u0026ldquo;剪切\u0026quot;\u0026ldquo;复制\u0026quot;\u0026ldquo;粘贴\u0026quot;这样的术语,这个比喻来自编辑打字机文件,真的是剪刀\u0026quot;剪切\u0026rdquo; 然后胶水\u0026quot;粘贴\u0026rdquo; 到另一个文件,然后再复印一次,新文件就是一层了,看不出编辑的痕迹……\n感谢计算机的出现!\n文字处理软件出现后 这种手工做法就消失了。apple ii 和 commodore pet 上有文字处理软件,但施乐在这点上走的更远。无论你在计算机上做什么, 文件打印出来应该长得一样,他们叫这个\u0026quot;所见即所得\u0026rdquo;。不幸的是,就像恩格尔巴特的 on-line system ,施乐之星也领先于那个时代,销售量不高,因为在办公室里配一个,相当如今 20 万美元 。ibm 同年推出了 ibm pc ,之后便宜的\u0026quot;ibm 兼容\u0026quot;计算机席卷市场,但 parc 研究人员花了十几年做的这些 没有被浪费。\n1979 年 12 月,施乐之星出货前一年半,有个人去施乐公司参观 你可能听说过这个人:史蒂夫·乔布斯。这次参观有很多传闻,许多人认为乔布斯和苹果偷走了施乐的创意,但那不是事实。事实上是施乐公司主动找苹果,希望合作,最终施乐还买了苹果的一百万美元股份,在苹果备受瞩目的 首次公开募股 (ipo) 前买的,但一个额外条款是: \u0026ldquo;公布一切施乐研究中心正在进行的酷工作\u0026rdquo; 。史蒂夫知道他们很厉害,但他完全没预想到这些,其中有个演示是,一个清晰的位图显示器上,运行着施乐公司的图形界面 ,操作全靠鼠标直观进行。史蒂夫后来说:\u0026ldquo;就像拨开了眼前的一层迷纱,我可以看到计算机产业的未来\u0026rdquo;。史蒂夫和随行的工程师回到苹果公司,开始开发新功能,比如菜单栏和垃圾桶,垃圾桶存删除文件,满了甚至会膨胀 - 再次使用了隐喻。苹果第一款有图形界面和鼠标的产品,是 1983 年发行的 apple lisa ,一台超级先进的机器,标了\u0026quot;超级先进\u0026quot;的价格 - 差不多是如今的 25000 美元。虽然比施乐之星便宜不少,但在市场上同样失败。幸运的是,苹果还有另一个项目: macintosh,于 1984 年发布,价格大约是如今的 6000 美元 - lisa 的四分之一。它成功了,开售 100 天就卖了 7 万台,但在最初的狂潮后,销售额开始波动。苹果公司卖的 apple ii 比 mac 多,一个大问题是:没人给这台新机器做软件,之后情况变得更糟,竞争对手赶上来了。不久,其它价格只有 mac 几分之一的个人计算机 有了原始但可用的图形界面,消费者认可它们, pc 软件开发者也认可。随着苹果的财务状况日益严峻 以及和苹果新 ceo 约翰·斯卡利 的关系日益紧张,史蒂夫乔布斯被赶出了苹果公司。几个月后,微软发布了 windows 1.0 ,它也许不如 mac os 漂亮,但让微软在市场中站稳脚跟 奠定了统治地位。十年内,95%的个人计算机上都有微软的 windows。最初,mac os 的爱好者还可以说 mac 有卓越的图形界面和易用性,windows 早期版本都是基于 dos 而 dos 设计时 ,没想过运行图形界面,但 windows 3.1 之后,微软开始开发新的,面向消费者的 gui 操作系统,叫 windows 95,这是一个意义非凡的版本 ,不仅提供精美的界面,还有 mac os 没有的高级功能,比如\u0026quot;多任务\u0026quot;和\u0026quot;受保护内存\u0026quot;。windows 95 引入了许多 如今依然见得到的 gui 元素,比如开始菜单,任务栏和 windows 文件管理器。\n不过微软也失败过,为了让桌面更简单友好, 微软开发了一个产品叫 microsoft bob ,将比喻用到极致。现在屏幕上有了一个虚拟房间,程序是物品,可以放在桌子和书架上,甚至还有噼啪作响的壁炉 和提供帮助的虚拟狗狗,你看到那边的门没?,是的,那些门通往不同房间 房间里有不同程序,你可能猜到了,它没有获得成功。这是一个好例子,说明如今的用户界面是自然选择后的结果。无论你用的是 windows,mac,linux 或其他 gui,几乎都是施乐奥托 wimp 的变化版。一路上,人们试了各种做法并失败了。一切都必须发明,测试,改进,适应或抛弃,如今,图形界面无处不在 使用体验一般只是可以接受,而不是非常好,你肯定体验过差劲的设计,比如下载了很烂的 app,用过别人糟糕的手机,或者看到过很差的网站,因此计算机科学家和界面设计师 会继续努力工作,做出更好更强大的界面,朝着恩格尔巴特\u0026quot;增强人类智能\u0026quot;的愿景努力。\n我们下周见。\n27. 3d 图形 i.e. 3d graphics\n嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n在过去五集,我们从基于电传打字机的命令行界面讲到图形怎么显示到屏幕上,再到上集的 图形用户界面(gui),以及图形界面的美味。\n之前的例子都是 2d, 但我们生活的世界是 3d 的,我也是个三维 girl~\n所以今天,我们讲 3d 图形的基础知识,以及如何渲染 3d 图形到 2d 屏幕上。24 集中说过,可以写一个函数,从 a 到 b 画一条线,通过控制 a 和 b 的 (x,y) 坐标,可以控制一条线。在 3d 图像中,点的坐标不再是两点,而是三点,x,y,z ,或读\u0026quot;zee\u0026quot;,但我之后会读成\u0026quot;zed\u0026quot; 。当然,2d 的电脑屏幕上、不可能有 xyz 立体坐标轴,所以有图形算法 负责把 3d 坐标\u0026quot;拍平\u0026quot;显示到 2d 屏幕上,这叫 \u0026ldquo;3d 投影\u0026rdquo; 。所有的点都从 3d 转成 2d 后,就可以用画 2d 线段的函数 来连接这些点,这叫 \u0026ldquo;线框渲染\u0026rdquo; 。想象用筷子做一个立方体,然后用手电筒照它,墙上的影子就是投射,是平的。如果旋转立方体,投影看起来会像 3d 物体,尽管是投影面是平的,电脑也是这样 3d 转 2d ,只不过用大量数学,而不是筷子。\n3d 投影有好几种,你现在看到的,叫 正交投影 。立方体的各个边,在投影中互相平行,在真实 3d 世界中,平行线段会在远处收敛于一点,就像远处的马路汇聚到一点,这叫 透视投射 。过程是类似的,只是数学稍有不同。有时你想要透视投影,有时不想,具体取决于开发人员。\n如果想画立方体这种简单图形,直线就够了,但更复杂的图形,三角形更好,在 3d 图形学中 我们叫三角形\u0026quot;多边形\u0026quot;(polygons),看看这个多边形组成的 漂亮茶壶。一堆多边形的集合叫 网格 。网格越密,表面越光滑,细节越多,但意味着更多计算量。游戏设计者要平衡角色的真实度 和多边形数量,如果数量太多 帧率会下降到肉眼可感知,用户会觉得卡,因此有算法用来简化网格。\n之所以三角形更常用 而不是用正方形,或其它更复杂的图形,是因为三角形的简单性。空间中三点定义一个平面,如果给 3 个 3d 点,我能画出一个平面,而且只有这一个答案,4 个或多于 4 个点就不一定了,而 2 个点不够定义平面,只能定义线段,所以 3 是最完美的数字,三角形万岁。\n线框渲染 虽然很酷,但 3d 图像需要填充,填充图形的经典算法叫 扫描线渲染 (scanline rendering) ,于 1967 年诞生在犹他州大学。为了例子简单,我们只看一个多边形。我们要思考这个多边形如何转成一块填满像素的区域,我们先铺一层像素网格,扫描线算法 先读多边形的 3 个点,找最大和最小的 y 值,只在这两点间工作,然后算法从上往下,一次处理一行,计算每一行和多边形相交的 2 个点。因为是三角形,如果相交一条边,必然相交另一条,扫描线算法 会填满 2 个相交点之间的像素。\n来看个具体例子。第一行 相交于这里和这里,算法把两点间填满颜色,然后下一行,再下一行,所以叫 扫描线渲染,扫到底部就完成了。填充的速度叫 fillrate(填充速率)。当然 这样的三角形比较丑,边缘满是锯齿,当像素较小时 就不那么明显,但尽管如此,你肯定在游戏里见过这种效果,特别是低配电脑。一种减轻锯齿的方法叫 抗锯齿 (antialiasing),与其每个像素都涂成一样的颜色,可以判断多边形切过像素的程度,来调整颜色,如果像素在多边形内部,就直接涂颜色,如果多边形划过像素,颜色就浅一些,这种边缘羽化的效果看着更舒服些。抗锯齿 被广泛使用,比如字体和图标,如果你把脸贴近屏幕,近点, 再近点,你能看到浏览器里字体是抗锯齿的,超平滑。\n在 3d 场景中,多边形到处都是,但只有一部分能看见,因为其它的被挡住了,这叫 遮挡 。最直接的处理办法是用排序算法,从远到近排列,然后从远到近渲染,这叫 画家算法 。因为画家也是先画背景,然后再画更近的东西。\n看这个例子,有 3 个重叠的多边形。为了简单,我们画成不同颜色,同时,假设 3 个多边形都和屏幕平行,但在实际应用中,比如游戏里,多边形可能是倾斜的,3 个多边形 a,b,c,距离 20,12,14 。画家算法的第一件事,是从远到近排序,现在有序了,我们可以用 扫描线算法 填充多边形,一次填一个。我们从最远的 a 开始,然后重复这个过程,填充第二远的 c ,然后是 b 。现在完成了,可以看到顺序是对的,近的多边形在前面!\n还有一种方法叫 深度缓冲 ,它和之前的算法做的事情一样,但方法不同。我们回到之前的例子,回到排序前的状态。因为这个算法不用排序,所以速度更快。简而言之,z-buffering 算法会记录场景中每个像素和摄像机的距离,在内存里存一个数字矩阵。首先,每个像素的距离被初始化为\u0026quot;无限大\u0026quot;,然后 z-buffering 从列表里第一个多边形开始处理,也就是 a ,它和扫描线算法逻辑相同,但不是给像素填充颜色,而是把多边形的距离 和 z-buffer 里的距离进行对比,它总是记录更低的值 ,a 距离 20,20 小于\u0026quot;无限大\u0026quot;,所以缓冲区记录 20 ,算完 a 之后算下一个,以此类推 。因为没对多边形排序,所以后处理的多边形并不总会覆盖前面的,对于多边形 c ,缓冲区里只有一部分值会被多边形 c 的距离值覆盖。z 缓冲区完成后,会和\u0026quot;扫描线\u0026quot;算法的改进高级版配合使用,不仅可以勘测到线的交叉点,还可以知道某像素是否在最终场景中可见。如果不可见,扫描线算法会跳过那个部分,当两个多边形距离相同时,会出现一个有趣问题,比如多边形 a 和 b 距离都是 20, 哪个画上面?多边形会在内存中移来移去,访问顺序会不断变化。另外,计算浮点数有舍入误差,所以哪一个画在上面,往往是不可预测的,导致出现 z-fighting 效果 如果你玩过 3d 游戏,肯定见过。\n说起 故障,3d 游戏中有个优化叫 背面剔除 。你想想,三角形有两面,正面和背面,游戏角色的头部或地面,只能看到朝外的一面,所以为了节省处理时间,会忽略多边形背面,减了一半多边形面数。这很好,但有个 bug 是 如果进入模型内部往外看,头部和地面会消失。\n继续,我们讲灯光,也叫 明暗处理 ,因为 3d 场景中,物体表面应该有明暗变化。我们回到之前的茶壶网格,用\u0026quot;扫描线\u0026quot;算法渲染所有多边形后,茶壶看起来像这样,没什么 3d 感。我们来加点灯光,提高真实感。为了举例,我们从茶壶上挑 3 个不同位置的多边形,和之前的例子不同,这次要考虑这些多边形面对的方向,它们不平行于屏幕,而是面对不同方向,他们面对的方向叫 \u0026ldquo;表面法线\u0026rdquo; 。我们可以用一个垂直于表面的小箭头来显示这个方向,现在加个光源,每个多边形被照亮的程度不同,有的更亮,因为面对的角度导致更多光线反射到观察者。举个例子,底部的多边形向下倾斜,远离光源,所以更暗一些。类似的,最右的多边形更背对光源,所以只有部分照亮。最后是左上角的多边形,因为它面对的角度 意味着会把光线反射到我们这里,所以会显得更亮。如果对每个多边形执行同样的步骤,看上去会更真实!这叫 平面着色 ,是最基本的照明算法。不幸的是,这使多边形的边界非常明显,看起来不光滑,因此开发了更多算法,比如 高洛德着色 和 冯氏着色,不只用一种颜色给整个多边形上色,而是以巧妙的方式改变颜色得到更好的效果。\n我们还要说下 \u0026ldquo;纹理\u0026rdquo; ,纹理在图形学中指外观,而不是手感,就像照明算法一样,纹理也有多种算法,来做各种花哨效果。最简单的是 纹理映射 ,为了理解纹理映射,回到单个多边形,用\u0026quot;扫描线算法\u0026quot;填充时,可以看看内存内的纹理图像,决定像素用什么颜色。为了做到这点,需要把多边形坐标和纹理坐标对应起来,我们来看看\u0026quot;扫描线算法\u0026quot;要填充的第一个像素,纹理算法会查询纹理,从相应区域取平均颜色,并填充多边形,重复这个过程,就可以获得纹理。\n如果结合这集提到的所有技巧 会得到一个精美的小茶壶。这个茶壶可以放进更大的场景里,场景由上百万个多边形组成。渲染这样的场景需要大量计算,但重要的是,再大的场景,过程都是一样的,一遍又一遍,处理所有多边形,扫描线填充,抗锯齿,光照,纹理化,然而,有几种方法可以加速渲染:\n首先,我们可以为这种特定运算做专门的硬件来加快速度,让运算快如闪电 其次,我们可以把 3d 场景分解成多个小部分,然后并行渲染,而不是按顺序渲染。 cpu 不是为此设计的,因此图形运算不快,所以,计算机工程师为图形做了专门的处理器,叫 gpu \u0026ldquo;图形处理单元\u0026rdquo; 。gpu 在显卡上,周围有专用的 ram ,所有网格和纹理都在里面,让 gpu 的多个核心可以高速访问。现代显卡,如 geforce gtx 1080 ti 有 3584 个处理核心,提供大规模并行处理,每秒处理上亿个多边形!\n好了,本集对 3d 图形的介绍到此结束。下周我们聊全新的主题。\n我到时会 ping 你~\n28. 计算机网络 i.e. computer networks\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n互联网太棒啦,键盘敲几下就能在 youtube 直播\u0026ndash;哈喽!在维基百科上阅读文章,在亚马逊买东西,和朋友视频发一条天气推特。毫无疑问,用户在全球网络中发送和接收信息的能力,永远改变了这个世界。150 年前 发一封信件从伦敦到加州 要花 2~3 周,而且还是特快邮件,如今,电子邮件只要几分之一秒。\u0026ldquo;时延\u0026quot;改善了上百万倍 (时延指传播一条信息所需的时间),振兴了全球经济,帮助现代世界在遍布全球的光纤中快速发展。\n你可能觉得计算机和网络密切相关,但事实上,1970 年以前 大多数计算机是独立运行的,然而 ,因为大型计算机开始随处可见,廉价机器开始出现在书桌上,分享数据和资源渐渐变得有用起来,首个计算机网络出现了。\n今天起,我们花 3 集视频讲网络是如何发展成现在的样子,以及支撑它们的基础原理和技术。\n第一个计算机网络出现在 1950~1960 年代,通常在公司或研究室内部使用,为了方便信息交换,比把纸卡或磁带送到另一栋楼里更快速可靠,这叫 \u0026ldquo;球鞋网络\u0026rdquo; 。\n第二个好处是能共享物理资源。举个例子,与其每台电脑配一台打印机,大家可以共享一台联网的打印机。早期网络也会共享存储空间,因为每台电脑都配存储器太贵了。\n计算机近距离构成的小型网络叫局域网,简称 lan(local area networks)。局域网能小到是同一个房间里的两台机器,或大到校园里的上千台机器。尽管开发和部署了很多不同 lan 技术,其中最著名和成功的是 \u0026ldquo;以太网\u0026rdquo;, 开发于 1970 年代 ,在施乐的\u0026quot;帕洛阿尔托研究中心\u0026quot;诞生,今日仍被广泛使用。\n以太网的最简单形式是:一条以太网电线连接数台计算机,当一台计算机要传数据给另一台计算机时,它以电信号形式,将数据传入电缆,当然 因为电缆是共享的,连在同一个网络里的其他计算机也看得到数据,但不知道数据是给它们的,还是给其他计算机的。为了解决这个问题,以太网需要每台计算机有唯一的媒体访问控制地址,简称 mac 地址。这个唯一的地址放在头部,作为数据的前缀发送到网络中,所以,计算机只需要监听以太网电缆,只有看到自己的 mac 地址,才处理数据。这运作得很好,现在制造的每台计算机都自带唯一的 mac 地址,用于以太网和无线网络。\n多台电脑共享一个传输媒介,这种方法叫 \u0026ldquo;载波侦听多路访问\u0026rdquo; ,简称\u0026quot;csma\u0026rdquo; 。载体 (carrier) 指运输数据的共享媒介,以太网的\u0026quot;载体\u0026quot;是铜线,wifi 的\u0026quot;载体\u0026quot;是传播无线电波的空气。很多计算机同时侦听载体,所以叫\u0026quot;侦听\u0026quot;和\u0026quot;多路访问\u0026quot;,而载体传输数据的速度 叫 \u0026ldquo;带宽\u0026rdquo; 。不幸的是,使用共享载体有个很大的弊端 - 当网络流量较小时 计算机可以等待载体清空,然后传送数据,但随着网络流量上升,两台计算机想同时写入数据的概率也会上升,这叫冲突,数据全都乱套了、就像两个人同时在电话里讲话,幸运的是,计算机能够通过监听电线中的信号检测这些冲突,最明显的解决办法是停止传输,等待网络空闲,然后再试一遍。问题是 其他计算机也打算这样做,其他等着的计算机可能在任何停顿间隙闯入,导致越来越多冲突。很快,每个人都一个接一个地讲话,而且有一堆事要说,就像在家庭聚餐中和男朋友分手一样,馊主意!\n以太网有个超简单有效的解决方法,当计算机检测到冲突,就会在重传之前等待一小段时间,因为要举例,假设是 1 秒好了,当然 如果所有计算机用同样的等待时间 是不行的,它们会在一秒后再次冲突,所以加入一个随机时间 一台计算机可能等 1.3 秒,另一台计算机等待 1.5 秒,要是运气好 等 1.3 秒的计算机会醒来,发现载体是空闲的 然后开始传输,当 1.5 秒的计算机醒来后,会发现载体被占用,会等待其他计算机完成,这有用,但不能完全解决问题,所以要用另一个小技巧。 正如我刚才说的,如果一台计算机在传输数据期间检测到冲突,会等一秒+随机时间,然而 ,如果再次发生冲突 表明有网络拥塞,这次不等 1 秒,而是等 2 秒,如果再次发生冲突 等 4 秒 然后 8 秒 16 秒等等,直到成功传输。因为计算机的退避,冲突次数降低了,数据再次开始流动起来,网络变得顺畅,家庭晚餐有救啦! 这种指数级增长等待时间的方法叫: 指数退避。以太网和 wifi 都用这种方法,很多其他传输协议也用。但即便有了\u0026quot;指数退避\u0026quot;这种技巧,想用一根网线链接整个大学的计算机还是不可能的,为了减少冲突+提升效率,我们需要减少同一载体中设备的数量。载体和其中的设备总称 \u0026ldquo;冲突域\u0026rdquo; 。\n让我们回到之前以太网的例子,一根电缆连 6 台计算机,也叫一个冲突域。为了减少冲突,我们可以用交换机把它拆成两个冲突域,交换机位于两个更小的网络之间,必要时才在两个网络间传数据。交换机会记录一个列表,写着哪个 mac 地址在哪边网络。如果 a 想传数据给 c ,交换机不会把数据转发给另一边的网络,没必要。如果 e 想同一时间传数据给 f,网络仍然是空的,两个传输可以同时发生,但如果 f 想发数据给 a 数据会通过交换机,两个网络都会被短暂占用。\n大的计算机网络也是这样构建的,包括最大的网络 - 互联网,也是多个连在一起的稍小一点网络,使不同网络间可以传递信息。这些大型网络有趣之处是,从一个地点到另一个地点通常有多条路线,这就带出了另一个话题 路由 。\n连接两台相隔遥远的计算机或网路,最简单的办法 是分配一条专用的通信线路,早期电话系统就是这样运作的。假设\u0026quot;印第安纳波利斯\u0026quot;和\u0026quot;米苏拉\u0026quot;之间,有五条电话线,如果在 1910 年代,john 想打电话给 hank,john 要告诉操作员他想打到什么地方,然后工作人员手动将 john 的电话连到 通往米苏拉的未使用线路。通话期间,这条线就被占用了,如果五条线都被占用了 john 要等待某条线空出来,这叫 \u0026ldquo;电路交换\u0026rdquo; ,因为是把电路连接到正确目的地。能用倒是能用 ,但不灵活而且价格昂贵 ,因为总有闲置的线路。好处是 如果有一条专属于自己的线路 你可以最大限度地随意使用,无需共享。因此军队,银行和其他一些机构,依然会购买专用线路来连接数据中心。\n传输数据的另一个方法是 \u0026ldquo;报文交换\u0026rdquo; ,\u0026ldquo;报文交换\u0026rdquo; 就像邮政系统一样,不像之前 a 和 b 有一条专有线路,消息会经过好几个站点。 如果 john 写一封信给 hank,信件可能从\u0026quot;印第安纳波利斯\u0026quot;到\u0026quot;芝加哥\u0026quot;,然后\u0026quot;明尼阿波利斯\u0026quot; ,然后\u0026quot;比林斯\u0026quot; 最后到\u0026quot;米苏拉\u0026quot;。每个站点都知道下一站发哪里 ,因为站点有表格,记录到各个目的地,信件该怎么传。报文交换的好处是 可以用不同路由 ,使通信更可靠更能容错。\n回到邮件的例子,如果\u0026quot;明尼阿波利斯\u0026quot;有暴风雪中断了通信 \u0026ldquo;芝加哥\u0026quot;可以传给\u0026quot;奥马哈\u0026rdquo;,在这个例子里,城市就像路由器一样,消息沿着路由跳转的次数 叫 \u0026ldquo;跳数\u0026rdquo; (hop count)。记录跳数很有用,因为可以分辨出路由问题。举例,假设芝加哥认为 去米苏拉的最快路线是 奥马哈,但奥马哈认为 去米苏拉的最快路线是 芝加哥,这就糟糕了,因为 2 个城市看到目的地是米苏拉,结果报文会在 2 个城市之间 不停传来传去,不仅浪费带宽 ,而且这个路由错误需要修复! 这种错误会被检测到,因为跳数记录在消息中 ,而且传输时会更新跳数。如果看到某条消息的跳数很高 ,就知道路由肯定哪里错了,这叫 \u0026ldquo;跳数限制\u0026rdquo; 。\n报文交换的缺点之一是有时候报文比较大,会堵塞网络 ,因为要把整个报文从一站传到下一站后 才能继续传递其他报文。传输一个大文件时,整条路都阻塞了,即便你只有一个 1kb 的电子邮件要传输 ,也只能等大文件传完,或是选另一条效率稍低的路线,这就糟了。\n解决方法是 将大报文分成很多小块,叫 \u0026ldquo;数据包\u0026rdquo; ,就像报文交换 ,每个数据包都有目标地址 ,因此路由器知道发到哪里。报文具体格式由\u0026quot;互联网协议\u0026quot;定义,简称 ip 。这个标准创建于 1970 年代,每台联网的计算机都需要一个 ip 地址。你可能见过,以点分隔的 4 组数字,例如 172.217.7.238 是 google 其中一个服务器的 ip 地址。数百万台计算机在网络上不断交换数据 ,瓶颈的出现和消失是毫秒级的,路由器会平衡与其他路由器之间的负载, 以确保传输可以快速可靠,这叫 \u0026ldquo;阻塞控制\u0026rdquo; 。\n有时,同一个报文的多个数据包 会经过不同线路,到达顺序可能会不一样,这对一些软件是个问题。幸运的是,在 ip 之上还有其他协议,比如 tcp/ip, 可以解决乱序问题。我们下周会讲。\n将数据拆分成多个小数据包,然后通过灵活的路由传递,非常高效且可容错,如今互联网就是这么运行的,这叫 \u0026ldquo;分组交换\u0026rdquo; 。有个好处是 它是去中心化的,没有中心权威机构,没有单点失败问题。事实上 ,因为冷战期间有核攻击的威胁,所以创造了分组交换。如今,全球的路由器协同工作,找出最高效的线路,用各种标准协议运输数据,比如 \u0026ldquo;因特网控制消息协议\u0026rdquo;(icmp) 和 \u0026ldquo;边界网关协议\u0026rdquo;(bgp)。世界上第一个分组交换网络以及现代互联网的祖先是 arpanet(advanced research projects agency),名字来源于赞助这个项目的机构,美国高级研究计划局。\n这是 1974 年整个 arpanet 的样子,每个小圆表示一个地点, 比如大学或实验室,那里运行着一个路由器,并且有一台或多台计算机,能看到 \u0026ldquo;pdp-1\u0026rdquo; 和\u0026quot;ibm 360 系统\u0026quot;,甚至还有一个伦敦的 atlas 是通过卫星连到网络里的。显然 ,互联网在这几十年间发展迅速,如今不再只有几十台计算机联网 据估计 有接近 100 亿台联网设备,而且互联网会继续快速发展,特别是如今各种智能设备层出不穷 ,比如联网冰箱,恒温器,以及其他智能家电,它们组成了\u0026quot;物联网\u0026quot;。\n第一部分到此结束 我们对计算机网络进行了概览。\n网络是一堆管子组成的吗?额 算是吧。下周我们会讨论一些高级传输协议,然后讲万维网。\n到时见啦。\n29. 互联网 i.e. the internet\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n上集讲到,你的计算机和一个巨大的分布式网络连在一起,这个网络叫互联网。\n你现在就在网上看视频呀。互联网由无数互联设备组成,而且日益增多。计算机为了获取这个视频 ,首先要连到局域网,也叫 lan ,你家 wifi 路由器连着的所有设备,组成了局域网。局域网再连到广域网,广域网也叫 wan (wide area network),wan 的路由器一般属于你的\u0026quot;互联网服务提供商\u0026quot;,简称 isp(internet service provider),比如 comcast,at\u0026amp;t 和 verizon 这样的公司。\n广域网里,先连到一个区域性路由器,这路由器可能覆盖一个街区。然后连到一个更大的 wan,可能覆盖整个城市。可能再跳几次,但最终会到达互联网主干。互联网主干由一群超大型、带宽超高路由器组成,为了从 youtube 获得这个视频,数据包(packet)要先到互联网主干,沿着主干到达有对应视频文件的 youtube 服务器,数据包从你的计算机跳到 youtube 服务器,可能要跳个 10 次,先跳 4 次到互联网主干,2 次穿过主干,主干出来可能再跳 4 次,然后到 youtube 服务器。如果你在用 windows, mac os 或 linux 系统,可以用 traceroute 来看跳了几次,更多详情看视频描述(youtube 原视频下)。\n我们在\u0026quot;印第安纳波利斯\u0026quot;的 chad\u0026amp;stacy emigholz 工作室,访问加州的 dftba 服务器,经历了 11 次中转。从 192.168.0.1 出发,这是我的电脑在 局域网(lan)里的 ip 地址,然后到工作室的 wifi 路由器,然后穿过一个个地区路由器,到达主干。然后从主干出来,又跳了几次,到达\u0026quot;dftba.com”的服务器,ip 地址是 104.24.109.186 。\n但数据包到底是怎么过去的 ?如果传输时数据包被弄丢了,会发生什么?如果在浏览器里输 \u0026ldquo;dftba.com\u0026rdquo;,浏览器怎么知道服务器的地址多少?\n我们今天会讨论这些话题。\n上集说过,互联网是一个巨型分布式网络 ,会把数据拆成一个个数据包来传输。如果要发的数据很大,比如邮件附件, 数据会被拆成多个小数据包。举例,你现在看的这个视频 ,就是一个个到达你电脑的数据包,而不是一整个大文件发过来。数据包(packet)想在互联网上传输 ,要符合\u0026quot;互联网协议\u0026quot;的标准,简称 ip 。就像邮寄手写信一样,邮寄是有标准的每封信需要一个地址,而且地址必须是独特的,并且大小和重量是有限制的,违反这些规定,信件就无法送达。\nip 数据包也是如此,因为 ip 是一个非常底层的协议,数据包的头部(或者说前面)只有目标地址,头部存 \u0026ldquo;关于数据的数据\u0026rdquo; 也叫 元数据 (metadata),这意味着当数据包到达对方电脑 ,对方不知道把包交给哪个程序,是交给 skype 还是使命召唤?因此需要在 ip 之上,开发更高级的协议。\n这些协议里 最简单最常见的叫\u0026quot;用户数据报协议\u0026quot;,简称 udp 。udp 也有头部,这个头部位于数据前面,头部里包含有用的信息。信息之一是端口号,每个想访问网络的程序 ,都要向操作系统申请一个端口号,比如 skype 会申请端口 3478 。当一个数据包到达时 ,接收方的操作系统会读 udp 头部,读里面的端口号,如果看到端口号是 3478,就把数据包交给 skype。\n总结:ip 负责把数据包送到正确的计算机, udp 负责把数据包送到正确的程序。\nudp 头部里还有\u0026quot;校验和\u0026quot;,用于检查数据是否正确,正如\u0026quot;校验和\u0026quot;这个名字所暗示的, 检查方式是把数据求和来对比。\n以下是个简单例子。假设 udp 数据包里 原始数据是 89 111 33 32 58 41 ,在发送数据包前 ,电脑会把所有数据加在一起,算出\u0026quot;校验和\u0026quot; - 89+111+33+\u0026hellip; 以此类推,得到 364,这就是\u0026quot;校验和\u0026quot;。 udp 中,\u0026ldquo;校验和\u0026rdquo; 以 16 位形式存储 (就是 16 个 0 或 1),如果算出来的和,超过了 16 位能表示的最大值, 高位数会被扔掉,保留低位。当接收方电脑收到这个数据包,它会重复这个步骤 把所有数据加在一起,89+111+33\u0026hellip; 以此类推,如果结果和头部中的校验和一致 ,代表一切正常。如果不一致,数据肯定坏掉了。也许传输时碰到了功率波动,或电缆出故障了。\n不幸的是,udp 不提供数据修复或数据重发的机制,接收方知道数据损坏后,一般只是扔掉。而且,udp 无法得知数据包是否到达。发送方发了之后,无法知道数据包是否到达目的地,这些特性听起来很糟糕,但是有些程序不在意这些问题,因为 udp 又简单又快。\n拿 skype 举例 ,它用 udp 来做视频通话,能处理坏数据或缺失数据,所以网速慢的时候 skype 卡卡的 因为只有一部分数据包到了你的电脑。但对于其他一些数据,这个方法不适用。 比如发邮件,邮件不能只有开头和结尾 ,没有中间,邮件要完整到达收件方!\n如果\u0026quot;所有数据必须到达\u0026quot; ,就用\u0026quot;传输控制协议\u0026quot;,简称 tcp(transmission control protocol)。tcp 和 udp 一样,头部也在存数据前面,因此,人们叫这个组合 tcp/ip 。就像 udp ,tcp 头部也有\u0026quot;端口号\u0026quot;和\u0026quot;校验和\u0026quot;,但 tcp 有更高级的功能,我们这里只介绍重要的几个。\n1、 tcp 数据包有序号\n15 号之后是 16 号,16 号之后是 17 号,以此类推 发上百万个数据包也是有可能的。序号使接收方可以把数据包排成正确顺序,即使到达时间不同。哪怕到达顺序是乱的,tcp 协议也能把顺序排对。\n2、 tcp 要求接收方的电脑收到数据包 并且\u0026quot;校验和\u0026quot;检查无误后(数据没有损坏)给发送方发一个确认码,代表收到了\n\u0026ldquo;确认码\u0026rdquo; 简称 ack . 得知上一个数据包成功抵达后,发送方会发下一个数据包。假设这次发出去之后,没收到确认码 ,那么肯定哪里错了。如果过了一定时间还没收到确认码, 发送方会再发一次。注意 ,数据包可能的确到了,只是确认码延误了很久,或传输中丢失了,但这不碍事 ,因为收件方有序列号,如果收到重复的数据包就删掉。\n还有,tcp 不是只能一个包一个包发,可以同时发多个数据包,收多个确认码 ,这大大增加了效率,不用浪费时间等确认码。有趣的是,确认码的成功率和来回时间 可以推测网络的拥堵程度,tcp 用这个信息,调整同时发包数量,解决拥堵问题。\n简单说,tcp 可以处理乱序和丢失数据包,丢了就重发,还可以根据拥挤情况自动调整传输率。相当厉害!\n你可能会奇怪,既然 tcp 那么厉害,还有人用 udp 吗?tcp 最大的缺点是 ,那些\u0026quot;确认码\u0026quot;数据包把数量翻了一倍,但并没有传输更多信息,有时候这种代价是不值得的 ,特别是对时间要求很高的程序,比如在线射击游戏。如果你玩游戏很卡,你也会觉得这样不值!\n当计算机访问一个网站时 需要两个东西:1.ip 地址, 2. 端口号 。\n例如 172.217.7.238 的 80 端口 ,这是谷歌的 ip 地址和端口号。事实上,你可以输到浏览器里,然后你会进入谷歌首页。有了这两个东西就能访问正确的网站, 但记一长串数字很讨厌,google.com 比一长串数字好记,所以互联网有个特殊服务 ,负责把域名和 ip 地址一一对应,就像专为互联网的电话簿 它叫 \u0026ldquo;域名系统\u0026rdquo; ,简称 dns 。\n它的运作原理你可能猜到了,在浏览器里输 youtube.com ,浏览器会去问 dns 服务器,它的 ip 地址是多少。一般 dns 服务器 是互联网供应商提供的,dns 会查表,如果域名存在,就返回对应 ip 地址。如果你乱敲键盘加个。com, 然后按回车,你很可能会看到 dns 错误,因为那个网站不存在,所以 dns 无法返回给你一个地址。如果你输的是有效地址,比如 youtube.com ,dns 按理会返回一个地址,然后浏览器会给这个 ip 地址 发 tcp 请求。如今有三千万个注册域名,所以为了更好管理,dns 不是存成一个超长超长的列表,而是存成树状结构。顶级域名(简称 tld)在最顶部,比如 .com 和 .gov ,下一层是二级域名,比如 .com 下面有 google.com 和 dftba.com ,再下一层叫子域名,比如 images.google.com, store.dftba.com ,这个树超!级!大!\n我前面说的\u0026quot;三千万个域名\u0026quot;只是二级域名 ,不是所有子域名,因此,这些数据散布在很多 dns 服务器上,不同服务器负责树的不同部分。\n好了 我知道你肯定在等这个梗:我们到了一层新抽象!\n过去两集里 我们讲了线路里的电信号,以及无线网络里的无线信号,这些叫\u0026quot;物理层\u0026quot;,而\u0026quot;数据链路层\u0026quot; 负责操控 \u0026ldquo;物理层\u0026rdquo;,数据链路层有:媒体访问控制地址(mac),碰撞检测,指数退避,以及其他一些底层协议。再上一层是\u0026quot;网络层\u0026quot;,负责各种报文交换和路由。而今天,我们讲了\u0026quot;传输层\u0026quot;里一大部分, 比如 udp 和 tcp 这些协议,负责在计算机之间进行点到点的传输,而且还会检测和修复错误。我们还讲了一点点\u0026quot;会话层\u0026quot;,\u0026ldquo;会话层\u0026rdquo; 会使用 tcp 和 udp 来创建连接,传递信息,然后关掉连接,这一整套叫\u0026quot;会话\u0026quot;。查询 dns 或看网页时,就会发生这一套流程。这是 开放式系统互联通信参考模型 (osi,open system interconnection) 的底下 5 层,这个概念性框架 把网络通信划分成多层,每一层处理各自的问题。如果不分层 直接从上到下捏在一起实现网络通信,是完全不可能的!\n抽象使得科学家和工程师能分工同时改进多个层 不被整体复杂度难倒。而且惊人的是!我们还没讲完呢!\nosi 模型还有两层,\u0026ldquo;表示层\u0026quot;和\u0026quot;应用程序层\u0026rdquo;,其中有浏览器,skype,html 解码,在线看电影等。\n我们下周说,到时见。\n30. 万维网 i.e. the world wide web\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n前两集我们深入讨论了电线 信号 交换机 数据包 路由器以及协议,它们共同组成了互联网。今天我们向上再抽象一层,来讨论万维网。\n万维网 (world wide web) 和互联网 (internet) 不是一回事,尽管人们经常混用这两个词。万维网在互联网之上运行,互联网之上还有 skype, minecraft 和 instagram 。互联网是传递数据的管道,各种程序都会用,其中传输最多数据的程序是万维网,分布在全球数百万个服务器上,可以用\u0026quot;浏览器\u0026quot;来访问万维网,这集我们会深入讲解万维网。\n万维网的最基本单位,是单个页面。页面有内容,也有去往其他页面的链接 ,这些链接叫\u0026quot;超链接\u0026quot;。你们都见过:可以点击的文字或图片,把你送往另一个页面,这些超链接形成巨大的互联网络,这就是\u0026quot;万维网\u0026quot;名字的由来。现在说起来觉得很简单,但在超链接做出来之前,计算机上每次想看另一个信息时,你需要在文件系统中找到它 ,或是把地址输入搜索框,有了超链接,你可以在相关主题间轻松切换。超链接的价值早在 1945 年 就被 vannevar bush 意识到了,在第 24 集中我们说过,他发过一篇文章 ,描述一个假想的机器 memex ,bush 的形容是\u0026quot;关联式索引 - 选一个物品会引起另一个物品被立即选中\u0026quot;。他解释道:\u0026ldquo;将两样东西联系在一起的过程十分重要,在任何时候,当其中一件东西进入视线,只需点一下按钮,立马就能回忆起另一件\u0026rdquo; 。1945 年的时候计算机连显示屏都没有,所以这个想法非常超前!因为文字超链接是如此强大,它得到了一个同样厉害的名字:\u0026ldquo;超文本\u0026rdquo;!如今超文本最常指向的,是另一个网页,然后网页由浏览器渲染,我们待会会讲。\n为了使网页能相互连接,每个网页需要一个唯一的地址,这个地址叫 \u0026ldquo;统一资源定位器\u0026rdquo;,简称 url(uniform resource locator)。一个网页 url 的例子是 \u0026ldquo;thecrashcourse.com/courses\u0026rdquo; ,就像上集讨论的,当你访问一个网站时,计算机首先会做\u0026quot;dns 查找\u0026quot;,\u0026ldquo;dns 查找\u0026quot;的输入是一个域名 比如 thecrashcourse.com ,会输出对应的 ip 地址,现在有了 ip 地址 ,你的浏览器会打开一个 tcp 连接到这个 ip 地址,这个地址运行着\u0026quot;网页服务器\u0026rdquo;,网页服务器的标准端口是 80 端口,这时,你的计算机连到了 thecrashcourse.com 的服务器。下一步是向服务器请求\u0026quot;courses\u0026quot;这个页面,这里会用\u0026quot;超文本传输协议\u0026quot;(http)。\nhttp 的第一个标准,http 0.9,创建于 1991 年,只有一个指令,\u0026ldquo;get\u0026rdquo; 指令。幸运的是,对当时来说也够用,因为我们想要的是\u0026quot;courses\u0026quot;页面,我们向服务器发送指令:\u0026ldquo;get /courses\u0026rdquo;,该指令以\u0026quot;ascii 编码\u0026quot;发送到服务器,服务器会返回该地址对应的网页 ,然后浏览器会渲染到屏幕上。如果用户点了另一个链接,计算机会重新发一个 get 请求。你浏览网站时,这个步骤会不断重复。\n在之后的版本,http 添加了状态码,状态码放在请求前面。举例,状态码 200 代表 \u0026ldquo;网页找到了,给你\u0026rdquo;。状态码 400~499 代表客户端出错,比如网页不存在,就是可怕的 404 错误。\n\u0026ldquo;超文本\u0026quot;的存储和发送都是以普通文本形式。举个例子,编码可能是 ascii 或 utf-16 , 我们在第 4 集和第 20 集讨论过,因为如果只有纯文本 ,无法表明什么是链接,什么不是链接,所以有必要开发一种标记方法,因此开发了 超文本标记语言(html)。\nhtml 第一版的版本号是 0.8,创建于 1990 年,有 18 种 html 指令,仅此而已!\n我们来做一个网页吧!\n\u0026gt; 首先,给网页一个大标题\r\u0026gt; 我们输 h1 代表一级标题,然后用\u0026lt;\u0026gt;括起来\r\u0026gt; 这就是一个 html 标签\r\u0026gt; \u0026gt; 然后输入想要的标题\r\u0026gt; 我们不想一整页都是标题 所以加 \u0026lt;/h1\u0026gt; 作为结束标签\r\u0026gt; \u0026gt; 现在来加点内容\r\u0026gt; 读者可能不知道\u0026#34;克林贡\u0026#34;是什么,所以我们给这个词\r\u0026gt; 加一个超链接到\u0026#34;克林贡语言研究院\u0026#34;\r\u0026gt; 我们用 \u0026lt;a\u0026gt; 标签来做,它有一个 href 属性\r\u0026gt; 说明链接指向哪里,当点击链接时就会进入那个网页\r\u0026gt; 最后用 \u0026lt;/a\u0026gt; 关闭标签\r\u0026gt; \u0026gt; 接下来用 \u0026lt;h2\u0026gt; 标签做二级标题\r\u0026gt; \u0026gt; html 也有做列表的标签\r\u0026gt; 我们先写\u0026lt;ol\u0026gt; 代表 有序列表(ordered list)\r\u0026gt; 然后想加几个列表项目 , 就加几个 用 \u0026lt;li\u0026gt; 包起来就行\r\u0026gt; 读者可能不知道 bat\u0026#39;leth 是什么,那么也加上超链接\r\u0026gt; 最后,为了保持良好格式,用\u0026lt;/ol\u0026gt;代表列表结束\r\u0026gt; \u0026gt; 这就完成了 - 一个很简单的网页! 如果把这些文字存入记事本或文本编辑器,然后文件取名 \u0026ldquo;test.html\u0026rdquo;,就可以拖入浏览器打开。\n当然,如今的网页更复杂一些,最新版的 html,html5,有 100 多种标签,图片标签,表格标签,表单标签,按钮标签,等等。还有其他相关技术就不说了比如 层叠样式表 (css) 和 javascript,这俩可以加进网页,做一些更厉害的事。\n让我们回到浏览器,网页浏览器可以和网页服务器沟通,浏览器不仅获取网页和媒体,获取后还负责显示。\n第一个浏览器和服务器,是 tim berners-lee 在 1990 年写的,一共花了 2 个月。那时候他在瑞士的\u0026quot;欧洲核子研究所\u0026quot;工作,为了做出来,他同时建立了几个最基本的网络标准 - url, html 和 http。两个月能做这些很不错啊!不过公平点说,他研究超文本系统已经有十几年了,和同事在 cern 内部使用一阵子后,在 1991 年发布了出去,万维网就此诞生。\n重要的是,万维网有开放标准,大家都可以开发新服务器和新浏览器,因此\u0026quot;伊利诺伊大学香槟分校\u0026quot;的一个小组在 1993 年做了 mosaic 浏览器,第一个可以在文字旁边显示图片的浏览器,之前浏览器要单开一个新窗口显示图片,还引进了书签等新功能,界面友好,使它很受欢迎。尽管看上去硬邦邦的,但和如今的浏览器长的差不多。\n1990 年代末有许多浏览器面世,netscape navigator, internet explorer opera, omniweb, mozilla ,也有很多服务器面世,比如 apache 和 微软互联网信息服务 (iis)。每天都有新网站冒出来,如今的网络巨头,比如亚马逊和 ebay,创始于 1990 年代中期,那是个黄金时代!\n随着万维网日益繁荣,人们越来越需要搜索。如果你知道网站地址, 比如 ebay.com,直接输入浏览器就行,如果不知道地址呢?比如想找可爱猫咪的图片,现在就要!去哪里找呢?\n起初人们会维护一个目录,链接到其他网站,其中最有名的叫\u0026quot;jerry 和 david 的万维网指南\u0026rdquo;,1994 年改名为 yahoo 。随着网络越来越大,人工编辑的目录变得不便利,所以开发了搜索引擎。\n让我们进入思想泡泡!\n长的最像现代搜索引擎的最早搜素引擎,叫 jumpstation ,由 jonathon fletcher 于 1993 年在斯特林大学创建,它有 3 个部分:\n第一个是爬虫,一个跟着链接到处跑的软件,每当看到新链接,就加进自己的列表里;第二个部分是不断扩张的索引,记录访问过的网页上,出现过哪些词;最后一个部分,是查询索引的搜索算法,举个例子,如果我在 jumpstation 输入\u0026quot;猫\u0026quot;,每个有\u0026quot;猫\u0026quot;这个词的网页都会出现。\n早期搜索引擎的排名方式 非常简单,取决于 搜索词在页面上的出现次数。刚开始还行,直到有人开始钻空子,比如在网页上写几百个\u0026quot;猫\u0026quot;,把人们吸引过来。谷歌成名的一个很大原因是, 创造了一个聪明的算法来规避这个问题。与其信任网页上的内容 ,搜索引擎会看其他网站 有没有链接到这个网站。如果只是写满\u0026quot;猫\u0026quot;的垃圾网站,没有网站会指向它,如果有关于猫的有用内容,有网站会指向它,所以这些\u0026quot;反向链接\u0026quot;的数量,特别是有信誉的网站,代表了网站质量。\ngoogle 一开始时是 1996 年斯坦福大学 一个叫 backrub 的研究项目,两年后分离出来,演变成如今的谷歌。\n谢谢思想泡泡!\n最后 我想讲一个词,你最近可能经常听到 - 网络中立性 。现在你对数据包,路由和万维网,有了个大体概念,足够你理解这个争论的核心点,至少从技术角度。简单说\u0026quot;网络中立性\u0026quot;是应该平等对待所有数据包,不论这个数据包是我的邮件,或者是你在看视频,速度和优先级应该是一样的,但很多公司会乐意让它们的数据优先到达。拿 comcast 举例,它们不但是大型互联网服务提供商而且拥有多家电视频道,比如 nbc 和 the weather channel,可以在线看。我不是特意找 comcast 麻烦 ,但要是没有网络中立性,comcast 可以让自己的内容优先到达 ,节流其他线上视频。节流 (throttled) 意思是故意给更少带宽和更低优先级。再次重申,这只是举例,不是说 comcast 很坏。支持网络中立性的人说 没有中立性后,服务商可以推出提速的\u0026quot;高级套餐\u0026quot;,给剥削性商业模式埋下种子。互联网服务供应商成为信息的\u0026quot;守门人\u0026quot;,它们有着强烈的动机去碾压对手,另外,netflix 和 google 这样的大公司可以花钱买特权,而小公司,比如刚成立的创业公司,会处于劣势,阻止了创新。另一方面,从技术原因看,也许你会希望不同数据传输速度不同,你希望 skype 的优先级更高,邮件晚几秒没关系。而反对\u0026quot;网络中立性\u0026quot;的人认为,市场竞争会阻碍不良行为,如果供应商把客户喜欢的网站降速 ,客户会离开供应商。\n这场争辩还会持续很久,就像我们在 crash course 其他系列中说过,你应该自己主动了解更多信息,因为\u0026quot;网络中立性\u0026quot;的影响十分复杂而且广泛。\n我们下周再见\n31. 计算机安全 i.e. cybersecurity\n(。・∀・)ノ゙嗨,我是 carrie anne ,欢迎收看计算机科学速成课!\n过去 3 集 我们讲了计算机如何互连,让我们能瞬时跨全球沟通,但不是每个使用网络的人都会规规矩矩,不损害他人利益。就像现实世界中我们用锁和栅栏保证物理安全,有警察减少犯罪,我们需要网络安全减少虚拟世界中的犯罪 🚨。\n计算机没有道德观念。只要给计算机写清具体问题 , 它们很乐意地闪电般算出答案。破坏医院计算机系统的代码 和 保持病人心跳的代码 ,对计算机来说没有区别,就像\u0026quot;原力\u0026quot;一样 ,计算机可以被拉到\u0026quot;光明面\u0026quot;或\u0026quot;黑暗面\u0026quot;。网络安全就像 绝地武士团 ,给网络世界带来和平与正义。\n计算机安全的范围,和计算能力的发展速度一样快。我们可以把计算机安全,看成是保护系统和数据的:保密性,完整性和可用性 。我们逐个细说:\n\u0026ldquo;保密性\u0026quot;是只有有权限的人 ,才能读取计算机系统和数据。黑客泄露别人的信用卡信息,就是攻击保密性。\n\u0026ldquo;完整性\u0026quot;是只有有权限的人 ,才能使用和修改系统和数据。黑客知道你的邮箱密码,假冒你发邮件,就是攻击\u0026quot;完整性\u0026rdquo;。\n\u0026ldquo;可用性\u0026quot;是有权限的人 ,应该随时可以访问系统和数据。拒绝服务攻击 (ddos) 就是黑客发大量的假请求到服务器,让网站很慢或者挂掉,这就是攻击\u0026quot;可用性\u0026rdquo;。\n为了实现这三个目标,安全专家会从 抽象层面想象\u0026quot;敌人\u0026quot;可能是谁,这叫\u0026quot;威胁模型分析\u0026rdquo;,模型会对攻击者有个大致描述:能力如何,目标可能是什么,可能用什么手段 。攻击手段又叫\u0026quot;攻击矢量\u0026quot; ,\u0026ldquo;威胁模型分析\u0026quot;让你能为特定情境做准备,不被可能的攻击手段数量所淹没 ,因为手段实在有太多种了。假设你想确保笔记本计算机的\u0026quot;物理安全\u0026rdquo; ,你的威胁模型是\u0026quot;好管闲事的室友\u0026quot;。为了保证保密性,完整性和可用性, 你可以藏在脏兮兮的洗衣篮里。但如果威胁模型是调皮的兄弟姐妹,知道你喜欢藏哪里,那么你需要更多保护:比如锁在保险箱里。换句话说,要怎么保护,具体看对抗谁。当然,威胁模型通常比\u0026quot;好管闲事的室友\u0026quot;更正式一些,通常威胁模型分析里 会以能力水平区分,比如\u0026quot;某人可以物理接触到笔记本计算机,而且时间无限\u0026quot;。在给定的威胁模型下,安全架构师要提供解决方案,保持系统安全。只要某些假设不被推翻,比如没人会告诉攻击者密码,保护计算机系统,网络和数据的方法有很多。\n很多安全问题可以总结成 2 个问题: 你是谁?你能访问什么?\n权限应该给合适的人,拒绝错误的人,比如银行员工可以打开取款机来补充现金。但我不应该有权限打开,因为我会把钱拿走 全拿走!陶瓷猫收藏品可不会从天上掉下来哟!所以,为了区分谁是谁,我们用 \u0026ldquo;身份认证\u0026rdquo;(authentication) - 让计算机得知使用者是谁。\n身份认证有三种,各有利弊:你知道什么、你有什么、你是什么。\n\u0026ldquo;你知道什么\u0026rdquo; 是基于某个秘密,只有用户和计算机知道,比如 用户名和密码,这是如今使用最广泛的,因为最容易实现,但如果黑客通过猜测或其他方式,知道你的密码,就惨了。有些密码很容易猜中,比如 12356 或 qwerty 。但有些密码对计算机很容易,比如 pin 码:2580 ,看起来很难猜中 - 起码对人类来说是这样,但 4 位数字,只有一万种可能。一台计算机可以尝试 0000,然后 0001,然后 0002,然后到 9999,不到一秒内试完,这叫\u0026quot;暴力攻击\u0026quot;,因为只是试遍一切可能,这种算法没什么聪明的地方。如果你错误尝试 3 次,有些系统会阻止你继续尝试,或让你等一会儿,这个策略普遍而且合理。对于一般的攻击者确实很难,但假设黑客控制了数以万计的计算机,形成一个僵尸网络,用这么多计算机尝试密码 2580 ,同时尝试很多银行账户,即使每个账户只试一次,也很可能,碰到某个账户刚好用这个 pin,事实上,看视频的某人可能刚好用这个 pin 。增加密码长度有帮助,但即使 8 位数字的 pin 码也很容易破解,这就是为什么现在很多网站 要求大写+小写字母,还有特殊符号等,大大增加可能的密码。8 位数字的 pin 只有一亿种组合,对计算机轻而易举,但包含各种字符的 8 位长度密码,有超过 600 万亿种组合。当然,这些密码会难以记住,所以更好的方法是 选一些更好记的东西,比如三个单词连在一起:\u0026ldquo;格林兄弟好厉害\u0026quot;或\u0026quot;披萨尝起来好好吃\u0026rdquo;。英文大约有 10 万个单词,所以三个单词连一起大概有 1 亿亿种可能,想猜中的话,祝你好运!另外使用不在字典内的单词被猜中的可能性更低,但我们没时间细说这个。computerphile 频道有个视频讲怎么选择好密码 - 链接请看 youtube 描述。\n\u0026ldquo;你有什么\u0026quot;这种验证方式,是基于用户有特定物体,比如钥匙和锁。如果你有钥匙,就能开门,这避免了被人\u0026quot;猜中\u0026quot;的问题,而且通常需要人在现场,所以远程攻击就更难了。另一个国家的人,得先来佛罗里达州,才能到你家前门。但如果攻击者离你比较近,那么也不安全,钥匙可以被复制,手机可能被偷,锁可以撬开。\n最后,\u0026ldquo;你是什么\u0026quot;这种验证,是基于你把特征展示给计算机进行验证,生物识别验证器,比如指纹识别器和虹膜扫描仪就是典型例子,这些非常安全,但最好的识别技术仍然很贵,而且,来自传感器的数据每次会不同。\n\u0026ldquo;你知道什么\u0026quot;和\u0026quot;你有什么\u0026rdquo;。这两种验证是\u0026quot;确定性\u0026quot;的 - 要么正确,要么错误。如果你知道密码,或有钥匙,那么 100%能获得访问权限,如果没有,就绝对进不去,但\u0026quot;生物识别\u0026quot;是概率性的,系统有可能认不出你,可能你戴了帽子,或者光线不好。更糟的是,系统可能把别人错认成你,比如你的邪恶双胞胎。当然,在现实世界中几率很低,但不是零。\n生物认证的另一个问题是无法重设。你只有这么多手指,如果攻击者拿到你的指纹数据怎么办,你一辈子都麻烦了。最近还有研究人员表示,拍个照都有可能伪造虹膜,所以也不靠谱。\n所有认证方法都有优缺点,它们都可以被攻破,所以,对于重要账户,安全专家建议用两种或两种以上的认证方式,这叫\u0026quot;双因素\u0026quot;或\u0026quot;多因素\u0026quot;认证。攻击者可能猜出你密码,或偷走你的手机:但两个都做到,会比较难。\n\u0026ldquo;身份验证\u0026quot;后,就来到了\u0026quot;访问控制\u0026rdquo;。一旦系统知道你是谁,它需要知道你能访问什么,因此应该有个规范,说明谁能访问什么,修改什么,使用什么。这可以通过\u0026quot;权限\u0026quot;或\u0026quot;访问控制列表\u0026rdquo;(acl)来实现,其中描述了用户对每个文件,文件夹和程序的访问权限。\n\u0026#34;读\u0026#34;权限允许用户查看文件内容,\r\u0026#34;写\u0026#34;权限允许用户修改内容,\r\u0026#34;执行\u0026#34;权限允许用户运行文件,比如程序 有些组织需要不同层级的权限,比如间谍机构,\u0026ldquo;访问控制列表\u0026quot;的正确配置非常重要,以确保保密性,完整性和可用性。假设我们有三个访问级别:公开,机密,绝密。\n第一个普遍的好做法是,用户不能\u0026quot;读上\u0026rdquo;, 不能读等级更高的信息,如果用户能读\u0026quot;机密\u0026quot;文件那么不应该有权限读\u0026quot;绝密\u0026quot;文件,但能访问\u0026quot;机密\u0026quot;和\u0026quot;公开\u0026quot;文件\n第二个法则是用户不能\u0026quot;写下\u0026rdquo;,如果用户等级是\u0026quot;绝密\u0026quot;,那么能写入或修改\u0026quot;绝密\u0026quot;文件,但不能修改\u0026quot;机密\u0026quot;或\u0026quot;公共\u0026quot;文件。听起来好像很奇怪 ,有最高等级也不能改等级更低的文件,但这样确保了\u0026quot;绝密\u0026quot; 不会意外泄露到\u0026quot;机密\u0026quot;文件或\u0026quot;公共\u0026quot;文件里。\n这个\u0026quot;不能向上读,不能向下写\u0026quot;的方法叫 bell-lapadula 模型,它是为美国国防部\u0026quot;多层安全政策\u0026quot;制定的,还有许多其他的访问控制模型 - 比如\u0026quot;中国墙\u0026quot;模型和\u0026quot;比伯\u0026quot;模型。哪个模型最好,取决于具体情况。\n身份验证\u0026quot;和\u0026quot;访问控制\u0026quot;帮助计算机知道\u0026quot;你是谁\u0026quot;,以及\u0026quot;你可以访问什么\u0026quot;,但做这些事情的软硬件必须是可信的,这个依赖很重要。如果攻击者给计算机装了恶意软件 - 控制了计算机的操作系统。我们怎么确定安全程序没有给攻击者留后门?短回答是 - 无法确定!我们仍然无法保证程序或计算机系统的安全,因为安全软件在理论上可能是\u0026quot;安全的\u0026quot;,实现时可能会不小心留下漏洞,但我们有办法减少漏洞出现的可能性,比如一找到就马上修复,以及当程序被攻破时尽可能减少损害。大部分漏洞都是具体实现的时候出错了,为了减少执行错误,减少执行。\n系统级安全的圣杯之一是\u0026quot;安全内核\u0026quot;,或\u0026quot;可信计算基础\u0026quot;:一组尽可能少的操作系统软件。安全性都是接近可验证的,构建安全内核的挑战在于 决定内核应该有什么。记住,代码越少越好!在最小化代码数量之后,要是能\u0026quot;保证\u0026quot;代码是安全的,会非常棒。\n正式验证代码的安全性 是一个活跃的研究领域,我们现在最好的手段,叫 \u0026ldquo;独立安全检查和质量验证\u0026rdquo; 。让一群安全行业内的软件开发者来审计代码,这就是为什么安全型代码几乎都是开源的,写原始代码的人通常很难找到错误,但外部开发人员有新鲜的眼光和不同领域的专业知识,可以发现问题。 另外还有一些安全大会,安全专家可以相互认识,分享想法。 一年一次在拉斯维加斯举办的 def con 是全球最大的安全大会。\n最后,即便尽可能减少代码 ,并进行了安全审计。聪明的攻击者还是会找到方法入侵,因为如此,优秀的开发人员应该计划当程序被攻破后,如何限制损害,控制损害的最大程度,并且不让它危害到计算机上其他东西,这叫 \u0026ldquo;隔离\u0026rdquo; 。要实现隔离,我们可以\u0026quot;沙盒\u0026quot;程序,这好比把生气的小孩放在沙箱里,他们只能摧毁自己的沙堡,不会影响到其他孩子。操作系统会把程序放到沙盒里,方法是给每个程序独有的内存块,其他程序不能动。一台计算机可以运行多个虚拟机,虚拟机模拟计算机,每个虚拟机都在自己的沙箱里。如果一个程序出错,最糟糕的情况是它自己崩溃或者搞坏它处于的虚拟机。计算机上其他虚拟机是隔离的,不受影响。\n好,一些重要安全概念的概览 我们到此就介绍完了。我都还没讲网络安全,比如防火墙。下集我们会讨论 黑客侵入系统的一些方法,然后我们学加密。在此之前,别忘了加强你的密码,打开两步验证,永远不要点可疑邮件。\n我们下周见。\n32. 黑客 \u0026amp; 攻击 i.e. hackers \u0026amp; cyber attacks\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讲了计算机安全的基础知识,包括各种原则和技术,但尽管尽了最大努力,新闻上还是各种 个人,公司,政府被黑客攻击的故事。那些黑客凭技术知识闯入计算机系统,不是所有黑客都是坏人,有些黑客会寻找并修复软件漏洞 ,让系统更安全,他们经常被公司和政府雇来做安全评估,这些黑客叫\u0026quot;白帽子\u0026quot;,他们是好人。另一方面,也有\u0026quot;黑帽\u0026quot;黑客,他们窃取,利用和销售计算机漏洞和数据。黑客的动机有很多种,有些是好玩和好奇,而网络罪犯一般是为了钱。还有的叫\u0026quot;黑客行动主义者\u0026quot;,通过黑客手段影响社会或达到政治目的,这只是冰山一角。\n一般对黑客的刻板印象是某个不受欢迎的小孩在黑暗的房间里,到处都是吃完的比萨盒,这个印象是错的,形容约翰·格林的宿舍还更贴切些。\n今天,我们不会教你如何成为黑客,而是讨论一些入侵原理,给你一个大概概念。\n黑客入侵最常见的方式,不是通过技术,而是欺骗别人,这叫\u0026quot;社会工程学\u0026quot;,欺骗别人让人泄密信息,或让别人配置电脑系统,变得易于攻击。\n最常见的攻击是网络钓鱼,你可能见过,银行发邮件叫你点邮件里的链接,登陆账号,然后你会进入一个像官网的网站,但实际上是个假网站,当你输入用户名和密码时,信息会发给黑客,然后黑客就可以假扮你登陆网站,坏消息!即使成功率只有 1/1000,发一百万封钓鱼邮件,也有一千个帐户中招。\n另一种方法叫 假托 (pretexting),攻击者给某个公司打电话,假装是 it 部门的人,攻击者的第一通电话一般会叫人转接,这样另一个人接的时候,电话看起来像内部的,然后让别人把电脑配置得容易入侵,或让他们泄露机密信息,比如密码或网络配置。\n\u0026gt; 不好意思,等一下。\r\u0026gt; 嘿,我是 it 部门的苏珊\r\u0026gt; 我们遇到一些网络问题,你能帮我检查一个配置吗?\r\u0026gt; 然后就开始了…… 只要预先做一点研究,攻击者可以装得很像真的,比如关键员工的名字。也许要 10 通电话才能找到一个受害者,但只要一个人上当就够了。\n邮件里带\u0026quot;木马\u0026quot;也是常见手段,木马会伪装成无害的东西,比如照片或发票,但实际上是恶意软件。恶意软件有很多种,有的会偷数据,比如银行凭证,有的会加密文件,交赎金才解密,也就是\u0026quot;勒索软件\u0026quot;。如果攻击者无法用木马或电话欺骗,攻击者只能被迫用其他手段,方法之一是暴力尝试,我们上集讨论过,尝试所有可能的密码,直到进入系统。大多数现代系统会加长等待时间,来抵御这种攻击,每次失败就加长等待时间,甚至失败超过一定次数后,完全锁住。\n最近出现一种攻破方法叫 \u0026ldquo;nand 镜像\u0026rdquo;,如果能物理接触到电脑,可以往内存上接几根线,复制整个内存,复制之后,暴力尝试密码,直到设备让你等待,这时只要把复制的内容覆盖掉内存,本质上重置了内存,就不用等待,可以继续尝试密码了,这项方法在 iphone 5c 上管用,更新的设备有机制阻止这种攻击。\n如果你无法物理接触到设备,就必须远程攻击,比如通过互联网。 远程攻击一般需要攻击者利用系统漏洞,来获得某些能力或访问权限,这叫\u0026quot;漏洞利用\u0026quot;(exploit)。一种常见的漏洞利用叫\u0026quot;缓冲区溢出\u0026quot;,\u0026ldquo;缓冲区\u0026quot;是一种概称,指预留的一块内存空间。\n我们在第 23 集,讨论过存像素数据的视频缓冲区。举个简单例子,假设我们在系统登陆界面,要输入用户名和密码。在幕后,系统用缓冲区存输入的值,假设缓冲区大小是 10 ,两个文本缓冲区看起来会像这样:\n当然,操作系统记录的远不止用户名和密码,所以缓冲区前后 肯定有其他数据。当用户输入用户名和密码时,这些值会复制到缓冲区,然后验证是否正确。缓冲区溢出\u0026quot;正如名字所暗示的:它会溢出缓冲区。在这个例子中,超过十个字符的密码会覆盖掉相邻的数据,有时只会让程序或系统崩溃,因为重要值被垃圾数据覆盖了。系统崩溃是坏事,但也许恶作剧黑客就只是想系统崩溃,当个讨厌鬼。但攻击者可以更巧妙地利用这个漏洞 (bug),注入有意义的新值到程序的内存中,比如把\u0026quot;is_admin\u0026quot;的值改成 true。有了任意修改内存的能力,黑客可以绕过\u0026quot;登录\u0026quot;之类的东西,甚至使用那个程序劫持整个系统。有很多方法阻止缓冲区溢出,最简单的方法是,复制之前先检查长度,这叫 \u0026ldquo;边界检查\u0026rdquo;。 许多现代编程语言自带了边界检查,程序也会随机存放变量在内存中的位置, 比如我们之前假设的\u0026quot;is_admin\u0026rdquo;,这样黑客就不知道应该覆盖内存的哪里,导致更容易让程序崩溃,而不是获得访问权限。程序也可以在缓冲区后,留一些不用的空间,然后跟踪里面的值,看是否发生变化,如果发生了变化,说明有攻击者在乱来,这些不用的内存空间叫\u0026quot;金丝雀\u0026quot;,因为以前矿工会带金丝雀下矿,金丝雀会警告危险。\n另一种经典手段叫\u0026quot;代码注入\u0026quot;,最常用于攻击用数据库的网站,几乎所有大网站都用数据库。我们这个系列中不会讲解数据库,所以以下是个简单例子。\n我们会用\u0026quot;结构化查询语言\u0026quot;,也叫 sql,一种流行的数据库 api。假设网页上有登录提示,当用户点击\u0026quot;登录\u0026quot;时,值会发到服务器,服务器会运行代码,检查用户名是否存在,如果存在,看密码是否匹配。 为了做检查,服务器会执行一段叫 \u0026ldquo;sql 查询\u0026rdquo; 的代码,看起来像这样。\n首先,语句要指定从数据库里查什么数据。在这个例子中,我们想查的是密码 (password) (select password),还要指定从哪张表查数据 (from users)。在这个例子里,我们假设所有用户数据都存在 \u0026ldquo;users\u0026rdquo; 表里,最后,服务器不想每次取出一个巨大密码列表,包含所有用户密码,所以用 username = \u0026lsquo;用户名\u0026rsquo;代表只要这个用户,用户输的值会复制到\u0026quot;sql 查询\u0026quot;,所以实际发到 sql 数据库的命令,是这样的 - where username=\u0026lsquo;philbin\u0026rsquo; 。还要注意,sql 命令以分号结尾,那怎么破解这个? 做法是把\u0026quot;sql 命令\u0026quot;输入到用户名里! 比如我们可以发这个奇怪的用户名:\n当服务器把值复制到 sql 查询中,会变成这样:\n正如之前提的,分号用于分隔命令,所以第一条被执行的命令是:\n如果有个用户叫\u0026quot;whateer\u0026quot;,数据库将返回密码。当然,我们不知道密码是什么,所以会出错,服务器会拒绝我们。如果没有一个用户叫\u0026quot;whatever\u0026quot;,数据库会返回 空密码或直接错误,服务器也会拒绝我们。总之 ,我们不在乎,我们感兴趣的是下一个 sql 命令: \u0026ldquo;drop table users\u0026rdquo; - 我们注入的命令。这条命令的意思是删掉 users 这张表,全删干净!这会造成很多麻烦,不管是银行或什么其他地方,注意,我们甚至不需要侵入系统,我们没有猜到正确的用户名和密码,即使没有正式访问权限,还是可以利用 bug 来制造混乱,这是代码注入的一个简单例子。如今几乎所有服务器都会防御这种手段,如果指令更复杂一些,也许可以添加新记录到数据库 - 比如一个新管理员帐户 - 甚至可以让数据库泄露数据,使得黑客窃取信用卡号码,社会安全号码,以及各种其他信息,但我们不会教你具体怎么做 。\n就像缓冲区溢出攻击一样,应该总是假设外部数据是危险的,应该好好检查。\n很多用户名和密码表单,不让你输入特殊字符,比如分号或者括号,作为第一道防御。好的服务器也会清理输入,比如修改或删除特殊字符,然后才放到数据库查询语句里。管用的漏洞利用 (exploits) 一般会在网上贩卖或分享,如果漏洞很流行,或造成的危害很大,价格会越高,或者名气越大,有时甚至政府也会买漏洞利用,让他们侵入系统做间谍工作。当软件制造者不知道软件有新漏洞被发现了,那么这个漏洞叫 \u0026ldquo;零日漏洞\u0026rdquo;。黑帽黑客经常赶时间,抢在白帽程序员做出补丁之前,尽可能利用漏洞,所以保持系统更新非常重要,很多更新都是安全性补丁。如果有足够多的电脑有漏洞,让恶意程序可以在电脑间互相传播,那么叫\u0026quot;蠕虫\u0026quot;。如果黑客拿下大量电脑,这些电脑可以组成\u0026quot;僵尸网络\u0026quot;,可以用于很多目的,比如发大量垃圾邮件,用别人电脑的计算能力和电费挖 bitcoin,或发起\u0026quot;拒绝服务攻击\u0026quot;简称 ddos,攻击服务器。ddos 就是僵尸网络里的所有电脑发一大堆垃圾信息,堵塞服务器,要么迫使别人交钱消灾,或纯粹为了作恶。\n尽管白帽黑客非常努力工作,漏洞利用的文档都在网上,编写软件有很多\u0026quot;最佳实践\u0026quot;,网络攻击每天都在发生,每年损害全球经济差不多 5000 亿,并且随着我们越来越依赖计算机系统,这个数字只会增加。 这使得政府非常担心,因为基础设施越来越电脑化,比如电力厂,电网,交通灯,水处理厂,炼油厂,空管,还有很多其他关键系统。\n很多专家预测下一次大战会主要是网络战争。国家不是被物理攻击打败,而是因为网络战争导致经济和基础设施崩溃。也许不会发射一颗子弹,但是人员伤亡的可能性依然很高,甚至可能高于传统战争,所以大家都应该知道一些方法保证网络安全。\n全球社区因为互联网而互相连接,我们应该确保自己的电脑安全,抵御其他想做坏事的人,也许不要再忽略更新提示?\n我们下周见。\n33. 加密 i.e. cryptography\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n在过去两集,我们聊了很多计算机安全话题,但事实是 ,世上不存在 100%安全的系统,总会有漏洞存在,而且安全专家知道这一点,所以系统架构师会部署\u0026quot;多层防御\u0026quot;,用多层不同的安全机制来阻碍攻击者。有点像城堡的设计一样,首先要避开弓箭手,穿过护城河,翻过城墙,避开热油,打败守卫,才能达到王座。不过我们这里要说的是,计算机安全中最常见的防御形式 - 密码学!\n密码学 (cryptography) 一词 来自 crypto 和 graphy,大致翻译成\u0026quot;秘密写作\u0026quot;。为了加密信息,要用加密算法 (cipher) 把明文转为密文,除非你知道如何解密,不然密文看起来只是一堆乱码。把明文转成密文叫\u0026quot;加密\u0026quot;,把密文恢复回明文叫\u0026quot;解密\u0026quot;(decryption),加密算法早在计算机出现前就有了。\n朱利叶斯·凯撒 用如今我们叫\u0026quot;凯撒加密\u0026quot;的方法 来加密私人信件,他会把信件中的字母 向前移动三个位置,所以 a 会变成 d,brutus 变成 euxwxv。为了解密,接收者要知道 1. 用了什么算法 2. 要偏移的字母位数。\n有一大类算法叫\u0026quot;替换加密\u0026quot;,凯撒密码是其中一种,算法把每个字母替换成其他字母。但有个巨大的缺点是,字母的出现频率是一样的,举个例子,e 在英语中是最常见的字母,如果把 e 加密成 x ,那么密文中 x 的出现频率会很高,熟练的密码破译师可以从统计数据中发现规律,进而破译密码。 1587 年,正因为一个\u0026quot;替换加密\u0026quot;的密文被破译,导致杀伊丽莎白女王的阴谋暴露,使得玛丽女王被处决。\n另一类加密算法叫 \u0026ldquo;移位加密\u0026rdquo;。我们来看一个简单例子叫 \u0026ldquo;列移位加密\u0026rdquo;,我们把明文填入网格,网格大小我们这里选择 5x5,为了加密信息,我们换个顺序来读,比如从左边开始,从下往上,一次一列。加密后字母的排列不同了,解密的关键是,知道读取方向和网格大小是 5x5 。就像之前,如果接收者知道密文和加密方法 才能解密得到原始消息。\n到了 1900 年代,人们用密码学做了加密机器,其中最有名的是德国的英格玛(enigma)纳粹在战时用英格玛加密通讯信息。正如第 15 集中说过,enigma 是一台像打字机的机器,有键盘和灯板,两者都有完整的字母表,而且它有一系列\u0026quot;转子\u0026quot;(rotros) ,是加密的关键。首先,我们只看一个转子,它一面有 26 个接触点,代表 26 个字母,然后线会连到另一面,替换字母,如果输入\u0026rsquo;h\u0026rsquo;,\u0026lsquo;k\u0026rsquo;会从另一边出来,如果输入\u0026rsquo;k\u0026rsquo;,\u0026lsquo;f\u0026rsquo;会从另一边出来,以此类推,这个字母替换过程你应该听起来很熟悉:它是\u0026quot;替换加密\u0026quot;! 但英格玛 (enigma) 更复杂一些,因为它有 3 个或更多转子,一个转子的输出作为下一个转子的输入。 转子还有 26 个起始位置,还可以按不同顺序放入转子,提供更多字母替换映射。转子之后是一个叫\u0026quot;反射器\u0026quot;的特殊电路,它每个引脚会连到另一个引脚,并把信号发回给转子。最后,机器前方有一个插板,可以把输入键盘的字母预先进行替换,又加了一层复杂度。\n让我们用这里的简化版电路,加密一些字母。如果我们按下\u0026quot;h\u0026quot;键,电流会先通过插板,然后通过转子,到达反射器,然后回来转子,回来插板,并照亮键盘灯板的字母\u0026quot;l\u0026quot;,h 就加密成了 l 。注意,电路是双向的,所以如果我们按下 l,h 会亮起来。换句话说,加密和解密的步骤是一样的,你只需要确保 发送机和接收机的初始配置一样就行。如果你有仔细观察,会注意到字母加密后一定会变成另一个字母,之后这成为最大的弱点。最后,为了让英格玛不只是简单的\u0026quot;替换加密\u0026quot;,每输入一个字母,转子会转一格,有点像汽车里程表。如果你输入 a-a-a,可能会变成 b-d-k,映射会随着每次按键而改变。英格玛当然是一块难啃的骨头,但正如我们第 15 集中说的,艾伦·图灵和同事破解了英格玛加密,并把大部分破解流程做成了自动化。\n但随着计算机出现,加密从硬件转往软件,早期加密算法中,应用最广泛的是 ibm 和 nsa 于 1977 年开发的\u0026quot;数据加密标准\u0026quot;(data encryption standard)。des 最初用的是 56 bit 长度的二进制密钥,意味着有 2 的 56 次方,或大约 72 千万亿个不同密钥。在 1977 年时,也许 nsa 有这能力,但没有其他人有足够计算能力 来暴力破解所有可能密钥。但到 1999 年,一台 25 万美元的计算机能在两天内 把 des 的所有可能密钥都试一遍,让 des 算法不再安全。因此 2001 年出了:高级加密标准(aes,advanced encryption standard)。\naes 用更长的密钥 - 128 位/192 位/256 位 - 让暴力破解更加困难。128 位的密钥,哪怕用现在地球上的所有计算机也要上万亿年才能试遍所有组合。你最好赶紧开始!aes 将数据切成一块一块,每块 16 个字节,然后用密钥进行一系列替换加密和移位加密,再加上一些其他操作,进一步加密信息,每一块数据,会重复这个过程 10 次或以上。你可能想知道:为什么只重复 10 次?为什么用 128 位密钥,而不是 10000 位?这其实是基于性能的权衡。如果要花几小时加密和发邮件,或几分钟载入网站,没人愿意用。aes 在性能和安全性间取得平衡,如今 aes 被广泛使用,比如 iphone 上加密文件,用 wpa2 协议在 wifi 中访问 https 网站。\n到目前为止 ,我们讨论过的加密技术依赖于发送者和接收者都知道密钥,发件人用密钥加密,收件人用相同的密钥解密。以前,密钥可以口头约定,或依靠物品,比如德国人给英格玛配了密码本,上面有每天的配置,但互联网时代没法这样做,你能想象 要打开密码本才能访问 youtube 吗?我们需要某种方法 在公开的互联网上传递密钥给对方,这看起来好像不安全,如果密钥被黑客拦截了,黑客不就能解密通信了吗?解决方案是 \u0026ldquo;密钥交换\u0026rdquo;!\n密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法,我们可以用\u0026quot;单向函数\u0026quot;来做。单项函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难。为了让你明白单项函数,我们拿颜色作比喻,将颜色混合在一起很容易,但想知道混了什么颜色很难,要试很多种可能才知道,用这个比喻,那么我们的密钥是一种独特的颜色。首先,有一个公开的颜色,所有人都可以看到,然后,约翰和我各自选一个秘密颜色,只有自己知道。为了交换密钥,我把我的 秘密颜色 和 公开颜色 混在一起,然后发给约翰,可以写信发,用信鸽发,什么方式都行。约翰也这样做,把他的秘密颜色和公开颜色混在一起,然后发我。我收到约翰的颜色之后,把我的秘密颜色加进去, 现在 3 种颜色混合在一起。john 也一样做。瞧!我们有了一样的颜色,我们可以把这个颜色当密钥,尽管我们从来没有给对方发过这颜色。外部窥探者可以知道部分信息,但无法知道最终颜色,当然,计算机要传输数据时,混合颜料和发颜料不太合适,但幸运的是,数学单向函数是完美的,我们可以用 \u0026ldquo;迪菲-赫尔曼密钥交换\u0026rdquo;。在 diffie-hellman 中,单向函数是模幂运算,意思是先做幂运算,拿一个数字当底数,拿一个数字当指数,比如 a\nb\n然后除以第三个数字,最后拿到我们想要的余数。\n举个例子,假设我们想算 3 的 5 次方,模 31 ,我们先算 3 的 5 次方,得到 243,然后除 31,取余数,得到 26 。重点是如果只给余数和基数,很难得知指数是多少。如果我告诉你,3 的某次方 模 31,余数是 7 。你要试很多次,才能知道次方是多少。如果把数字变长一些,比如几百位长,想找到秘密指数是多少,几乎是不可能的。\n现在我们来讨论 diffie-hellman 是怎么,用模幂运算 算出双方共享的密钥。首先,我们有公开的值 - 基数和模数,就像公开的油漆颜色,所有人都看的到,甚至坏人!为了安全向 john 发信息,我选一个秘密指数:x ,然后算 b^x mod m 的结果,然后把这个大数字发给 john 。john 也一样做,选一个秘密指数 y,然后把 b^y mod m 的结果发我,为了算出 双方共用的密钥,我把 john 给我的数,用我的秘密指数 x,进行模幂运算 (看上图),数学上相等于 b 的 xy 次方 模 m 。john 也一样做,拿我给他的数 进行模幂运算,最终得到一样的数。双方有一样的密钥,即使我们从来没给对方发过各自的秘密指数。我们可以用这个大数字当密钥,用 aes 之类的加密技术来加密通信,\u0026ldquo;diffie-hellman 密钥交换\u0026quot;是建立共享密钥的一种方法。\n双方用一样的密钥加密和解密消息,这叫\u0026quot;对称加密\u0026rdquo;, 因为密钥一样,凯撒加密,英格玛,aes 都是\u0026quot;对称加密\u0026quot;,还有\u0026quot;非对称加密\u0026quot;,有两个不同的密钥,一个是公开的,另一个是私有的。人们用公钥加密消息 ,只有有私钥的人能解密。换句话说,知道公钥只能加密但不能解密 - 它是\u0026quot;不对称\u0026quot;的!\n想象一个可以锁上的盒子,为了收到安全的信息,我们可以给别人箱子和锁,别人把信息放箱子,然后锁起来,把盒子寄回给我,只有我的钥匙能打开。上锁后,如果发件人或其他人想打开盒子 除了暴力尝试没有其他办法。和盒子例子一样,公钥加密后只能私钥来解密。反过来也是可以的:私钥加密后 ,用公钥解密。这种做法用于签名,服务器可以用私钥加密,任何人都可以用服务器的公钥解密,就像一个不可伪造的签名,因为只有私钥的持有人 能加密,这能证明数据来自正确的服务器或个人,而不是某个假冒者。\n目前最流行的\u0026quot;非对称加密\u0026quot;技术是 rsa ,名字来自发明者: rivest, shamir, adleman 。\n现在你学会了现代密码学的所有\u0026quot;关键\u0026quot;部分: 对称加密,密钥交换,公钥密码学。\n当你访问一个安全的网站,比如银行官网,绿色锁图标代表 用了公钥密码学,验证服务器的密钥,然后建立临时密钥,然后用对称加密保证通信安全。不管你是网上购物,发邮件给朋友,还是看猫咪视频,不管你是网上购物,发邮件给朋友,还是看猫咪视频,密码学都在保护你的隐私和安全。\n谢啦密码学!\n34. 机器学习 \u0026amp; 人工智能 i.e. machine learning \u0026amp; artificial intelligence\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n我们之前说过 计算机很擅长存放,整理,获取和处理大量数据,很适合有上百万商品的电商网站,或是存几十亿条健康记录,方便医生看,但如果想根据数据做决定呢?这是机器学习的本质,机器学习算法让计算机可以从数据中学习,然后自行做出预测和决定,能自我学习的程序很有用 。比如判断是不是垃圾邮件?这人有心律失常吗?youtube 的下一个视频该推荐哪个?虽然有用,但我们不会说它有人类一般的智能,虽然 ai 和 ml 这两词经常混着用。\n大多数计算机科学家会说 ,机器学习是为了实现人工智能这个更宏大目标的技术之一,人工智能简称 ai(artificial intelligence)。机器学习和人工智能算法一般都很复杂,所以我们不讲具体细节,重点讲概念。\n我们从简单例子开始:判断飞蛾是\u0026quot;月蛾\u0026quot;还是\u0026quot;帝蛾\u0026quot;,这叫\u0026quot;分类\u0026quot;,做分类的算法叫 \u0026ldquo;分类器\u0026rdquo;。虽然我们可以用 照片和声音 来训练算法,很多算法会减少复杂性,把数据简化成 \u0026ldquo;特征\u0026rdquo;,\u0026ldquo;特征\u0026quot;是用来帮助\u0026quot;分类\u0026quot;的值。对于之前的飞蛾分类例子我们用两个特征:\u0026ldquo;翼展\u0026quot;和\u0026quot;重量\u0026rdquo;,为了训练\u0026quot;分类器\u0026quot;做出好的预测,我们需要\u0026quot;训练数据\u0026rdquo;,为了得到数据,我们派昆虫学家到森林里 收集\u0026quot;月蛾\u0026quot;和\u0026quot;帝蛾\u0026quot;的数据。专家可以认出不同飞蛾,所以专家不只记录特征值,还会把种类也写上,这叫 \u0026ldquo;标记数据\u0026rdquo;。因为只有两个特征,很容易用散点图把数据视觉化。红色标了 100 个帝蛾蓝色标了 100 个月蛾,可以看到大致分成了两组,但中间有一定重叠,所以想完全区分两个组比较困难,所以机器学习算法登场 - 找出最佳区分。我用肉眼大致估算下,然后判断 翼展小于 45 毫米的 很可能是帝蛾,可以再加一个条件,重量必须小于 75 ,才算是帝蛾。这些线叫 \u0026ldquo;决策边界\u0026rdquo;,如果仔细看数据,86 只帝蛾在正确的区域,但剩下 14 只在错误的区域。另一方面,82 只月蛾在正确的区域,18 个在错误的区域。这里有个表 记录正确数和错误数,这表叫\u0026quot;混淆矩阵\u0026quot;。\u0026ldquo;黑客帝国三部曲\u0026quot;的后两部也许该用这个标题。注意我们没法画出 100% 正确分类的线,降低翼展的决策边界,会把更多\u0026quot;帝蛾\u0026quot;误分类成\u0026quot;月蛾\u0026rdquo;。如果提高,会把更多月蛾分错类。\n机器学习算法的目的,是最大化正确分类 + 最小化错误分类。在训练数据中,有 168 个正确,32 个错误,平均准确率 84% 。用这些决策边界,如果我们进入森林,碰到一只不认识的飞蛾,我们可以测量它的特征,并绘制到决策空间上,这叫 \u0026ldquo;未标签数据\u0026rdquo;。决策边界可以猜测飞蛾种类,这里我们预测是\u0026quot;月蛾\u0026quot;,这个把决策空间 切成几个盒子的简单方法,可以用\u0026quot;决策树\u0026quot;来表示。画成图像,会像左侧 用 if 语句写代码,会像右侧。生成决策树的 机器学习算法需要选择用什么特征来分类,每个特征用什么值。\u0026ldquo;决策树\u0026quot;只是机器学习的一个简单例子,如今有数百种算法,而且新算法不断出现,一些算法甚至用多个\u0026quot;决策树\u0026quot;来预测,计算机科学家叫这个\u0026quot;森林\u0026rdquo;,因为有多颗树嘛。也有不用树的方法,比如\u0026quot;支持向量机\u0026quot;,本质上是用任意线段来切分\u0026quot;决策空间\u0026quot;,不一定是直线,可以是多项式或其他数学函数。就像之前,机器学习算法负责找出最好的线,最准的决策边界。之前的例子只有两个特征,人类也可以轻松做到,如果加第 3 个特征,比如\u0026quot;触角长度\u0026quot;,那么 2d 线段,会变成 3d 平面,在三个维度上做决策边界,这些平面不必是直的,而且 真正有用的分类器 会有很多飞蛾种类。你可能会同意 现在变得太复杂了,但这也只是个简单例子 - 只有 3 个特征和 5 个品种,我们依然可以用 3d 散点图 画出来。不幸的是,一次性看 4 个或 20 个特征,没有好的方法,更别说成百上千的特征了,但这正是机器学习要面临的问题。你能想象靠手工 在一个上千维度的决策空间里给超平面 (hyperplane) 找出一个方程吗?大概不行。但聪明的机器学习算法可以做到, google,facebook,微软和亚马逊的计算机里整天都在跑这些算法。\n\u0026ldquo;决策树\u0026quot;和\u0026quot;支持向量机\u0026quot;这样的技术 发源自统计学,统计学早在计算机出现前,就在用数据做决定。\n有一大类机器学习算法用了统计学,但也有不用统计学的算法,其中最值得注意的是 人工神经网络,灵感来自大脑里的神经元,想学习神经元知识的人,可以看这 3 集。神经元是细胞,用电信号和化学信号 来处理和传输消息,它从其他细胞 得到一个或多个输入,然后处理信号并发出信号,形成巨大的互联网络,能处理复杂的信息。就像你的大脑 在看这个视频,人造神经元很类似,可以接收多个输入,然后整合并发出一个信号,它不用电信号或化学信号,而是吃数字进去,吐数字出来。它们被放成一层层,形成神经元网络,因此得名神经网络。\n回到飞蛾例子,看如何用神经网络分类。\n\u0026gt; 我们的第一层 - 输入层 -\r\u0026gt; 提供需要被分类的单个飞蛾数据\r\u0026gt; 同样,这次也用重量和翼展\r\u0026gt; \u0026gt; 另一边是输出层,有两个神经元:\r\u0026gt; 一个是帝蛾,一个是月蛾\r\u0026gt; 2 个神经元里最兴奋的 就是分类结果\r\u0026gt; \u0026gt; 中间有一个隐藏层\r\u0026gt; 负责把输入变成输出,负责干分类这个重活\r\u0026gt; 为了看看它是如何分类的\r\u0026gt; 我们放大\u0026#34;隐藏层\u0026#34;里的一个神经元\r\u0026gt; 神经元做的第一件事 是把每个输入乘以一个权重\r\u0026gt; \u0026gt; 假设 2.8 是第一个输入,0.1 是第二个输入。\r\u0026gt; 然后它会相加输入\r\u0026gt; 总共是 9.74\r\u0026gt; \u0026gt; 然后对这个结果,用一个偏差值处理\r\u0026gt; 意思是 加或减一个固定值\r\u0026gt; 比如-6,得到 3.74\r\u0026gt; \u0026gt; 做神经网络时,这些偏差和权重,一开始会设置成随机值\r\u0026gt; 然后算法会调整这些值 来训练神经网络\r\u0026gt; 使用\u0026#34;标记数据\u0026#34;来训练和测试\r\u0026gt; 逐渐提高准确性\r\u0026gt; - 很像人类学习的过程\r\u0026gt; \u0026gt; 最后,神经元有激活函数,它也叫传递函数,\r\u0026gt; 会应用于输出,对结果执行最后一次数学修改\r\u0026gt; 例如,把值限制在-1 和+1 之间\r\u0026gt; 或把负数改成 0\r\u0026gt; 我们用线性传递函数,它不会改变值\r\u0026gt; 所以 3.74 还是 3.74\r\u0026gt; \u0026gt; 所以这里的例子\r\u0026gt; 输入 0.55 和 82,输出 3.74\r\u0026gt; 这只是一个神经元,\r\u0026gt; 但加权,求和,偏置,激活函数\r\u0026gt; 会应用于一层里的每个神经元\r\u0026gt; 并向前传播,一次一层\r\u0026gt; \u0026gt; 数字最高的就是结果:\r\u0026gt; 月蛾 重要的是,隐藏层不是只能有一层,可以有很多层,\u0026ldquo;深度学习\u0026quot;因此得名。训练更复杂的网络, 需要更多的计算量和数据。尽管神经网络 50 多年前就发明了,深层神经网络直到最近才成为可能,感谢强大的处理器和超快的 gpu,感谢游戏玩家对帧率的苛刻要求!\n几年前,google 和 facebook 展示了深度神经网络 在照片中识别人脸的准确率,和人一样高 - 人类可是很擅长这个的! 这是个巨大的里程碑! 现在有深层神经网络开车,翻译,诊断医疗状况等等,这些算法非常复杂,但还不够\u0026quot;聪明\u0026rdquo;,它们只能做一件事,分类飞蛾,找人脸,翻译,这种 ai 叫\u0026quot;弱 ai\u0026quot;或\u0026quot;窄 ai\u0026rdquo;,只能做特定任务。但这不意味着它没用,能自动做出诊断的医疗设备,和自动驾驶的汽车真是太棒了!但我们是否需要这些计算机来创作音乐,在空闲时间找美味食谱呢?也许不要! 如果有的话 还挺酷的,真正通用的,像人一样聪明的 ai,叫 \u0026ldquo;强 ai\u0026rdquo;,目前没人能做出来 接近人类智能的 ai,有人认为不可能做出来,但许多人说 数字化知识的爆炸性增长 - 比如维基百科,网页和 youtube 视频 - 是\u0026quot;强 ai\u0026quot;的完美引燃物。你一天最多只能看 24 小时的 youtube ,计算机可以看上百万小时,比如,ibm 的沃森吸收了 2 亿个网页的内容,包括维基百科的全文。虽然不是\u0026quot;强 ai\u0026quot;, 但沃森也很聪明 。在 2011 年的知识竞答中碾压了人类,ai 不仅可以吸收大量信息 也可以不断学习进步,而且一般比人类快得多。2016 年 google 推出 alphago - 一个会玩围棋的窄 ai ,它和自己的克隆版下无数次围棋 ,从而打败最好的人类围棋选手。\n学习什么管用,什么不管用 自己发现成功的策略,这叫 \u0026ldquo;强化学习\u0026rdquo; ,是一种很强大的方法,和人类的学习方式非常类似。人类不是天生就会走路,是上千小时的试错学会的,计算机现在才刚学会反复试错来学习,对于很多狭窄的问题,强化学习已被广泛使用,有趣的是,如果这类技术可以更广泛地应用,创造出类似人类的\u0026quot;强 ai\u0026quot; 能像人类小孩一样学习,但学习速度超快。 如果这发生了,对人类可能有相当大的影响 - 我们以后会讨论。\n感谢收看。我们下周见。\n35. 计算机视觉 i.e. computer vision\n(。・∀・)ノ゙嗨 我是 carrie anne,欢迎收看计算机科学速成课\n今天, 我们来思考视觉的重要性,大部分人靠视觉来做饭,越过障碍,读路牌,看视频,以及无数其它任务。视觉是信息最多的感官 ,比如周围的世界是怎样的,如何和世界交互,因此半个世纪来计算机科学家一直在想办法让计算机有视觉,因此诞生了\u0026quot;计算机视觉\u0026quot;这个领域,目标是让计算机理解图像和视频。用过相机或手机的都知道 ,可以拍出有惊人保真度和细节的照片 - 比人类强得多,但正如计算机视觉教授 李飞飞 最近说的,\u0026ldquo;听到\u0026quot;不等于\u0026quot;听懂\u0026rdquo;,\u0026ldquo;看到\u0026quot;不等于\u0026quot;看懂\u0026rdquo;。\n复习一下,图像是像素网格,每个像素的颜色通过三种基色定义:红,绿,蓝,通过组合三种颜色的强度 ,可以得到任何颜色, 也叫 rgb 值。\n最简单的计算机视觉算法,最合适拿来入门的,是跟踪一个颜色物体,比如一个粉色的球。首先,我们记下球的颜色,保存最中心像素的 rgb 值,然后给程序喂入图像,让它找最接近这个颜色的像素。算法可以从左上角开始,逐个检查像素,计算和目标颜色的差异,检查了每个像素后,最贴近的像素,很可能就是球。不只是这张图片, 我们可以在视频的每一帧图片跑这个算法,跟踪球的位置。当然,因为光线,阴影和其它影响,球的颜色会有变化,不会和存的 rgb 值完全一样,但会很接近。如果情况更极端一些 比如比赛是在晚上,追踪效果可能会很差。如果球衣的颜色和球一样,算法就完全晕了,因此很少用这类颜色跟踪算法,除非环境可以严格控制。\n颜色跟踪算法是一个个像素搜索, 因为颜色是在一个像素里,但这种方法 不适合占多个像素的特征,比如物体的边缘,是多个像素组成的。为了识别这些特征,算法要一块块像素来处理,每一块都叫\u0026quot;块\u0026quot;。举个例子,找垂直边缘的算法,假设用来帮无人机躲避障碍。为了简单,我们把图片转成灰度 ,不过大部分算法可以处理颜色,放大其中一个杆子,看看边缘是怎样的,可以很容易地看到 杆子的左边缘从哪里开始,因为有垂直的颜色变化。我们可以弄个规则说,某像素是垂直边缘的可能性 取决于左右两边像素的颜色差异程度,左右像素的区别越大,这个像素越可能是边缘,如果色差很小,就不是边缘,这个操作的数学符号 看起来像这样,这叫\u0026quot;核\u0026quot;或\u0026quot;过滤器\u0026quot;,里面的数字用来做像素乘法,总和 存到中心像素里。\n我们来看个实际例子。我已经把所有像素转成了灰度值,现在把\u0026quot;核\u0026quot;的中心,对准感兴趣的像素,这指定了每个像素要乘的值,然后把所有数字加起来。在这里,最后结果是 147,成为新像素值,把 核 应用于像素块,这种操作叫\u0026quot;卷积\u0026quot;。现在我们把\u0026quot;核\u0026quot;应用到另一个像素,结果是 1,色差很小,不是边缘。如果把\u0026quot;核\u0026quot;用于照片中每个像素,结果会像这样,垂直边缘的像素值很高,注意,水平边缘(比如背景里的平台),几乎看不见。如果要突出那些特征,要用不同的\u0026quot;核\u0026quot;,用对水平边缘敏感的\u0026quot;核\u0026quot;,这两个边缘增强的核叫 \u0026ldquo;prewitt 算子\u0026rdquo;,以发明者命名,这只是众多\u0026quot;核\u0026quot;的两个例子。\n\u0026ldquo;核\u0026quot;能做很多种图像转换,比如这个\u0026quot;核\u0026quot;能锐化图像,这个\u0026quot;核\u0026quot;能模糊图像。\u0026ldquo;核\u0026quot;也可以像饼干模具一样,匹配特定形状,之前做边缘检测的\u0026quot;核\u0026rdquo;,会检查左右或上下的差异,但我们也可以做出 擅长找线段的\u0026quot;核\u0026rdquo;,或者包了一圈对比色的区域,这类\u0026quot;核\u0026quot;可以描述简单的形状,比如鼻梁往往比鼻子两侧更亮,所以线段敏感的\u0026quot;核\u0026quot;对这里的值更高,眼睛也很独特 - 一个黑色圆圈被外层更亮的一层像素包着,有其它\u0026quot;核\u0026quot;对这种模式敏感。\n当计算机扫描图像时,最常见的是用一个窗口来扫,可以找出人脸的特征组合,虽然每个\u0026quot;核\u0026quot;单独找出脸的能力很弱 ,但组合在一起会相当准确,不是脸但又有一堆脸的特征在正确的位置,这种情况不太可能。这是一个早期很有影响力的算法的基础,叫 维奥拉·琼斯 人脸检测算法,如今的热门算法是 \u0026ldquo;卷积神经网络\u0026rdquo;。\n我们上集谈了神经网络,如果需要可以去看看,总之,神经网络的最基本单位,是神经元,它有多个输入,然后会把每个输入 乘一个权重值,然后求总和。听起来好像挺耳熟,因为它很像\u0026quot;卷积\u0026quot;。实际上,如果我们给神经元输入二维像素,完全就像\u0026quot;卷积\u0026quot;,输入权重等于\u0026quot;核\u0026quot;的值,但和预定义\u0026quot;核\u0026quot;不同,神经网络可以学习对自己有用的\u0026quot;核\u0026quot;,来识别图像中的特征。\u0026ldquo;卷积神经网络\u0026quot;用一堆神经元处理图像数据,每个都会输出一个新图像,本质上是被不同的\u0026quot;核\u0026quot;处理了,输出会被后面一层神经元处理,卷积卷积再卷积,第一层可能会发现\u0026quot;边缘\u0026quot;这样的特征,单次卷积可以识别出这样的东西,之前说过,下一层可以在这些基础上识别,比如由\u0026quot;边缘\u0026quot;组成的角落,然后下一层可以在\u0026quot;角落\u0026quot;上继续卷积,下一些可能有识别简单物体的神经元,比如嘴和眉毛,然后不断重复,逐渐增加复杂度,直到某一层把所有特征放到一起: 眼睛,耳朵,嘴巴,鼻子,然后说:\u0026ldquo;啊哈,这是脸!\u0026rdquo;\n\u0026ldquo;卷积神经网络\u0026quot;不是非要很多很多层,但一般会有很多层,来识别复杂物体和场景,所以算是\u0026quot;深度学习\u0026rdquo; 。\u0026ldquo;维奥拉·琼斯\u0026quot;和\u0026quot;卷积神经网络\u0026quot;不只是认人脸,还可以识别手写文字,在 ct 扫描中发现肿瘤,监测马路是否拥堵,但我们这里接着用人脸举例。\n不管用什么算法,识别出脸之后,可以用更专用的计算机视觉算法 来定位面部标志,比如鼻尖和嘴角,有了标志点,判断眼睛有没有张开就很容易了,只是点之间的距离罢了,也可以跟踪眉毛的位置,眉毛相对眼睛的位置 可以代表惊喜或喜悦,根据嘴巴的标志点,检测出微笑也很简单,这些信息可以用\u0026quot;情感识别算法\u0026quot;来识别。让电脑知道你是开心,忧伤,沮丧,困惑等等,然后计算机可以做出合适的行为。比如当你不明白时 给你提示,你心情不好时,就不弹更新提示了,这只是计算机通过视觉感知周围的一个例子。不只是物理环境 - 比如是不是在上班,或是在火车上,还有社交环境 - 比如是朋友的生日派对,还是正式商务会议。你在不同环境会有不同行为,计算机也应如此,如果它们够聪明的话。..\n面部标记点 也可以捕捉脸的形状,比如两只眼睛之间的距离,以及前额有多高,做生物识别,让有摄像头的计算机能认出你。不管是手机解锁, 还是政府用摄像头跟踪人,人脸识别有无限应用场景。另外,跟踪手臂和全身的标记点,最近也有一些突破,让计算机理解用户的身体语言,比如用户给联网微波炉的手势。\n正如系列中常说的,抽象是构建复杂系统的关键。\n计算机视觉也是一样。硬件层面,有工程师在造更好的摄像头 ,让计算机有越来越好的视力,我自己的视力却不能这样。用来自摄像头的数据 ,可以用视觉算法找出脸和手,然后可以用其他算法接着处理,解释图片中的东西,比如用户的表情和手势。有了这些,人们可以做出新的交互体验,比如智能电视和智能辅导系统 会根据用户的手势和表情来回应。\n这里的每一层都是活跃的研究领域,每年都有突破,这只是冰山一角。如今, 计算机视觉无处不在 - 商店里扫条形码, 等红灯的自动驾驶汽车,或是 snapchat 里添加胡子的滤镜。令人兴奋的是,一切才刚刚开始。最近的技术发展,比如超快的 gpu,会开启越来越多可能性。视觉能力达到人类水平的计算机 会彻底改变交互方式,当然,如果计算机能听懂我们然后回话,就更好了。\n我们下周讨论,到时见。\n36. 自然语言处理 i.e. natural language processing\n(。・∀・)ノ゙嗨 我是 carrie anne,欢迎收看计算机科学速成课\n上集我们讨论了计算机视觉 - 让电脑能看到并理解,今天我们讨论怎么让计算机理解语言。你可能会说:计算机已经有这个能力了,在第 9 和第 12 集,我们聊了机器语言和更高层次的编程语言。虽然从定义来说 ,它们也算语言,但词汇量一般很少,而且非常结构化,代码只能在拼写和语法完全正确时,编译和运行。\n当然,这和人类语言完全不同 - 人类语言叫\u0026quot;自然语言\u0026rdquo;。自然语言有大量词汇,有些词有多种含义,不同口音,以及各种有趣的文字游戏。人们在写作和说话时也会犯错,比如单词拼在一起发音,关键细节没说 ,导致意思模糊两可,以及发错音。但大部分情况下,另一方能理解,人类有强大的语言能力,因此,让计算机拥有语音对话的能力,这个想法从构思计算机时就有了。\n\u0026ldquo;自然语言处理\u0026quot;因此诞生,简称 nlp(natural language processing),结合了计算机科学和语言学的 一个跨学科领域。单词组成句子的方式有无限种,我们没法给计算机一个字典,包含所有可能句子,让计算机理解人类在嘟囔什么,所以 nlp 早期的一个基本问题是 怎么把句子切成一块块,这样更容易处理。\n上学时,老师教你 英语单词有九种基本类型:名词,代词,冠词,动词,形容词,副词,介词,连词和感叹词,这叫\u0026quot;词性\u0026rdquo;。还有各种子类,比如单数名词 vs 复数名词 ,副词最高级 vs 副词比较级,但我们不会深入那些。\n了解单词类型有用,但不幸的是,很多词有多重含义 比如 rose 和 leaves ,可以用作名词或动词。仅靠字典,不能解决这种模糊问题,所以电脑也要知道语法,因此开发了 \u0026ldquo;短语结构规则\u0026rdquo; 来代表语法规则。例如,英语中有一条规则,句子可以由一个名词短语和一个动词短语组成,名词短语可以是冠词,如 the ,然后一个名词,或一个形容词后面跟一个名词。你可以给一门语言制定出一堆规则,用这些规则,可以做出\u0026quot;分析树\u0026rdquo;,它给每个单词标了可能是什么词性,也标明了句子的结构。数据块更小 ,更容易处理,每次语音搜索,都有这样的流程,比如 \u0026ldquo;最近的披萨在哪里\u0026rdquo;,计算机能明白这是\u0026quot;哪里\u0026quot;(where)的问题,知道你想要名词\u0026quot;披萨\u0026quot;(pizza),而且你关心的维度是\u0026quot;最近的\u0026quot;(nearest)。\u0026ldquo;最大的长颈鹿是什么?\u0026ldquo;或\u0026quot;thriller 是谁唱的?\u0026rdquo; 也是这样处理。\n把语言像乐高一样拆分,方便计算机处理,计算机可以回答问题 ,以及处理命令,比如\u0026quot;设 2:20 的闹钟\u0026rdquo;,或\u0026quot;用 spotify 播放 t-swizzle\u0026quot;,但你可能体验过,如果句子复杂一点,计算机就没法理解了。\n嘿 siri ...... 俺觉得蒙古人走得太远了\r在这个最温柔的夏日的日子里,你觉得怎么样?\rsiri:我没明白 还有,\u0026ldquo;短语结构规则\u0026quot;和其他把语言结构化的方法,可以用来生成句子。数据存在语义信息网络时,这种方法特别有效,实体互相连在一起,提供构造句子的所有成分。siri:thriller 于 1983 年发行,由迈克尔杰克逊演唱,google 版的叫\u0026quot;知识图谱\u0026rdquo;。\n在 2016 年底,包含大概七百亿个事实,以及不同实体间的关系,处理, 分析, 生成文字 ,是聊天机器人的最基本部件 - 聊天机器人就是能和你聊天的程序。早期聊天机器人大多用的是规则,专家把用户可能会说的话,和机器人应该回复什么,写成上百个规则,显然,这很难维护,而且对话不能太复杂。\n一个著名早期例子叫 eliza ,1960 年代中期 诞生于麻省理工学院,一个治疗师聊天机器人。它用基本句法规则 来理解用户打的文字,然后向用户提问,有时候会感觉像和人类沟通一样,但有时会犯简单甚至很搞笑的错误。\n聊天机器人和对话系统,在过去五十年发展了很多,如今可以和真人很像!如今大多用机器学习,用上 gb 的真人聊天数据 来训练机器人,现在聊天机器人已经用于客服回答,客服有很多对话可以参考,人们也让聊天机器人互相聊天。在 facebook 的一个实验里,聊天机器人甚至发展出自己的语言,很多新闻把这个实验 报导的很吓人,但实际上只是计算机 在制定简单协议来帮助沟通,这些语言不是邪恶的,而是为了效率。\n但如果听到一个句子 - 计算机怎么从声音中提取词汇?这个领域叫\u0026quot;语音识别\u0026quot;。这个领域已经重点研究了几十年,贝尔实验室在 1952 年推出了第一个语音识别系统,绰号 audrey,自动数字识别器。如果你说得够慢,它可以识别全部十位数字,这个项目没有实际应用,因为手输快得多。十年后,1962 年的世界博览会上,ibm 展示了一个鞋盒大小的机器,能识别 16 个单词,为了推进\u0026quot;语音识别\u0026quot;领域的研究,darpa 在 1971 年启动了一项雄心勃勃的五年筹资计划,之后诞生了卡内基梅隆大学的 harpy。harpy 是第一个可以识别 1000 个单词以上的系统,但那时的电脑,语音转文字,经常比实时说话要慢十倍或以上。幸运的是,1980,1990 年代 计算机性能的大幅提升,实时语音识别变得可行,同时也出现了处理自然语言的新算法,不再是手工定规则,而是用机器学习,从语言数据库中学习。\n如今准确度最高的语音识别系统 用深度神经网络,我们在第 34 集讲过。为了理解原理,我们来看一些对话声音,先看元音,比如 a 和 e ,这是两个声音的波形,我们在第 21 集(文件格式)说过。\n这个信号来自 麦克风内部隔膜震动的频率,在这个视图中,横轴是时间,竖轴是隔膜移动的幅度,或者说振幅,虽然可以看到 2 个波形有区别,但不能看出,\u0026ldquo;啊!这个声音肯定是 e\u0026rdquo;。为了更容易识别,我们换个方式看:\n谱图\n这里横轴还是时间,但竖轴不是振幅,而是不同频率的振幅,颜色越亮,那个频率的声音越大,这种波形到频率的转换 是用一种很酷的算法做的 - 快速傅立叶变换(fft)。如果你盯过立体声系统的 eq 可视化器,它们差不多是一回事,谱图是随着时间变化的,你可能注意到,信号有种螺纹图案,那是我声道的回声。为了发出不同声音,我要把声带,嘴巴和舌头变成不同形状,放大或减少不同的共振,可以看到有些区域更亮,有些更暗。如果从底向上看,标出高峰 - 叫\u0026quot;共振峰\u0026quot; - 可以看到有很大不同,所有元音都是如此,这让计算机可以识别元音,然后识别出整个词。\n让我们看一个更复杂的例子,当我说\u0026quot;她很开心\u0026quot;的时候,可以看到 e 声,和 a 声,以及其它不同声音,比如 she 中的 shh 声,was 中的 wah 和 sss,等等,这些构成单词的声音片段叫\u0026quot;音素\u0026quot;。语音识别软件 知道这些音素,英语有大概 44 种音素,所以本质上变成了音素识别,还要把不同的词分开,弄清句子的开始和结束点,最后把语音转成文字,使这集视频开头里讨论的那些技术成为可能。因为口音和发音错误等原因,人们说单词的方式略有不同,所以结合语言模型后,语音转文字的准确度会大大提高。里面有单词顺序的统计信息,比如:\u0026ldquo;她\u0026quot;后面很可能跟一个形容词,比如\u0026quot;很开心\u0026rdquo;,\u0026ldquo;她\u0026quot;后面很少是名词,如果不确定是 happy 还是 harpy,会选 happy,因为语言模型认为可能性更高。\n最后,我们来谈谈 \u0026ldquo;语音合成\u0026rdquo;,让计算机输出语音,它很像语音识别,不过反过来。把一段文字,分解成多个声音,然后播放这些声音。早期语音合成技术,可以清楚听到音素是拼在一起的,比如这个 1937 年贝尔实验室的手动操作机器,不带感情的说 \u0026ldquo;她看见了我\u0026rdquo;。\n到了 1980 年代,技术改进了很多,但音素混合依然不够好,产生明显的机器人声。如今,电脑合成的声音,比如 siri, cortana, alexa 好了很多,但还不够像人,但我们非常非常接近了,这个问题很快会被解决。\n现在语音界面到处都是,手机里,汽车里,家里,也许不久之后耳机也会有。这创造一个正反馈循环,人们用语音交互的频率会提高,这又给了谷歌,亚马逊,微软等公司更多数据来训练语音系统,提高准确性。准确度高了,人们更愿意用语音交互,越用越好,越好越用。很多人预测,语音交互会越来越常见,就像如今的屏幕,键盘,触控板等设备,这对机器人发展是个好消息。\n机器人就不用走来走去时带个键盘和人类沟通。\n下周我们讲机器人。到时见。\n37. 机器人 i.e. robots\n嗨,我是 carrie anne,欢迎收看计算机速成课。\n今天 我们要讨论机器人,你脑中冒出来的第一个印象估计是 类人机器人,经常在电视剧和电影里看到。有时候它们是朋友和同事,但更常见的是阴险无情,身经百战。我们经常把机器人看成未来科技,但事实是:机器人时代已经来临了 - 它们是同事,帮我们把困难的工作,做得更快更好。机器人的定义有很多种,但总的来说,机器人由计算机控制,可以自动执行一系列动作的机器,外观并不重要,可以是给汽车喷漆的机械臂,无人机,或辅助外科医生的蛇状机器人,以及人形机器人。\n有时我们叫虚拟人物\u0026quot;机器人\u0026rdquo;,但叫 bot 甚至 agent 会更合适,因为\u0026quot;机器人\u0026quot;的潜在含义是存在于现实世界中的机器。robot (机器人) 一词 ,首次出现在 1920 年的一部捷克戏剧,代表人造的类人角色。robot 源于斯拉夫语词汇 robota 代表强迫劳动,代表农民在十九世纪 欧洲封建社会的强迫劳动,戏剧没讲太多技术细节。但即使一个世纪后,这种描述依然很普遍:机器人都是大规模生产,高效不知疲倦,看起来像人的东西,但毫无情感,不会保护自己,没有创造力。\n更广义的自动运行机器,早在 1920 年代前就有了。很多古代发明家 发明了能自动运行的机械装置,比如计时和定时敲钟。有很多装置 ,有动物和人类的形象 ,能跳舞,唱歌,打鼓等。这些不用电,而且肯定没有电子部件的机器,叫\u0026quot;自动机\u0026quot;。\n举个例子 1739 年法国人 jacques de vaucans 做了个自动机,法语叫 canard digerateur,翻译过来是 \u0026ldquo;吃饭鸭\u0026rdquo;,一个像鸭子的机器,能吃东西然后排便。伏尔泰在 1739 年写,\u0026ldquo;如果没有吃饭鸭的声音,还有什么能提醒你法国的荣光呢?\u0026rdquo;\n一个名声很臭的例子是\u0026quot;土耳其行棋傀儡\u0026quot;,一个能下国际象棋的人形机器人,在 1770 年建造完成后,就在欧洲各地展览。好棋艺惊叹观众,像某种机械人工智能,不幸的是,这是个骗局 - 机器里有人控制。\n第一台计算机控制的机器,出现在 1940 年代晚期,这些计算机数控的机器,简称 cnc (computer numerical control)机器,可以执行一连串 程序指定的操作,精细的控制 ,让我们能生产之前很难做的物品,比如从一整块铝 加工出复杂的螺旋桨 - 这用普通机械工具很难做到,并且误差容忍度很小,无法手工加工。cnc 机器大大推进了制造业,不仅提高了制造能力和精确度 ,还降低了生产成本 - 我们之后会深入讨论这个(第 40 集)。\n第一个商业贩卖的 可编程工业机器人 叫 unimate,于 1960 年卖给通用汽车公司,它可以把压铸机做出来的热金属成品提起来,然后堆起来,机器人行业由此开始。很快,机器人开始堆叠货盘,焊接,给汽车喷漆等等。对于简单运动 - 比如机器爪子 在轨道上来回移动,可以指示它移动到特定位置,它会一直朝那个方向移动,直到到达 然后停下来,这种行为 可以用简单控制回路做。\n首先,判断机器人的位置,我们到了吗?\r没有\r那么继续前进\r再次判断位置\r我们到了吗?\r没有,所以继续前进\r我们到了吗?\r是的!\r现在可以停下来了,别问了! 因为我们在不断缩小 当前位置和目标位置的距离,这个控制回路 更准确的叫 \u0026ldquo;负反馈回路\u0026rdquo;。负反馈回路 有三个重要部分:\n首先是一个传感器,可以测量现实中的东西,比如水压,马达位置,气温,或任何你想控制的东西,根据传感器,计算和目标值相差多大,得到一个\u0026quot;错误\u0026quot;,然后\u0026quot;控制器\u0026quot;会处理这个\u0026quot;错误\u0026quot;,决定怎么减小错误,然后用泵,电机,加热元件,或其他物理组件来做出动作。在严格控制的环境中,这种简单控制回路也够用了,但在很多现实应用中,情况复杂得多,假设爪子很重,哪怕控制回路叫停了,惯性让爪子超过了预期位置,然后控制回路又开始运行,叫爪子移动回去。一个糟糕的控制回路 可能会让爪子不断来回移动,甚至永远循环。更糟糕的是,现实世界中,机器人会受到各种外力影响,比如摩擦力,风,等等。为了处理这些外力,我们需要更复杂的控制逻辑。\n一个使用广泛的机制,有控制回路和反馈机制,叫 \u0026ldquo;比例-积分-微分控制器\u0026rdquo;。这个有点绕口,所以一般简称 \u0026ldquo;pid 控制器\u0026rdquo;,它以前是机械设备,现在全是纯软件了。想象有一个机器人,端咖啡给客人,设计目标是 每秒两米的速度在顾客间穿行,这个速度是理想速度,安全又合适。当然,环境是会变化的,有时候有风,有时候有上坡下坡,以及其他影响机器人速度的因素,所以,给马达的动力要加大或减少,以保持目标速度。用机器人的速度传感器,我们可以把当前速度和目标速度画张图,pid 控制器根据这些数据,算出 3 个值,首先是 \u0026ldquo;比例值\u0026rdquo; ,就是\u0026quot;实际值\u0026quot;和\u0026quot;理想值\u0026quot;差多少,\u0026ldquo;实际值\u0026quot;可能有一定滞后,或者是实时的。之前的简单控制回路,用的就是这个值,\u0026ldquo;实际值\u0026quot;和\u0026quot;理想值\u0026quot;的差距越大,就越用力。换句话说,它是\u0026quot;比例控制\u0026quot;的。接下来,算 \u0026ldquo;积分值\u0026rdquo; ,就是一段时间内 误差的总和,比如最近几秒,帮助弥补误差,比如上坡时可能就会产生误差。如果这个值很大,说明比例控制不够,要继续用力前进。最后有 \u0026ldquo;导数值\u0026rdquo; ,是期望值与实际值之间的变化率,有助于解决 未来可能出现的错误, 有时也叫\u0026quot;预期控制\u0026rdquo;,比如前进的太快,要稍微放松一点,避免冲过头。\n这三个值会一起使用,它们有不同权重,然后用来控制系统。pid 控制器到处都是,比如汽车里的巡航控制,无人机调整螺旋桨速度,以保持水平,以及一些更奇怪的机器人,比如这个用球来平衡和移动的机器人。更高级的机器人一般需要多个控制回路同时运行,来保持机器人平衡,调整肢体位置,等等。\n之前说过,控制回路负责把机器人的属性(比如当前位置)变成期望值,你可能好奇这些值 是哪里来的,这是更高层软件的责任。软件负责做出计划 并让机器人执行动作,比如制定一条路线来绕过障碍物,或者把任务分成一步步,比如把拿起一个球,分解成一个个简单连续动作。用这些技术,机器人已经取得不少令人印象深刻的成就 - 它们潜到了海洋最深处,在火星上跑了十几年。\n但有趣的是,许多对人类来说很简单的任务,对机器人很困难: 比如两条腿走路,开门,拿东西时不要捏碎了,或是穿 t 恤,或是摸狗,这些你可能想都不用想,但有超级计算机能力的机器人却做不到。机器人研究领域在全力解决这些问题,我们前几集聊过的 人工智能,最有可能解决这些问题。例如,谷歌在进行一项实验,让一堆机器人手臂把各种东西,从一个盒子拿到另一个盒子,不断试错学习。经过数千小时的练习,机器人把错误率降低了一半。不像人类,机器人可以 24 小时全天运行,而且多个手臂同时练习,所以机器人擅长抓东西只是时间问题,但现在,小婴儿都比机器人更会抓东西。\n近年最大的突破之一,是无人驾驶汽车。如果你仔细想想,汽车没几个输入 - 只是加速减速,左转右转,难的问题是 判断车道,理解路标,预测车流,车流中穿行,留心行人和骑自行车的,以及各种障碍。车上布满了传感器,无人驾驶汽车非常依赖计算机视觉算法,我们在第 35 集讨论过。\n现在也开始出现类人机器人 - 外貌和行为像人类的机器人,不过现在两个目标都没接近(外貌和行为),因为看起来一般怪怪的,行为也怪怪的。但至少有《西部世界》可以看看。无论如何,对机器人研究者来说,把各种技术结合起来,比如人工智能,计算机视觉和自然语言处理,来让机器人越来越像人,是个诱人的目标。至于人类为什么如此着迷 做出和我们一样的机器人,你得去看《哲学速成课》。在未来好一段时间里,和人类一样的机器人 依然只能存在科幻小说里。军队也对机器人很有兴趣 - 因为机器人可以替换, 而且力量,耐力,注意力,准确性可以远超人类。拆弹机器人和无人侦察机如今很常见,但完全自主决定,全副武装的机器人也在慢慢出现,比如韩国的三星 sgr-a1 哨兵炮,有智力并且可以杀人的机器人,叫 \u0026ldquo;致命自主武器\u0026rdquo;,这种武器是复杂又棘手的问题。毫无疑问,它们可以把士兵从战场带离 挽救生命,甚至阻止战争的发生。值得注意的是 人们对炸药和核弹也说过一样的话。另一方面,我们可能会不小心创造出 无情又高效的杀人机器,没有人类般的判断力和同情心,战争的硝烟会变得更加黑暗和复杂。机器人会接受命令并高效执行,但有时人类的命令是错的。\n这场辩论会持续很长时间,而且随着机器人技术的进步,两边的辩论会越来越激烈,这也是个老话题了。\n科幻作家 艾萨克·阿西莫夫 早预见了这种危险,他在 1942 年短篇小说 runaround 中写了\u0026quot;机器人三定律\u0026rdquo;,之后又加了\u0026quot;定律 0\u0026quot;。简单说 这些定律指导机器人的行为准则 或者说道德指南,让机器人不要伤害,特别是不要伤害人类,这些规则实践起来相当不足,并且有很多模糊的地方,但阿西莫夫三定律 激发了大量科幻小说讨论和学术讨论, 如今有专门讨论机器人伦理的会议。重要的是,阿西莫夫写这些虚构规则,是为了反对 \u0026ldquo;机器人都很邪恶\u0026rdquo; 这种常见描述。他童年读的小说里,这样的场景很常见,机器人脱离控制,然后伤害甚至毁灭创造者。阿西莫夫认为 机器人有用,可靠,甚至可以让人喜爱。\n我想让你思考这种两面性。我们讨论过的许多技术,有好的一面也有坏的一面,我们要认真思考计算机的潜力和危害,来改善这个世界,而机器人最能提醒我们这一点了。\n我 们 下 周 见。\n38. 计算机心理学 i.e. psychology of computing\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n在这个系列中,我们聊的话题几乎全是计算机-比如电路和算法,毕竟这是计算机速成课,但归根结底,计算机只是给人用的工具,而人类有点。.. 乱。\n人类不是被工程师设计的,没有具体性能规格。我们一会儿是理性的,一会儿是不理性的。你有没有对导航生过气?或是漫无目的的刷维基百科?求浏览器加载快点?给扫地机器人取名?这些是人类行为!\n为了做出使用愉快的计算机,我们需要了解计算机和人类的优缺点。优秀的系统设计师在创造软件时,会运用社会心理学,认知心理学,行为心理学,感知心理学的原理。你肯定见过难用的物理界面/计算机界面 阻碍你做事,甚至糟糕到放弃使用,那个界面的\u0026quot;易用度\u0026quot;很差。\u0026ldquo;易用度\u0026quot;指的是人造物体,比如软件 达到目的的效率有多高。为了帮助人类工作,我们需要了解人类 - 怎么看,思考,反应和互动。\n举个例子,心理学家已经对 人类的视觉系统做了全面的研究,我们知道人类擅长给颜色强度排序,这里有三个颜色,你能从浅色到深色排序吗?你可以轻易做到,所以颜色强度很适合显示连续值。另一方面,人类很不擅长排序颜色,这是另一个例子,把橙色放到蓝色前面还是后面?绿色放哪里?你可能想通过光的波长排序 ,就像彩虹一样,但这样太累了。大部分人会很慢而且容易出错,由于视觉系统天生是这样,所以用不同颜色显示连续性数据,是个糟糕的选择,你得经常看表格来对比数据。 然而,如果数据没有顺序,用不同颜色就很合适,比如分类数据,也许这些看起来很明显 ,但你会惊讶有多少设计把这些基本事情搞错。\n除了视觉 ,理解人类的认知系统能帮我们设计更好的界面,比如,如果信息分块了 ,会更容易读,更容易记。分块是指把信息分成更小,更有意义的块,人类的短期记忆能记住 5 到 9 个东西,保守一点,分组一般是 5 个或更少,所以电话号码一般分块,比如 317-555-3897 。10 个连续数可能会忘,分成 3 块更好记。从计算机的角度来看,分块更费时费空间,效率更低,但这对人类更有效率 - 碰到这种抉择时,我们总是以人类优先。现在我们还是老大,暂时啦。\n界面设计用了分块, 比如下拉菜单 和带按钮的菜单栏,对电脑来说,全部挤在一起更有效率,分块浪费内存 浪费屏幕,但这样设计更容易扫视,记住和访问。界面设计中另一个重点概念是\u0026quot;直观功能\u0026rdquo;,don norman 让这个词在计算机界流行起来,根据他的说法,\u0026ldquo;直观功能 为如何操作物体提供线索,平板用来推,旋钮用来转,插槽用来插东西,[\u0026hellip;] 直观功能做的好,用户只需要看一眼就知道怎么做: 不需要图片,标签或指南来说明\u0026rdquo;。\n如果你拉过门把手打不开,然后意识到要推开才对,那么你发现了一个坏掉的\u0026quot;直观功能\u0026quot;。平板是更好的设计,因为只能推开,门是简单的东西,如果你要贴指示让人们明白怎么用,那么也许你应该重新设计。\n\u0026ldquo;直观功能\u0026quot;广泛用于图形界面,我们在第 26 集讨论过,这是图形界面比命令行更容易用的原因之一。你不用猜测屏幕上什么东西是可点的,可点的会看起来像按钮,他们弹出来,只是等着你压他们!\n我最喜欢的\u0026quot;直观功能\u0026quot;之一,是向用户表明元素是可拖动的,\u0026ldquo;滚花\u0026rdquo; - 一种视觉纹理,告诉用户哪里可以拖动,这个点子来自现实世界中的工具,和\u0026quot;直观功能\u0026quot;相关的一个心理学概念是 \u0026ldquo;认出与回想\u0026rdquo;。如果你考过试,肯定感受过这个,这就是为什么选择题比填空题容易。一般来说,用感觉来触发记忆会容易得多,比如文字,图片或声音,所以我们用图标代表功能 - 比如\u0026quot;垃圾桶\u0026quot;图标 代表里面放着被删除的文件。我们不用去回想图标的功能是什么,只要能认出来就行了,比命令行好得多,命令行得依靠记忆来输命令,到底是输入\u0026quot;删除\u0026quot;\u0026ldquo;移除\u0026quot;\u0026ldquo;垃圾\u0026quot;还是\u0026quot;射出\u0026rdquo;?可能是任何命令! 顺带一说,在 linux 里删除文件的命令是 \u0026ldquo;rm\u0026rdquo; 。\n回到正题,让所有菜单选项好找好记,有时候意味着用的时候会慢一些,这与另一个心理学概念冲突:\u0026ldquo;专业知识\u0026rdquo;。当你用界面熟悉之后,速度会更快一些,建立如何高效完成事情的\u0026quot;心理模型\u0026rdquo;,所以 好的界面应该提供多种方法来实现目标。一个好例子是复制粘贴,可以在\u0026quot;编辑\u0026quot;的下拉菜单中找到,也可以用快捷键,一种适合新手,一种适合专家,两者都不耽误,鱼和熊掌兼得!\n除了让人类做事更高效,我们也希望电脑能有一点情商,能根据用户的状态做出合适地反应,能根据用户的状态做出合适地反应,让使用电脑更加愉快。rosalind picard 在 1995 年关于\u0026quot;情感计算\u0026quot;的论文中,阐述了这一愿景,这篇论文开创了心理学,社会科学和计算机科学的跨学科结合,促进了让计算机理解人类情感的研究,这很重要,因为情绪会影响日常活动,比如学习,沟通和决策。情感系统会用传感器,录声音,录像(你的脸)以及生物指标,比如出汗和心率,得到的数据和计算模型结合使用。模型会写明人类如何表达情感,怎么是快乐 ,怎么是沮丧,以及社交状态,比如友谊和信任。模型会估算用户的情绪,以及怎样以最好的回应用户,以达到目标,比如让用户冷静下来,建立信任,或帮忙完成作业。\nfacebook 在 2012 年进行了一项\u0026quot;影响用户\u0026quot;的研究,数据科学家在一个星期内,修改了很多用户 时间线上显示的内容,有些人会看到更多积极向上的内容,有些人会看到更多负面消极的内容。研究人员分析了那一周内人们的发帖,发现看到积极向上内容的用户,发的帖子往往更正面。另一方面,看到负面内容的用户,发的内容也更负面。显然,facebook 和其他网站向你展示的内容,绝对会对你有影响。作为信息的守门人,这是巨大的机会 ,同时也是责任,研究结果相当有争议性。而且它还产生了一个有趣的问题: 计算机程序如何回应人类?\n如果用户的情绪比较负面,也许电脑不应该以一种烦人的 \u0026ldquo;你要振作起来呀\u0026rdquo; 的态度回答问题。或者,也许电脑应该试着积极正面的回应用户,即使这有点尴尬。什么行为是\u0026quot;正确的\u0026rdquo;,是个开放性的研究问题。\n既然说到 facebook,这是一个\u0026quot;以计算机为媒介沟通\u0026quot;的好例子,简称 \u0026ldquo;cmc\u0026rdquo;(computer-mediated communication),也是一个很大的研究领域。这包括同步通信 - 所有参与者同时在线进行视频通话,以及异步通信 - 比如推特,邮件,短信,人们可以随时随地回复信息。研究人员还研究用户怎么用表情包,怎么轮换发言,以及用不同沟通渠道时,用词有什么区别。\n一个有趣的发现是,比起面对面沟通,人们更愿意在网上透露自己的信息。所以如果想知道用户 真正花了多少小时看\u0026quot;大英烘培大赛\u0026quot;(电视节目),比起做个带脸的虚拟助理 做 聊天机器人 是个更好的选择。心理学研究也表明,如果想说服,讲课,或引起注意 眼神注视非常重要。在谈话时看着别人叫 相互凝视,这被证明可以促进参与感 帮助实现谈话目标,不管是学习,交朋友,还是谈生意。在录像讲座中,老师很少直视相机, 一般是看在场学生,对他们没问题,但这会让在线看视频的人没什么参与感,为此,研究人员开发了计算机视觉和图形软件 来纠正头部和眼睛,视频时会觉得对方在直视摄像头,看着他们,这叫\u0026quot;增强凝视\u0026quot;。类似技术也用于视频会议,纠正摄像头位置,因为摄像头几乎总在屏幕上方,因为你一般会盯着屏幕上的另一方 ,而不是盯着摄像头,所以视频里看起来像在向下看。没有相互凝视 - 这会导致各种不幸的副作用,比如权力不平衡,幸运的是,可以用软件修正,看起来像在凝视着对方的眼睛。人类也喜欢\u0026quot;拟人化\u0026quot;的物体,对计算机也不例外,特别是会动的计算机,比如上集说的机器人。\n在过去一个世纪,除了工业用途机器人,有越来越多机器人用于医疗,教育和娱乐 。它们经常和人类互动 - 人机交互,简称 hri(human-robot interaction) - 是一个研究人类和机器人交互的领域,比如人类如何感受 机器人的不同形式和不同行为,或是机器人如何明白人类暗示来社交,而不是尴尬的互动。\n正如上集说的,我们有追求。把机器人的外表和行为,做得尽可能像人一样。\n工程师在 1940 1950 年代刚开始做机器人时,看起来完全不像人,是完完全全的工业机器。随着时间的推移,工程师越来越擅长做类人机器人,它们有头,而且用两条腿走路,但它们做不到伪装成人类去餐馆点餐。随着机器人可以做得越来越像人类,用人造眼球代替摄像头,用人工肌肉盖住金属骨架,事情会开始变得有些奇怪,引起一种怪异不安的感觉,这个\u0026quot;几乎像人类\u0026quot;和\u0026quot;真的人类\u0026quot;之间的小曲线,叫 \u0026ldquo;恐怖谷\u0026rdquo;。\n对于机器人是否应该有人类一样的行为,也存在争议。很多证据表明,即使机器人的行为不像人类,人类也会用社交习俗对待它们,而当机器人违反习俗时 - 比如插队或踩了脚不道歉 人们会很生气!毫无疑问,心理学+计算机科学是强大的组合,可以影响日常生活的巨大潜力,这也带来了很多开放式问题,比如你可能会对计算机撒谎,但计算机应不应该对你撒谎?如果撒谎能让你更高效更快乐呢?或社交媒体公司 是否应该精心挑选展示给你的内容,让你在网站上多待一会儿,买更多东西?\n顺带一说,他们的确有这样做!!!\n这类道德问题不容易回答,但心理学至少可以帮助我们理解不同选择 带来的影响和意义。但从积极的方面来说,了解设计背后的心理学,能增加易用性,让更多人可以明白和使用电脑,如今计算机比以往更加直观,线上会议和虚拟教室的体验越来越好。随着机器人技术不断提高,互动也会越来越舒适。另外,感谢心理学,让我们能分享对\u0026quot;滚花\u0026quot;的热爱。\n我们下周见。\n39. 教育科技 i.e. educational technology\n(。・∀・)ノ゙嗨,我是 carrie anne ,欢迎收看计算机科学速成课!\\n\n计算机带来的最大改变之一 , 是信息的创造和传播能力。目前有 13 亿个网站在互联网上,仅维基百科就有 500 万篇英文文章,涵盖从\u0026quot;1518 年的舞蹈瘟疫\u0026quot;,到\u0026quot;正确的纸卷方向\u0026quot;。每天,google 提供 40 亿次搜索来访问这些信息,youtube 上每分钟有 350 万个视频被观看,每分钟用户上传 400 小时的新视频,很多观看量都是 gangnam style 和 despacito,但剩下的 大部分是教育型内容,就像你现在看的这个。\n如今只要手机上点几下 就能访问到这些宝藏,任何时间,任何地点,但能获取到信息和学习不是一回事。\n先说清楚,我们 crash course 喜欢互动式课堂学习,课上提问,以及上手实践,它们是很棒的学习途径,但我们也相信教育型技术在课内课外带来的帮助。今天我们要在这个教育型视频里 聊教育型科技,具体讲解计算机怎么帮助我们学习。从纸和笔 到用机器学习的智能系统,科技几千年来一直在辅助教育,甚至早期人类 在洞穴里画狩猎场景也是为了后代。远距离教育一直推动着教育科技的发展,例如公元 50 年左右,圣保罗就发书信 给亚洲设立的新教堂提供宗教课程,从那以后,有几大技术浪潮,自称要改变教育,从广播和电视,到 dvd 和光碟。事实上,在 1913 年 托马斯·爱迪生 预测说,\u0026ldquo;书籍很快会过时,用影片来教授所有知识是可能的,学校体系将在未来十年彻底改变\u0026rdquo;。当然,他的预测没有成真,但发布教育视频变得越来越流行。在讨论教育技术可以帮你做什么之前,有研究表明 - 有些简单事情 ,可以显著提高学习效率:\n把速度调整到适合你,youtube 的速度设置在右下角,让你能理解视频 有足够的时间思考; 暂停!在困难的部分暂停,问自己一些问题,看能不能回答,或想想视频接下来可能讲什么 然后继续播放,看猜对没有; 做视频中的提供的练习。 即使不是程序员,你也可以在纸上写伪代码,或试试学编程,这些主动学习的技巧已被证明 ,可以把学习效率提升 10 倍或以上。如果想学学习技巧,有整个系列专门讲这个。\n把高质量教育内容做成视频传播 ,在过去一个世纪吸引了很多人,这个老想法的新化身,以\u0026quot;大型开放式在线课程\u0026quot;(mooc,massive open online courses)的形式出现,纽约时报宣称 2012 年是 mooc 年!很多早期视频 直接录制著名教授上课,有段时间,有些人以为大学要终结了。不管你是担心还是开心,这暂时还没成为现实,现在热度也淡去了,这可能是因为加大规模时, 同时教百万名学生,但老师数量很少,甚至完全没有老师 - 会遇到很多问题。幸运的是,这引起了计算机科学家,或具体一点 \u0026ldquo;教育科技家\u0026quot;的兴趣,他们在想办法解决这些问题,比如,为了有效学习,学生要及时获得反馈。但如果有几百万学生,只有一名老师,怎么提供好的反馈?一个老师怎么给一百万份作业打成绩?为了解决问题,很多时候需要把科技和人类都用上,一种有用 但有些争议的做法是学生互相之间提供反馈。不幸的是,学生一般做不好,他们既不是专家也不是老师,但我们可以用技术来帮助他们,比如通过算法,从数百万个选择里 匹配出最完美的学习伙伴,另外,有些部分可以机器打分,剩下的让人类打分,例如,给 sat 写作部分打分的电脑算法,已被证实和人工打分一样准确,还有些算法提供个性化学习体验,类似于 netflix 的电影推荐 或 google 的个性化搜索结果,为了个性化推荐,软件需要了解用户知道什么,不知道什么,在正确的时间提供正确的资料。\n让用户练习没理解的难的部分,而不是给出用户已经学会的内容,这种系统一般用 ai 实现,泛称叫法是\u0026quot;智能辅导系统\u0026rdquo;。我们现在讲一个假想的辅导系统,假设学生在这个假想的辅导系统中,研究一个代数问题,正确的下一步是两边-7,我们可以用 \u0026ldquo;判断规则\u0026rdquo; 来表示这一步,用 if-then 语句来描述,伪代码是:\n*如果* 变量和常数在同一边\r*那么* 两侧都减去这个常数\r\u0026#34;判断规则\u0026#34; 酷的地方是也可以用来代表学生的常犯错误\r这些\u0026#34;判断规则\u0026#34;叫\u0026#34;错误规则\u0026#34;\r例如,学生可能不去减常数,而是去减系数\r这不行! 学生做完一个步骤后可能触发多个\u0026quot;判断规则\u0026quot;,系统不能完全弄清 是什么原因让学生选了那个答案,所以\u0026quot;判断规则\u0026quot;会和算法结合使用,判断可能原因,让学生得到有用反馈。\n\u0026ldquo;判断规则\u0026rdquo;+选择算法,组合在一起成为 \u0026ldquo;域模型\u0026rdquo;,它给知识,解决步骤和一门学科 ,比如代数,用一种\u0026quot;正式写法\u0026quot;来表示。域模型可以用来 帮助学习者解决特定问题,但它无法带着学习者 以正确顺序搞定整个学科该上的所有课程,因为域模型不记录进度,因此智能辅导系统 负责创建和维护学生模型 - 记录学生已经掌握的判断规则,以及还需练习的生疏部分,这正是个性化辅导系统需要的。\n听起来好像不难,但只靠学生对一些问题的回答,来弄清学生知道什么,不知道什么,是很大的挑战。\u0026ldquo;贝叶斯知识追踪\u0026rdquo; 常用来解决这个问题,这个算法把学生的知识 当成一组隐藏变量,这些变量的值,对外部是不可见的,比如我们的软件。\n这在现实中也是一样的,老师无法知道 学生是否完全掌握了某个知识点,老师会出考题,测试学生能否答对,同样,\u0026ldquo;贝叶斯知识追踪\u0026rdquo; 会看学生答题的正确度,更新学生掌握程度的估算值,它会记录四个概率:首先是 \u0026ldquo;学生已经学会的概率\u0026rdquo;,比如从代数方程的两边减去常数,假设学生正确将两边-7,做对了,我们可以假设她知道怎么做,但也有可能她是瞎蒙的,没有真的学会怎么解决问题,这叫 \u0026ldquo;瞎猜的概率\u0026rdquo;。类似的,如果学生答错了,你可能会假设她不会做,但她可能知道答案,只是不小心犯了个错,这叫 \u0026ldquo;失误的概率\u0026rdquo;。最后一个概率,是学生一开始不会做,但是在解决问题的过程中,学会了怎么做,这叫 \u0026ldquo;做题过程中学会的概率\u0026rdquo;。有一组方程,会用这四个概率,更新学生模型,对学生应该学会的每项技能进行持续评估。\n第一个等式问:学生已经知道某技能的概率是多少?等式里有 \u0026ldquo;之前已经学会的概率\u0026quot;和\u0026quot;做题过程中学会的概率\u0026rdquo;,就像老师一样,\u0026ldquo;之前已经学会的概率\u0026rdquo;,取决于学生回答问题正确与否,回答正确和错误分别有 2 个公式,算出结果之后,我们把结果放到第一个方程,更新\u0026quot;之前已经学会的概率\u0026quot;,然后存到学生模型里。\n虽然存在其他方法,但\u0026quot;智能辅导系统\u0026quot;通常用 贝叶斯知识追踪,让学生练习技能,直到掌握。为了高效做到这点,软件要选择合适的问题呈现给学生,让学生学,这叫:自适应式程序,个性化算法的形式之一,但我们的例子只是一个学生的数据。\n现在有 app 或网站,让教师和研究人员 收集上百万学习者的数据,从数据中可以发现常见错误一般哪里难倒学生,除了学生的回答,还可以看回答前暂停了多久,哪个部分加速视频,以及学生如何在论坛和其他人互动,这个领域叫 \u0026ldquo;教育数据挖掘\u0026rdquo;,它能用上学生所有的\u0026quot;捂脸\u0026quot;和\u0026quot;啊哈\u0026quot;时刻,帮助改善未来的个性化学习。\n谈到未来,教育技术人员经常从科幻小说中获得灵感,具体来说,neal stephenson 的\u0026quot;钻石时代\u0026quot;这本书激励了很多研究人员,里面说一个年轻女孩从书中学习,书中有一些虚拟助手会和她互动,教她知识,这些助手和她一起成长,直到她学会了什么,以及感觉如何,给她正确的反馈和支持,帮助她学习。如今 有非科幻小说研究者,比如 贾斯汀卡塞尔,在制作虚拟教学助手,助手可以\u0026quot;像人类一样沟通, 有人类一样的行为,在陪伴过程中和学习者建立信任,相处融洽,甚至和人类学生成为朋友\u0026quot;。\n2040 年的\u0026quot;速成课\u0026quot; 可能会有一个 john green ai,活在你的 iphone 30 上,教育科技和设备如今在逐渐扩展到笔记本和台式电脑之外,比如巨大桌面设备,让学生可以团队合作,以及小型移动设备,让学生路上也能学习。\n\u0026ldquo;虚拟现实\u0026quot;和\u0026quot;增强现实\u0026quot;也让人们兴奋不已,它们可以为学习者提供全新的体验 - 深潜海洋,探索太空,漫游人体,或是和现实中难以遇见的生物互动。如果猜想遥远的未来,教育可能会完全消失,直接在大脑层面进行,把新技能直接下载到大脑,这看起来可能很遥远,但科学家们已经在摸索 - 比如,仅仅通过检测大脑信号,得知某人是否知道什么。\n这带来了一个有趣的问题:如果我们可以把东西下载到大脑里,我们能不能上传大脑里的东西?\n下周的最后一集,我们会讨论计算的未来。\n到时见。\n40. 奇点,天网,计算机的未来 i.e. the singularity, skynet, and the future of computing\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n我们到了 最后一集!\n如果你看了整个系列,希望你对计算机影响的深度和广度 有全新的认知和欣赏。难以相信 我们从简单的晶体管和逻辑门开始,一直到计算机视觉,机器学习,机器人以及更多。\n我们站在巨人的肩膀上 - charles 、babbage 、ada 、lovelac 、herman 、hollerith 、alan turing 、j. presper eckert、 grace hopper 、 ivan sutherland douglas 、engelbart 、vannevar 、bush (memex) 、berners-lee (万维网) 、bill gates (微软)、steve wozniak (苹果),和许多其他先驱。\n我最大的希望是 这些视频能激励你 去了解这些东西如何影响你的人生,甚至开始学编程,或找一份计算机职业,这很棒!这是未来的技能!\n我在第一集说过,计算机科学不是魔法但它有点像魔法,学习使用电脑和编程,是 21 世纪的巫术,只不过用的不是咒语 ,而是代码。懂得运用的人,能创造出伟大的东西,不仅改善自己的生活,还有当地社区乃至整体人类。计算机会随处可见 - 不仅是放在桌上 ,带在包里,而是在所有可想象的东西里 - 厨房用具里,墙里,食物里,编织进衣服里,在你的血液里,这是\u0026quot;普适计算\u0026quot;的愿景。\n从某种角度来讲, 它已经来临了而换一个角度 , 还要几十年。有些人把这种未来看成 反乌托邦,到处都有监视器,有无数东西想吸引我们的注意力,但 1990 年代提出这个想法的 马克·维泽尔,看到了非常不同的潜力:\u0026quot;[五十] 年来,大多数界面和计算机设计,都是朝\u0026quot;戏剧性\u0026quot;方向前进,想把计算机做得超好,让人一刻也不想离开。另一条少有人走的路 是\u0026quot;无形\u0026quot;的,把计算机整合到所有东西里 ,用的时候很自然完全注意不到。最厉害的科技是看不见的科技,它们融入到日常生活的每一部分 ,直到无法区分\u0026rdquo;。\n如今我们还没达到这样 - 人们在电脑前连续坐好几小时,吃晚餐被手机推送通知打扰,但它可以描述计算的未来本系列最后一个主题,人们思考计算机的未来时,经常会直接想到人工智能。毫无疑问,接下来几十年人工智能会有巨大进步,但不是所有东西都要做成 ai ,或需要 ai,车有自动驾驶 ai,但门锁依然会很简单。人工智能可能只是增强现有设备,比如汽车,ai 带来了一个全新的产品种类,刚出现电力时也是这样,灯泡取代了蜡烛。但电气化也导致上百种新的电动小工具诞生。当然 我们如今仍然有蜡烛。最可能的情况是 ai 变成 计算机科学家手中的另一门新工具,但真正让人深思和担忧的是,人工智能是否会超越人类智能?\n这个问题很难 有多方面原因,比如 \u0026ldquo;智能的准确定义是什么?\u0026rdquo; 一方面,有会开车的计算机,几秒就能识别歌的 app 。翻译几十种语言,还称霸了一些游戏,比如象棋,知识竞答和围棋,听起来很聪明!但另一方面,计算机连一些简单事情都做不了,比如走楼梯,叠衣服,在鸡尾酒派对和人聊天,喂饱自己。人工智能成长到和人类一样通用,还有很长的路,因为\u0026quot;智能\u0026quot;是难以量化的指标,人们更喜欢用处理能力来区分,但这种衡量智能的方法比较\u0026quot;以计算为中心\u0026quot;,但如果把视频中出现过的电脑和处理器 画张图,可以看到 如今的计算能力粗略等同于一只老鼠。公平点说,老鼠也不会叠衣服,但如果真的会叠 ,就太可爱了。\n人类的计算能力在这儿,多 10 的 5 次方,也就是比如今电脑强 10 万倍。听起来差距很大,但按如今的发展速度,也许十几年就可以赶上了。虽然现在处理器的速度不再按摩尔定律增长了,我们在第 17 集讨论过,假设趋势继续保持下去,在本世纪结束前,计算机的处理能力/智能 会比全人类加起来还多。然后人的参与会越来越少,人工超级智能会开始改造自己,智能科技的失控性发展叫 \u0026ldquo;奇点\u0026rdquo;。\n第 10 集 约翰·冯·诺伊曼 最早用这个词,他说:\u0026ldquo;越来越快的技术发展速度和人类生活方式的改变,看起来会接近人类历史中某些重要的奇点,这个势头不会永远继续下去\u0026rdquo;。冯诺依曼在 1950 年代说的这话,那时计算机比现在慢得多,六十年后的今天,奇点仍然在遥远的地平线上。一些专家认为 发展趋势会更平缓一些,更像是 s 型,而不是指数型,而随着复杂度增加,进步会越来越难。微软联合创始人 保罗·艾伦 叫这个\u0026quot;复杂度刹车\u0026quot;,但当作思维练习,我们假设 超智能计算机会出现。这对人类意味着什么,是个讨论激烈的话题,有些人迫不及待,有些人则努力阻止它,最直接的影响可能是\u0026quot;技术性失业\u0026quot;。很多工作被计算机,比如 ai 和机器人,给代替掉了,它们的效率更高,成本更低。虽然计算机出现没多久,但\u0026quot;技术性失业\u0026quot;不是新事,还记得第 10 集里 雅卡尔的织布机 吗?它让 1800 年代的纺织工人失业,导致了骚乱,当时美国和欧洲 大部分人都是农民,如今农民占人口比例 \u0026lt;5%,因为有合成肥料和拖拉机等等技术。时间更近一些的例子是\u0026quot;电话接线员\u0026quot;,在 1960 年被自动接线板代替了,还有 1980 年代的\u0026quot;机器喷漆臂\u0026quot;替代了人工喷漆,这样的例子还有很多。一方面,因为自动化失去了工作,另一方面,我们有大量产品,衣服,食物,自行车,玩具等,因为可以廉价生产,但专家认为人工智能,机器人 以及更广义的计算,比之前更有破坏性。\n工作可以用两个维度概括,首先,手工型工作,比如组装玩具,或思维型工作 - 比如选股票,还有重复性工作,一遍遍做相同的事,或非重复性,需要创造性的解决问题。我们知道 重复性手工工作,可以让机器自动化,现在有些已经替代了,剩下的在逐渐替代。让人担心的是\u0026quot;非重复性手工型工作\u0026quot;,比如厨师,服务员,保安。思维型工作也一样,比如客服,收银员,银行柜员和办公室助理。剩下一个暂时比较安全的象限,非重复性思维型工作,包括教师和艺术家,小说家和律师,医生和科学家。这类工作占美国劳动力大概 40% ,意味着剩下 60%工作容易受自动化影响。有人认为这种规模的技术失业是前所未有的,会导致灾难性的后果,大部分人会失业,其他人则认为很好,让人们从无聊工作解脱,去做更好的工作,同时享受更高生活水平,有更多食物和物品,都是计算机和机器人生产的。\n没人知道未来到底会怎样,但如果历史有指导意义,长远看 一切会归于平静。毕竟,现在没人嚷嚷着让 90%的人 回归耕田和纺织,政界在讨论的棘手问题是怎么处理数百万人突然失业 造成的短期经济混乱。\n除了工作,计算机很可能会改变我们的身体,举个例子,未来学家 ray kurzweil 认为,\u0026ldquo;奇点会让我们超越 肉体和大脑的局限性,我们能掌控自己的命运,可以想活多久活多久 。 我们能完全理解并扩展大脑思维,超人类主义者认为会出现\u0026quot;改造人\u0026rdquo;,人类和科技融合在一起,增强智力和身体。如今已经有脑电接口了,而 google glass 和 微软 hololens 这样的穿戴式计算机 也在模糊这条界线,也有人预见到\u0026quot;数字永生\u0026quot;。jaron lanier 的说法是,\u0026ldquo;人类的肉体死去,意识上传到计算机\u0026rdquo;,从生物体变成数字体 可能是下一次进化跨越。\n一层新的抽象!\n其他人则预测,人类大体会保持原样,但超智能电脑会照顾我们,帮我们管农场,治病,指挥机器人收垃圾,建房子以及很多其他事情,让我们在这个可爱蓝点上(地球)好好享受。另一些人对 ai 持怀疑态度 - 为什么超级人工智能 会费时间照顾我们?人类不也没照顾蚂蚁吗?也许会像许多科幻电影一样,和计算机开战。我们无法知道未来到底会怎样,但现在已经有相关讨论了,这非常好。所以等这些技术出现后,我们可以更好地计划,不论你把计算机视为未来的朋友或敌人,更有可能的是,它们的存在时间会超过人类。许多未来学家和科幻作家猜测,机器人会去太空殖民,无视时间,辐射 以及一些其他让人类难以长时间太空旅行的因素。亿万年后太阳燃尽 ,地球成为星尘 ,也许我们的机器人孩子会继续努力探索宇宙每一个角落,以纪念它们的父母,同时让宇宙变得更好,大胆探索无人深空。\n与此同时,计算机还有很长的路要走,计算机科学家们在努力推进过去 40 集谈到的话题。在接下来的十几年,vr 和 ar,无人驾驶车,无人机,可穿戴计算机,和服务型机器人 会变得主流。互联网会继续诞生新服务,在线看新媒体,用新方式连接人们,会出现新的编程语言和范例,帮助创造令人惊叹的新软件,而新硬件能让复杂运算快如闪电 ,比如神经网络和 3d 图形。个人电脑也会创新,不像过去 40 年着重宣传 \u0026ldquo;桌面\u0026rdquo; 电脑,而是变成无处不在的虚拟助手。\n这个系列,我们还有很多话题没谈,比如加密货币,无线通讯,3d 打印,生物信息学和量子计算,我们正处于计算机的黄金时代,有很多事情在发生,全部总结是不可能的,但最重要的是 你可以学习计算机 成为这个惊人转型的一部分,把世界变得更好!\n感谢收看!\n结语 真的好长,也很有趣。知其先后,以预未来。\n","date":"2022-08-29","permalink":"https://loveminimal.github.io/posts/cscc/16/","summary":"\u003cblockquote\u003e\n\u003cp\u003e好吧,内容不少,为了后续插入图片之后 ,页面太大,我们这里拆分到两个页面中。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc16 - 其他 2"},{"content":" 后续章节为概念性章节,统一汇总在该章节内。\n15. 阿兰·图灵 i.e. alan turing\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n前几集我们聊了基础,比如函数,算法和数据结构,今天,我们来看一位对计算机理论 贡献巨大的人,计算机科学之父,长得不怎么像本尼的阿兰·图灵。\n\u0026gt; 阿兰·图灵\n阿兰·马蒂森·图灵 于 1921 年出生在伦敦, 从小就表现出惊人数学和科学能力。他对计算机科学的建树始于 1935 年,当时他是剑桥国王学院的硕士生,他开始解决德国数学家大卫·希尔伯特提出的问题 - 叫 entscheidungsproblem (德语),即\u0026quot;可判定性问题\u0026quot;: 是否存在一种算法,输入正式逻辑语句,输出准确的\u0026quot;是\u0026quot;或\u0026quot;否\u0026quot;答案?\n如果这样的算法存在,可以回答比如 \u0026ldquo;是否有一个数大于所有数\u0026rdquo;?不,没有。我们知道答案,但有很多其他数学问题,我们想知道答案,所以如果这种算法存在,我们想知道。\n美国数学家阿隆佐·丘奇于 1935 年 首先提出解决方法,开发了一个叫\u0026quot;lambda 算子\u0026quot;的数学表达系统,证明了这样的算法不存在。虽然\u0026quot;lambda 算子\u0026quot;能表示任何计算,但它使用的数学技巧难以理解和使用。同时在大西洋另一边,阿兰·图灵 想出了自己的办法来解决\u0026quot;可判定性问题\u0026quot;,提出了一种假想的计算机,现在叫\u0026quot;图灵机\u0026quot;。\n图灵机提供了简单又强大的数学计算模型,虽然用的数学不一样,但图灵机的计算能力和 lambda 算子一样,同时因为图灵机更简单,所以在新兴的计算机领域更受欢迎。因为它如此简单,我现在就给你解释。\n图灵机是一台理论计算设备,还有一个状态变量,保存当前状态,还有一组规则,描述机器做什么,规则是根据当前状态+读写头看到的符号,决定机器做什么,结果可能是在纸带写入一个符号,或改变状态,或把读写头移动一格,或执行这些动作的组合。\n为了更好理解,讲个简单例子:让图灵机读一个以零结尾的字符串,并计算 1 的出现次数是不是偶数。如果是,在纸带上写一个 1 ;如果不是,在纸带上写一个 0 。\n首先要定义\u0026quot;图灵机\u0026quot;的规则,如果当前状态是\u0026quot;偶数\u0026quot;, 当前符号是 1 ,那么把状态更新为\u0026quot;奇数\u0026quot;,把读写头向右移动;如果当前状态为偶数,当前符号是 0 ,意味着到了字符串结尾 。那么在纸带上写一个 1,并且把状态改成停机 (halt),状态改为\u0026quot;停机\u0026quot; 是因为图灵机已完成计算。但我们还需要 2 条规则,来处理状态为奇数的情况,一条处理奇数+纸带是 0 的情况, 一条处理奇数+纸带是 1 的情况,最后,要决定机器的初始状态,这里定成\u0026quot;偶数\u0026quot;。\n初始状态:偶\r偶 + 1 - 状态更新为奇数,读写头向右移动一格\n偶 + 0 - 状态更新为停机,在纸带上写一个 1\n奇 + 1 - 状态更新为偶数,读写头向右移动一格\n奇 + 0 - 在纸带上写一个 0\n💡 真的是需要天赋的……\n定义好了起始状态+规则,就像写好了程序,现在可以输入了。\n假设把 110 放在纸带上,有两个 1,是偶数,注意,规则只让读写头向右移动,其他部分无关紧要,为了简单所以留空。\n\u0026ldquo;图灵机\u0026quot;准备好了,开始吧。\n机器起始状态为\u0026quot;偶数\u0026rdquo;,看到的第一个数是 1,符合最上面那条规则,所以执行对应的步骤 - 把状态更新到\u0026quot;奇数\u0026quot;, 读写头向右移动一格;然后又看到 1, 但机器状态是\u0026quot;奇数\u0026quot;,所以执行第三条规则,使机器状态变回\u0026quot;偶数\u0026quot;,读写头向右移动一格;现在看到 0,并且机器状态是偶数,所以执行第二条规则,在纸带上写 1,表示\u0026quot;真\u0026quot; 的确有偶数个 1,然后机器停机。\n这就是图灵机的原理,很简单对吧?你可能想知道有什么大不了的。\n图灵证明了这个简单假想机器。如果有足够时间和内存,可以执行任何计算。它是一台通用计算机!刚才的程序就是个简单例子,只要有足够的规则,状态和纸带,可以创造任何东西 - 浏览器,魔兽世界 任何东西!当然 这样做效率很低,但理论上可行,所以图灵机是很强大的计算模型。\n事实上,就可计算和不可计算而言,没有计算机比图灵机更强大。和图灵机一样强大的,叫 \u0026ldquo;图灵完备\u0026rdquo;。每个现代计算系统,比如笔记本电脑,智能手机,甚至微波炉和恒温器内部的小电脑,都是\u0026quot;图灵完备\u0026quot;的。\n为了回答可判定性问题,他把图灵机用于一个有趣计算问题:\u0026ldquo;停机问题\u0026rdquo;。简单说就是,\u0026ldquo;给定图灵机描述和输入纸带,是否有算法可以确定机器会永远算下去还是到某一点会停机?\u0026rdquo; 我们知道输入 110,图灵机会停机,因为刚做过这个例子,它最后停机了,但如果是更复杂的问题呢?有没有办法在不执行的情况,弄清会不会停机?一些程序可能要运行好几年,所以在运行前知道 会不会出结果很有用,否则就要一直等啊等,忧虑到底会不会出结果,当几十年后变老了,再按强制结束。好悲伤!\n图灵通过一个巧妙逻辑矛盾证明了停机问题是无法解决的。\n我们来看看他的推理,想象有一个假想图灵机, 输入:问题的描述 + 纸带的数据,输出 yes 代表会\u0026quot;停机\u0026quot;,输出 no 代表不会。我要给这台机器一个有趣的名字叫 h,来自\u0026quot;停机\u0026quot;的第一个字母,不用担心它具体怎么工作,假设这样的机器存在就好,毕竟重点是推论。图灵推理说: 如果有个程序, h 无法判断是否会\u0026quot;停机\u0026quot;,意味着\u0026quot;停机问题\u0026quot;无法解决。为了找到这样的程序,图灵用 h 设计了另一个图灵机。如果 h 说程序会\u0026quot;停机\u0026quot;,那么新机器会永远运行(即不会停机)。如果 h 的结果为 no,代表不会停机,那么让新机器输出 no,然后\u0026quot;停机\u0026quot;。实质上是一台和 h 输出相反的机器,如果程序不停机,就停机,如果程序停机,就永远运行下去。我们还需要在机器前面加一个分离器,让机器只接收一个输入, 这个输入既是程序,也是输入,我们把这台新机器叫异魔😈。目前为止,这个机器不难理解,但接下来马上会变复杂,会有点难懂。如果把异魔的描述,作为本身的输入会怎样?意味着在问 h ,当异魔的输入是自己时会怎样,但如果 h 说异魔会停机,那么异魔会进入无限循环,因此不会停机,如果 h 说异魔不会停机,那么异魔会输出 no 然后停机,所以 h 不能正确判定停机问题,因为没有答案。这是一个悖论!意味着\u0026quot;停机问题\u0026quot;不能用图灵机解决!\n还记得刚刚说: 图灵证明了图灵机可以实现任何计算。\u0026ldquo;停机问题\u0026quot;证明了,不是所有问题都能用计算解决。哇,好难理解!我都可能要再看一遍。\n长话短说,丘奇和图灵证明了计算机的能力有极限。无论有多少时间或内存,有些问题是计算机无法解决的。丘奇和图灵证明了计算是有极限的,起步了可计算性理论,现在叫\u0026quot;丘奇-图灵论题\u0026rdquo;。\n当时是 1936 年,图灵只有 24 岁,他的职业生涯才刚刚开始。从 1936 年到 1938 年 在丘奇指导下,他在普林斯顿拿到博士学位,毕业后回到剑桥。1939 年后不久,英国卷入第二次世界大战,图灵的才能很快被投入战争。事实上,在战争开始前一年,他已经在英国政府的密码破译学校兼职 - 位于\u0026quot;布莱切利园\u0026quot;的一个密码破译组织。他的工作内容之一是破解德国的通信加密,特别是\u0026quot;英格玛机\u0026quot;加密的信息,简单说,英格玛机会加密明文,如果输入字母 h-e-l-l-o ,机器输出 x-w-d-b-j ,这个过程叫\u0026quot;加密\u0026quot;,文字不是随便打乱的。加密由\u0026quot;英格玛机\u0026quot;顶部的齿轮组合决定,每个齿轮有 26 个可能位置,机器前面还有插板,可以将两个字母互换,总共有上十亿种可能,如果你有\u0026quot;英格玛机\u0026quot;,并且知道正确的齿轮和插头设置,输入 x-w-d-b-j,机器会输出 hello ,解密了这条消息。\n当然,德军不会把机器设置发到微博上,盟军必须自己破译密码,有数十亿种组合,根本没法手工尝试所有组合。幸运的是,英格玛机和操作员不是完美的,一个大缺陷是:字母加密后绝不会是自己,h 加密后绝对不是 h 。图灵接着之前波兰破译专家的成果继续工作,设计了一个机电计算机,叫 bombe,利用了这个缺陷,它对加密消息尝试多种组合。如果发现字母解密后和原先一样,我们知道英格玛机决不会这么做,这个组合会被跳过,接着试另一个组合。bombe 大幅减少了搜索量,让破译人员把精力花在更有可能的组合,比如在解码文本中找常见的德语单词。德国人时不时会怀疑有人在破解,然后升级英格玛机,比如加一个齿轮,创造更多可能组合,他们甚至还做了全新的加密机。整个战争期间,图灵和同事在布莱切利园努力破解加密,解密得到的德国情报,为盟军赢得了很多优势,些史学家认为他们把战争减短了好几年。战后,图灵回到学术界,为许多早期计算机工作做出贡献,比如曼彻斯特 1 号,一个早期有影响力的存储程序计算机。但他最有名的战后贡献是\u0026quot;人工智能\u0026quot;,这个领域很新,直到 1956 年才有名字,这个话题很大,以后再谈(第 34 集)。\n1950 年,图灵设想了未来的计算机,拥有和人类一样的智力,或至少难以区分。图灵提出,如果计算机能欺骗人类相信它是人类,才算是智能,这成了智能测试的基础,如今叫\u0026quot;图灵测试\u0026quot;。想像你在和两个人沟通,不用嘴或面对面,而是来回发消息,可以问任何问题,然后会收到回答,但其中一个是计算机,如果你分不出哪个是人类,哪个是计算机,那么计算机就通过了图灵测试。这个测试的现代版叫 \u0026ldquo;公开全自动图灵测试,用于区分计算机和人类\u0026rdquo;,简称\u0026quot;验证码\u0026quot;。防止机器人发垃圾信息等,我承认 有时我都认不出那些扭曲的东西是什么字,这难道意味着我是计算机?🤔\n通常这个系列我们不会深入历史人物的个人生活,但图灵与悲剧密不可分,所以他的故事值得一提。图灵那个时代,同性恋是违法的,英国和大部分国家都是。1952 年调查他家的入室盗窃案时,向当局暴露了他的性取向,被起诉 \u0026ldquo;行为严重不检点\u0026rdquo;,图灵被定罪,有 2 个选择:1. 入狱; 2. 接受激素来压制性欲。他选了后者,部分原因是为了继续学术工作,但药物改变了他的情绪和性格,虽然确切情况永远无法得知。图灵于 1954 年服毒自尽,年仅 41 岁。\n图灵真的是惨……看看现在西方,lgbt 都上天了快……\r由于图灵对计算机科学贡献巨大,许多东西以他命名,其中最出名的是\u0026quot;图灵奖\u0026quot; - 计算机领域的最高奖项。相当于物理,化学等其它领域的诺贝尔奖。虽然英年早逝,但图灵激励了第一代计算机科学家,而且为如今便利的数字时代 做出了重要基石性工作。\n我们下周见。\n16. 软件工程 i.e. software engineering\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n之前花了很多时间讲排序,也写了些 10 行左右的排序代码,对 1 个程序员来说很容易写。而且代码很短,不必用专门工具 - 记事本就够了。真的!但排序算法很少会是独立程序 ,更可能是大项目的一小部分,举个例子,微软的 office 大约有 4000 万代码,4000 万!太多了,一个人不可能做到,为了写大型程序,程序员用各种工具和方法,所有这些形成了\u0026quot;软件工程\u0026quot;学科 - 这个词由工程师 margaret hamilton 创造,她帮助 nasa 在阿波罗计划中避免了严重问题。\nshe once explained it this way: 她曾说过:\u0026ldquo;有点像牙根管治疗:你总是拖到最后才做,但有些事可以预先做好。有点像预防性体检,只不过是预防软件出错。\u0026rdquo;\n第 12 集提过,把大项目分解成小函数,可以让多人同时工作,不用关心整个项目,关心自己的函数就好了。如果你的任务是写排序算法,你只需要确保高效和正确就可以了,然而把代码打包成函数 依然不够。如果只是这样,微软 office 会有几十万个函数,虽然比 4000 万行代码要好一些,但还是太多了。解决办法是:把函数打包成层级,把相关代码都放在一起,打包成对象(objects)。\n例如,汽车软件中 可能有几个和定速巡航有关的函数,比如 设定速度,逐渐加速减速,停止定速巡航,因为这些函数都相关,可以包装成一个\u0026quot;定速巡航对象\u0026quot;,但不止如此,我们还可以做更多。\u0026ldquo;定速巡航\u0026quot;只是引擎软件的一部分,可能还有 \u0026ldquo;火花塞点火\u0026rdquo; \u0026ldquo;燃油泵\u0026rdquo; 和 \u0026ldquo;散热器\u0026rdquo;,我们可以做一个\u0026quot;引擎对象\u0026rdquo; 来包括所有\u0026quot;子\u0026quot;对象,除了子对象,\u0026ldquo;引擎对象\u0026quot;可能有自己的函数,比如 开关引擎,它也会有自己的变量,比如汽车行驶了多少英里。\n总的来说,对象可以包其它对象,函数和变量。\n当然,\u0026ldquo;引擎对象\u0026quot;只是\u0026quot;汽车对象\u0026quot;的一部分,还有传动装置,车轮,门,窗等。作为程序员,如果想设\u0026quot;定速巡航\u0026rdquo;,要一层层向下,从最外面的对象往里找,最后找到想执行的函数:\n\u0026#34;car, then engine, then cruise control, then set cruise speed to 55\u0026#34;.\rcar. engine. cruisecontrol. setcruisespeed(55) 编程语言经常用类似这样的语法,把函数打包成对象的思想叫 \u0026ldquo;面向对象编程\u0026rdquo; 。这种思想和之前类似,通过封装组件,隐藏复杂度。之前把晶体管打包成了逻辑门,现在软件也这样做。\n又提升了一层抽象!\n把大型软件(如汽车软件) 拆成一个个更小单元,适合团队合作,一个团队负责定速巡航系统,团队里的一位程序员负责其中一些函数。类似建摩天大楼,有电工装电线、管道工配管、焊接工焊接、油漆工涂油漆,还有成百上千人做其他事情,在不同岗位同时工作,各尽其能,直到整栋楼完成。\n回到定速巡航的例子,定速巡航 要用到引擎的其它函数,来保持车速。定速巡航 团队不负责这些代码,另一个团队负责,因为是其他团队的代码,定速巡航 团队需要文档 帮助理解代码都做什么,以及定义好的 \u0026ldquo;程序编程接口\u0026rdquo; -简称 api。api 帮助不同程序员合作,不用知道具体细节,只要知道怎么使用就行了。例如\u0026quot;点火控制\u0026quot;对象中,可能有\u0026quot;设置发动机转数\u0026quot;函数、\u0026ldquo;检查火花塞电压\u0026quot;函数、\u0026ldquo;点燃单个火花塞\u0026quot;函数。\u0026ldquo;设置发动机转速\u0026quot;非常有用,\u0026ldquo;定速巡航\u0026quot;团队要用到这个函数,但他们对点火系统不怎么了解,让他们调用\u0026quot;点燃单个火花塞\u0026quot;函数,不是好主意,引擎可能会炸!可能啦 !👻 api 控制哪些函数和数据让外部访问,哪些仅供内部。\u0026ldquo;面向对象\u0026quot;的编程语言可以指定函数是 public 或 private,来设置权限。如果函数标记成 private ,意味着 只有同一个对象内的其他函数能调用它。在这个例子里,只有内部函数比如 setrpm 才能调用 firesparkplug 函数 ,而 setrpm 函数是 public,所以其它对象可以调用它,比如 定速巡航。\n\u0026ldquo;面向对象编程\u0026quot;的核心是 隐藏复杂度,选择性的公布功能。因为做大型项目很有效,所以广受欢迎。计算机上几乎所有软件,游戏机里几乎所有游戏,都是 \u0026ldquo;面向对象\u0026rdquo; 编程语言写的,比如 c++, c#, objective-c 等。其他流行 oo 语言,你可能听过 python 和 java 。\n有一点很重要:代码在编译前就只是文字而已,前面提过,你可以用记事本或任何文字处理器,有人确实这样做。😳 但一般来说,现代软件开发者会用专门的工具来写代码,工具里集成了很多有用功能 帮助写代码,整理,编译和测代码,因为集成了所有东西,因此叫 集成开发环境,简称 ide 。所有 ide 都有写代码的界面,还带一些有用功能,比如代码高亮,来提高可读性。许多 ide 提供实时检查,比如拼写。大型项目有很多源代码文件,ide 帮助开发者整理和看代码,很多 ide 还可以直接编译和运行代码。\n如果程序崩了,因为你还没写完呢,ide 可以定位到出错代码,还会提供信息 帮你解决问题,这叫 调试(debug)。调试很重要,大多数程序员会花 70%~80% 时间调试,而不是在写代码。好工具能极大帮助程序员防止和解决错误,很多开发者只用一款 ide,但承认吧,vim 才是最棒的编辑器,如果你知道怎么退出的话。\n除了写代码和调试,程序员工作的另一个重要部分是给代码写文档,文档一般放在一个叫 readme 的文件里,告诉其他程序员,看代码前先看这个文件。文档也可以直接写成\u0026quot;注释\u0026rdquo;,放在源代码里,注释是标记过的一段文字,编译代码时注释会被忽略,注释存在的唯一作用 - 就是帮助开发者理解代码。好文档能帮助开发者 ,几个月后理解自己的代码,对其他人也很重要。我想花一秒再强调一下注释很重要!最糟糕的就是拿到一堆代码,没有任何注释和文档,结果得逐行读代码,理解到底干嘛的。我是认真的,别做那种人!文档还可以提高复用性,与其让程序员一遍遍写同样的东西,可以直接用别人写好的来解决问题,读文档看怎么用就行,不用读代码,\u0026ldquo;读文档啊\u0026rdquo;!\n除了 ide,还有一个重要软件帮助团队协作,源代码管理,也叫\u0026quot;版本控制\u0026rdquo;!\n苹果或微软这样的大型软件公司,会把代码放到一个中心服务器上,叫\u0026quot;代码仓库\u0026rdquo;。程序员想改一段代码时,可以 check out ,有点像从图书馆借书 。一般这种操作,可以直接在 ide 内完成,然后开发者在自己的电脑上编辑代码,加新功能,测试。如果代码没问题了,所有测试通过了,可以把代码放回去,这叫 提交 (commit) 。当代码被 check out,而且可能被改过了,其他开发者不会动这段代码,防止代码冲突和重复劳动,这样多名程序员可以同时写代码,建立庞大的系统。重要的是,你不希望提交的代码里有问题,因为其他人可能用到了这些代码,导致他们的代码崩溃,造成困惑而且浪费时间。代码的主版本 (master),应该总是编译正常,尽可能少 bug ,但有时 bug 还是会出现 。幸运的是,源代码管理可以跟踪所有变化,如果发现 bug ,全部或部分代码,可以\u0026quot;回滚\u0026quot;到之前的稳定版 。\u0026ldquo;源代码管理\u0026rdquo; 也记录了谁改了什么代码,所以同事可以给你发 讨厌的,我是说\u0026quot;有帮助的\u0026rdquo; 邮件给写代码的人。\n写代码和测代码密不可分,测试一般由个人或小团队完成,测试可以统称 \u0026ldquo;质量保证测试\u0026rdquo;,简称 qa 。严格测试软件的方方面面,模拟各种可能情况,看软件会不会出错,基本上就是找 bug 。解决大大小小的错误需要很多工作,但对确保软件质量至关重要,让软件在各种情况下按预期运行。你可能听过 \u0026ldquo;beta 版\u0026rdquo; 软件,意思是软件接近完成,但不是 100% 完全测试过。公司有时会向公众发布 beta 版,以帮助发现问题,用户就像免费的 qa 团队。你听过比较少的是 beta 版之前的版本:alpha 版本,alpha 版一般很粗糙,错误很多, 经常只在公司内部测试。\n以上只是软件工程师用的工具和技巧的冰山一角。它们帮助软件工程师制作令人喜爱的软件,如 youtube,gta5 和 ppt 等等。如你所料,这些代码要强大的处理能力 才能高速速度运行。\n所以下集讨论,计算机怎么发展到如今这么快。\n到时见。\n17. 集成电路 \u0026amp; 摩尔定律 i.e. integrated circuits \u0026amp; moore’s law\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n过去 6 集我们聊了软件,从早期编程方式到现代软件工程。在大概 50 年里,软件从纸带打孔,变成面向对象编程语言,在集成开发环境中写程序,但如果没有硬件的大幅度进步,软件是不可能做到这些的。\n为了体会硬件性能的爆炸性增长 我们要回到电子计算机的诞生年代。\n大约 1940 年代~1960 年代中期这段时间里,计算机都由独立部件组成,叫\u0026quot;分立元件\u0026rdquo;,然后不同组件再用线连在一起。举例,eniac 有 1 万 7 千多个真空管,7 万个电阻,1 万个电容器,7 千个二极管,5 百万个手工焊点。如果想提升性能,就要加更多部件,这导致更多电线,更复杂,这个问题叫 \u0026ldquo;数字暴政\u0026rdquo; 。\n1950 年代中期,晶体管开始商业化(市场上买得到),开始用在计算机里。晶体管比电子管更小更快更可靠,但晶体管依然是分立元件。\n1959 年,ibm 把 709 计算机从原本的电子管 全部换成晶体管,诞生的新机器 ibm 7090,速度快 6 倍,价格只有一半。晶体管标志着\u0026quot;计算 2.0 时代\u0026quot;的到来。虽然更快更小 但晶体管的出现 还是没有解决\u0026quot;数字暴政\u0026quot;的问题,有几十万个独立元件的计算机不但难设计 而且难生产。\n1960 年代,这个问题的严重性达到顶点 电脑内部常常一大堆电线缠绕在一起。\n看看这个 1965 年 pdp-8 计算机的内部。\n解决办法是引入一层新抽象,封装复杂性。\n突破性进展在 1958 年 当时 jack killby 在德州仪器工作,演示了一个电子部件: \u0026ldquo;电路的所有组件都集成在一起\u0026rdquo;,简单说就是: 与其把多个独立部件用电线连起来,拼装出计算机,我们把多个组件包在一起,变成一个新的独立组件,这就是 集成电路(ic)。几个月后,在 1959 年 robert noyce 的仙童半导体 让集成电路变为现实,kilby 用锗来做集成电路,锗很稀少而且不稳定,仙童半导体公司用硅 硅的蕴藏量丰富,占地壳四分之一,也更稳定可靠,所以 noyce 被公认为现代集成电路之父,开创了电子时代,创造了硅谷(仙童公司所在地),之后有很多半导体企业都出现在硅谷。\n起初,一个 ic 只有几个晶体管,例如这块早期样品,由西屋公司制造。即使只有几个晶体管 也可以把简单电路,第 3 集的逻辑门,能封装成单独组件。ic 就像电脑工程师的乐高积木,可以组合出无数种设计,但最终还是需要连起来, 创造更大更复杂的电路,比如整个计算机,所以工程师们再度创新:印刷电路板,简称 pcb 。pcb 可以大规模生产,无需焊接或用一大堆线。它通过蚀刻金属线的方式,把零件连接到一起,把 pcb 和 ic 结合使用 可以大幅减少独立组件和电线,但做到相同的功能,而且更小,更便宜,更可靠。三赢!\n许多早期 ic 都是把很小的分立元件 封装成一个独立单元,例如这块 1964 年的 ibm 样品,不过,即使组件很小 塞 5 个以上的晶体管还是很困难。为了实现更复杂的设计,需要全新的制作工艺 \u0026ldquo;光刻\u0026quot;登场!简单说就是用光把复杂图案印到材料上,比如半导体,它只有几个基础操作,但可以制作出复杂电路。\n下面用一个简单例子,来做一片这个!\n我们从一片硅开始,叫\u0026quot;晶圆\u0026rdquo; 长得像薄饼干一样。美味!我们在第 2 集讨论过 硅很特别,它是半导体,它有时导电,有时不导电,我们可以控制导电时机,所以硅是做晶体管的绝佳材料。我们可以用晶圆做基础,把复杂金属电路放上面,集成所有东西,非常适合做。. 集成电路!\n下一步是在硅片顶部加一层薄薄的氧化层,作为保护层,然后加一层特殊化学品,叫 \u0026ldquo;光刻胶\u0026rdquo;,光刻胶被光照射后 会变得可溶,可以用一种特殊化学药剂洗掉。单单光刻胶本身,并不是很有用,但和\u0026quot;光掩膜\u0026quot;配合使用会很强大,光掩膜就像胶片一样,只不过不是 吃墨西哥卷饼的可爱仓鼠,而是要转移到晶圆上的图案。把光掩膜盖到晶圆上,用强光照射 挡住光的地方,光刻胶不会变化,光照到的地方,光刻胶会发生化学变化 洗掉它之后,暴露出氧化层,用另一种化学物质 - 通常是一种酸 可以洗掉\u0026quot;氧化层\u0026quot;露出的部分,蚀刻到硅层。注意,氧化层被光刻胶保护住了。为了清理光刻胶,我们用另一种化学药品洗掉它,是的,光刻法用很多化学品,每种都有特定用途,现在硅又露出来了,我们想修改硅露出来的区域,让它导电性更好,所以用一种化学过程来改变它,叫\u0026quot;掺杂\u0026rdquo;。不是开玩笑!我们继续!\u0026ldquo;掺杂\u0026rdquo; 通常用高温气体来做,比如磷 渗透进暴露出的硅,改变电学性质。\n半导体的具体物理和化学性质我们不会深究,如果你感兴趣,描述里有个视频链接 视频制作者是 derek muller 他的频道叫 veritasium 。\n但我们还需要几轮光刻法 来做晶体管,过程基本一样,先盖氧化层,再盖光刻胶,然后用新的光掩膜,这次图案不同 在掺杂区域上方开一个缺口,洗掉光刻胶,然后用另一种气体掺杂 把一部分硅转成另一种形式。为了控制深度,时机很重要,我们不想超过之前的区域,现在,所有需要的组件都有了。\n最后一步,在氧化层上做通道 这样可以用细小金属导线,连接不同晶体管,再次用光刻胶和光掩膜蚀刻出小通道。现在用新的处理方法 叫\u0026quot;金属化\u0026rdquo; 放一层薄薄的金属,比如铝或铜,但我们不想用金属盖住所有东西 我们想蚀刻出具体的电路,所以又是类似的步骤 用光刻胶+光掩膜,然后溶掉暴露的光刻胶,暴露的金属。咻~\n晶体管终于做好了! 它有三根线,连接着硅的三个不同区域,每个区域的掺杂方式不同,这叫双极型晶体管。这个 1962 年的真实专利,永远改变了世界!\n用类似步骤,光刻可以制作其他电子元件 比如电阻和电容,都在一片硅上,而且互相连接的电路也做好了。再见了,分立元件!\n之前的例子 只做了一个晶体管,但现实中 光刻法一次会做上百万个细节。芯片放大是这样的,导线上下交错,连接各个元件,尽管可以把光掩膜投影到一整片晶圆上 但光可以投射成任意大小,就像投影仪可以投满荧幕一样,我们可以把光掩膜 聚焦到极小的区域,制作出非常精细的细节。一片晶圆可以做很多 ic 整块都做完后,可以切割然后包进微型芯片,微型芯片就是在电子设备中那些小长方体,记住,芯片的核心都是一小片 ic 。\n随着光刻技术(photolithography techniques)发展,晶体管变小,密度变高。1960 年代初,ic 很少超过 5 个晶体管,因为塞不下,但 1960 年代中期 市场上开始出现超过 100 个晶体管的 ic 。1965 年,戈登·摩尔看到了趋势:每两年左右,得益于材料和制造技术的发展 同样大小的空间,能塞进两倍数量的晶体管!这叫 摩尔定律 ,然而这个名字不太对 因为它不是定律,只是一种趋势,但它是对的!\n芯片的价格也急剧下降 1962 年平均 50 美元,下降到 1968 年 2 美元左右。如今 几美分就能买到 ic ,晶体管更小密度更高 还有其他好处。晶体管越小,要移动的电荷量就越少 能更快切换状态,耗电更少。电路更紧凑 还意味着信号延迟更低,导致时钟速度更快。\n1968 年,罗伯特·诺伊斯 和 戈登·摩尔 联手成立了一家新公司,结合 intergrated(集成) 和 electronics(电子) 两个词,取名 intel, 如今最大的芯片制造商!\nintel 4004 cpu, 在第 7, 8 集介绍过 是个重要里程碑,发布于 1971 年 是第一个用 ic 做的处理器,也叫微型处理器,因为真的非常小!它有 2300 个晶体管,人们惊叹于它的整合水平 整个 cpu 在一个芯片里,而仅仅 20 年前,用分立元件会占满整个屋子。\n集成电路的出现 尤其是用来做微处理器,开启了计算 3.0 。\n而 intel 4004 只是个开始,cpu 晶体管数量爆发增长。1980 年,3 万晶体管。1990 年,100 万晶体管。2000 年,3000 万个晶体管。2010 年,10 亿个晶体管! 在一个芯片里!我的天啊!\n为了达到这种密度,光刻的分辨率 从大约一万纳米,大概是人类头发直径的 1/10 ,发展到如今的 14 纳米 比血红细胞小 400 倍!\n当然,cpu 不是唯一受益的元件。大多数电子器件都在指数式发展: 内存,显卡,固态硬盘,摄像头感光元件,等等。如今的处理器,比如 iphone 7 的 a10 cpu 有 33 亿个晶体管,面积仅有 1cm x 1cm,比一张邮票还小。\n现代工程师设计电路时,当然不是手工一个个设计晶体管 这不是人力能做到的,1970 年代开始,超大规模集成 (vlsi) 软件 用来自动生成芯片设计,用比如 \u0026ldquo;逻辑综合\u0026rdquo; 这种技术,可以放一整个高级组件,比如内存缓存。软件会自动生成电路,做到尽可能高效,许多人认为这是计算 4.0 的开始。\n坏消息是,专家们几十年来 一直在预言摩尔定律的终结,现在可能终于接近了。进一步做小,会面临 2 个大问题:\n用光掩膜把图案弄到晶圆上 因为光的波长,精度已达极限。所以科学家在研制波长更短的光源,投射更小的形状; 当晶体管非常小,电极之间可能只距离几个原子,电子会跳过间隙,这叫:量子隧穿效应。 如果晶体管漏电,就不是好开关。科学家和工程师在努力找解决方法,实验室中已造出小至 1 纳米的晶体管,能不能商业量产依然未知,未来也许能解决。\n我非常期待!下周见!\n18. 操作系统 i.e. operating systems\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n1940, 1950 年代的电脑,每次只能运行一个程序。程序员在打孔纸卡上写程序,然后拿到一个计算机房间,交给操作员,等计算机空下来了,操作员会把程序放入,然后运行,输出结果,停机。以前计算机慢,这种手动做法可以接受,运行一个程序通常要几小时,几天甚至几周。但上集说过,计算机越来越快,越来越快 - 指数级增长!很快,放程序的时间比程序运行时间还长,我们需要一种方式让计算机自动运作,于是\u0026quot;操作系统\u0026quot;诞生了。\n操作系统,简称 os(operating systems),其实也是程序,但它有操作硬件的特殊权限,可以运行和管理其它程序。操作系统一般是开机第一个启动的程序,其他所有程序都由操作系统启动。\n操作系统开始于 1950 年代,那时计算机开始变得更强大更流行。第一个操作系统加强了程序加载方式,之前只能一次给一个程序,现在可以一次多个。当计算机运行完一个程序,会自动运行下一个程序,这样就不会浪费时间找下一个程序的纸卡,这叫 批处理 。电脑变得更快更便宜,开始在出现在世界各地,特别是大学和政府办公室。很快,人们开始分享软件,但有一个问题 - 在哈佛 1 号和 eniac 那个时代,计算都是一次性的。程序员只需要给那\u0026quot;一台\u0026quot;机器写代码,处理器,读卡器,打印机都是已知的。但随着电脑越来越普遍,计算机配置并不总是相同的,比如计算机可能有相同 cpu,但不同的打印机。这对程序员很痛苦,不仅要担心写程序,还要担心程序怎么和不同型号打印机交互,以及计算机连着的其他设备,这些统称\u0026quot;外部设备\u0026quot; 。和早期的外部设备交互,是非常底层的,程序员要了解设备的硬件细节。加重问题的是,程序员很少能拿到所有型号的设备来测代码,所以一般是阅读手册来写代码,祈祷能正常运行。现在是\u0026quot;即插即用\u0026quot;,以前是\u0026quot;祈祷能用\u0026quot;。\n这很糟糕,所以为了程序员写软件更容易,操作系统充当软件和硬件之间的媒介。\n更具体地说,操作系统提供 api 来抽象硬件,叫 \u0026ldquo;设备驱动程序\u0026rdquo;,程序员可以用标准化机制和输入输出硬件(i/o)交互。比如,程序员只需调用 print(highscore),操作系统会处理输到纸上的具体细节。\n到 1950 年代尾声,电脑已经非常快了。处理器经常闲着,等待慢的机械设备(比如打印机和读卡器)。程序阻塞在 i/o 上,而昂贵的处理器则在度假,就是放松啥也不做。50 年代后期,英国曼彻斯特大学 开始研发世界上第一台超级计算机,atlas ,他们知道机器会超级快,所以需要一种方式来最大限度的利用它,他们的解决方案是一个程序叫 atlas supervisor ,于 1962 年完成。这个操作系统不仅像更早期的批处理系统那样,能自动加载程序,还能在单个 cpu 上同时运行几个程序,它通过调度来做到这一点。假设 atlas 上有一个游戏在运行,并且我们调用一个函数 print(highscore),它让 atlas 打印一个叫 highscore 的变量值,让朋友知道 我是最高分冠军。 print 函数运行需要一点时间,大概上千个时钟周期,但因为打印机比 cpu 慢,与其等着它完成操作,atlas 会把程序休眠,运行另一个程序,最终,打印机会告诉 atlas, 打印已完成,atlas 会把程序标记成可继续运行,之后在某时刻会安排给 cpu 运行,并继续 print 语句之后的下一行代码。这样,atlas 可以在 cpu 上运行一个程序,同时另一个程序在打印数据,同时另一个程序读数据。atlas 的工程师做的还要多,配了 4 台纸带读取器,4 台纸带打孔机,多达 8 个磁带驱动器。\n使多个程序可以同时运行,在单个 cpu 上共享时间,操作系统的这种能力叫 \u0026ldquo;多任务处理\u0026rdquo; 。同时运行多个程序有个问题,每个程序都会占一些内存,当切换到另一个程序时,我们不能丢失数据,解决办法是给每个程序分配专属内存块。\n举个例子,假设计算机一共有 10000 个内存位置,程序 a 分配到内存地址 0 到 999,而程序 b 分配到内存地址 1000 到 1999,以此类推。如果一个程序请求更多内存,操作系统会决定是否同意,如果同意,分配哪些内存块。这种灵活性很好,但带来一个奇怪的后果,程序 a 可能会分配到非连续的内存块,比如内存地址 0 到 999,以及 2000 到 2999。这只是个简单例子,真正的程序可能会分配到内存中数十个地方,你可能想到了,这对程序员来说很难跟踪。也许内存里有一长串销售额,每天下班后要算销售总额,但列表 存在一堆不连续的内存块里…… 🤬\n为了隐藏这种复杂性,操作系统会把内存地址进行 \u0026ldquo;虚拟化\u0026rdquo;,这叫 \u0026ldquo;虚拟内存\u0026rdquo; ,程序可以假定内存总是从地址 0 开始,简单又一致,而实际物理位置被操作系统隐藏和抽象了。\n一层新的抽象!!!\n用程序 b 来举例 它被分配了内存地址 1000 到 1999,对程序 b 而言,它看到的地址是 0 到 999,操作系统会自动处理 虚拟内存和物理内存之间的映射。如果程序 b 要地址 42,实际上是物理地址 1042,这种内存地址的虚拟化对程序 a 甚至更有用。在例子中,a 被分配了两块隔开的内存,程序 a 不知道这点,以 a 的视角,它有 2000 个连续地址。当程序 a 读内存地址 999 时 会刚好映射到物理内存地址 999,但如果程序 a 读下一个地址 1000 ,会映射到物理地址 2000 ,这种机制使程序的内存大小可以灵活增减 叫 \u0026ldquo;动态内存分配\u0026rdquo; 。对程序来说,内存看起来是连续的。它简化了一切,为操作系统同时运行多个程序 提供了极大的灵活性,给程序分配专用的内存范围,另一个好处是 这样隔离起来会更好。如果一个程序出错,开始写乱七八糟的数据,它只能捣乱自己的内存,不会影响到其它程序,这叫 \u0026ldquo;内存保护\u0026rdquo; 。防止恶意软件(如病毒)也很有用,例如,我们不希望其他程序有能力 读或改邮件程序的内存,如果有这种权限 恶意软件可能以你的名义发邮件,甚至窃取个人信息,一点都不好!\natlas 既有\u0026quot;虚拟内存\u0026quot;也有\u0026quot;内存保护\u0026quot;,是第一台支持这些功能的计算机和操作系统!\n到 1970 年代,计算机足够快且便宜,大学会买电脑让学生用。计算机不仅能同时运行多个程序,还能让多用户能同时访问。多个用户用\u0026quot;终端\u0026quot;来访问计算机,\u0026ldquo;终端\u0026quot;只是键盘+屏幕,连到主计算机 终端本身没有处理能力。冰箱大小的计算机可能有 50 个终端,能让 50 个用户使用,这时操作系统不但要处理多个程序,还要处理多个用户。为了确保其中一个人不会占满计算机资源,开发了 分时操作系统 ,意思是 每个用户只能用一小部分处理器,内存等。因为电脑很快 即使拿到 1/50 的资源也足以完成许多任务。\n早期分时操作系统中,最有影响力的是 multics(多任务信息与计算系统),于 1969 年发布。multics 是第一个,从设计时就考虑到安全的操作系统,开发人员不希望恶意用户访问不该访问的数据,比如学生假装成教授,访问期末考试的文件,这导致 multics 的复杂度超过当时的平均水准,操作系统会占大约 1 mb 内存,这在当时很多!可能是内存的一半,只拿来运行操作系统!\nmultics 的研究人员之一 dennis ritchie 曾说过,\u0026ldquo;阻碍 multics 获得商业成功的一个明显问题是 - 从某种方面来说,它被过度设计了,功能太多了\u0026rdquo; 。所以 dennis 和另一个 multics 研究员 ken thompson 联手打造新的操作系统,叫 unix 。他们想把操作系统分成两部分:\n首先是操作系统的核心功能,如内存管理,多任务和输入/输出处理 这叫 \u0026ldquo;内核\u0026rdquo; ; 第二部分是一堆有用的工具,但它们不是内核的一部分(比如程序和运行库)。 紧凑的内核意味着功能没有那么全面。\nmultics 的另一个开发者 tom van vleck 回忆说:\u0026ldquo;我对 dennis 说,我在 multics 写的一半代码都是错误恢复代码\u0026rdquo;。他说:\u0026ldquo;unix 不会有这些东西,如果有错误发生,我们就让内核\u0026quot;恐慌\u0026rdquo;(panic),当调用它时,机器会崩溃,你得在走廊里大喊,\u0026ldquo;嘿,重启电脑\u0026rdquo;!😹\n你可能听过 \u0026ldquo;内核恐慌\u0026rdquo;(kernel panic),这就是这个词的来源。内核如果崩溃,没有办法恢复,所以调用一个叫\u0026quot;恐慌\u0026rdquo;(panic)的函数。起初只是打印\u0026quot;恐慌\u0026quot;一词,然后无限循环,这种简单性意味着 unix 可以在更便宜更多的硬件上运行。\nunix 在 dennis 和 ken 工作的贝尔实验室大受欢迎,越来越多开发人员用 unix 写程序和运行程序,工具数量日益增长。1971 年发布后不久,就有人写了不同编程语言的编译器,甚至文字处理器,使得 unix 迅速成为 1970~80 年代最流行的操作系统之一。\n到 1980 年代早期,计算机的价格 降到普通人买得起,这些叫\u0026quot;个人电脑\u0026quot;或\u0026quot;家庭电脑\u0026quot;。这些电脑比大型主机简单得多,主机一般在大学,公司和政府,因此操作系统也得简单。举例,微软的磁盘操作系统(ms-dos)只有 160 kb 一张磁盘就可以容纳,于 1981 年发布,成为早期家用电脑最受欢迎的操作系统,虽然缺少\u0026quot;多任务\u0026quot;和\u0026quot;保护内存\u0026quot;这样功能,意味着程序经常使系统崩溃。虽然很讨厌但还可以接受,因为用户可以重启,哪怕是微软 1985 年发布的早期 windows 虽然在 90 年代很流行,但却缺乏\u0026quot;内存保护\u0026quot;,当程序行为不当时,就会\u0026quot;蓝屏\u0026quot;,代表程序崩溃的非常严重,把系统也带崩溃了…… 👻👻👻\n幸运的是,新版 windows 有更好的保护,不会经常崩溃。如今的计算机 有现代操作系统,比如 mac os x,windows 10 ,linux,ios 和 android 。虽然大部分设备只有一个人使用 - 你! 操作系统依然有\u0026quot;多任务,\u0026ldquo;虚拟内存\u0026rdquo;, \u0026ldquo;内存保护\u0026rdquo;,因此可以同时运行多个程序: 一边在浏览器看 youtube,一边在 photoshop 修图,用 spotify 放音乐,同步 dropbox 。如果没有操作系统这几十年的发展,这些都不可能,当然,我们也需要地方放程序 。\n下周会讨论。\n19. 内存 \u0026amp; 储存介质 i.e. memory \u0026amp; storage\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n系列中 我们多次谈到内存(memory), 甚至在第 6 集设计了一个简单内存。一般来说,电脑内存是 \u0026ldquo;非永久性\u0026rdquo;,如果 xbox 电源线不小心拔掉了,内存里所有数据都会丢失,所以内存叫\u0026quot;易失性\u0026quot;存储器。\n我们还没谈过的话题 是存储器(storage),存储器(storage)和内存(memory)有点不同 - 任何写入\u0026quot;存储器\u0026quot;的数据,比如你的硬盘 数据会一直存着,直到被覆盖或删除,断电也不会丢失。存储器是\u0026quot;非易失性\u0026quot;的,以前是\u0026quot;易失性\u0026quot;的速度快,\u0026ldquo;非易失性\u0026quot;的速度慢。但随着技术发展,两者的差异越来越小。\n如今我们认为稀松平常的技术,比如这个 u 盘能低成本+可靠+长时间 存储上 gb 的数据。但以前可不是这样的,最早的存储介质是 打孔纸卡 以及纸卡的亲戚 - 打孔纸带。到 1940 年代,纸卡标准是 80 列 x 12 行,一张卡能存 960 位数据 (80x12=960) 。据我们所知的最大纸卡程序是美国军方的\u0026quot;半自动地面防空系统\u0026rdquo;,简称 sage - 一个在 1958 年投入使用的防空系统,主程序存储在 62,500 个纸卡上,大小 5mb 左右,相当如今手机拍张照。\n纸卡用了十几年,因为不用电而且便宜耐用,然而坏处是读取慢,只能写入一次,打的孔无法轻易补上。对于存临时值,纸卡不好用,我们需要更快更大更灵活的存储方式。\nj. presper eckert 在 1944 年建造 eniac 时发明了一种方法,叫\u0026quot;延迟线存储器\u0026quot;(delay line memory),原理如下:拿一个管子装满液体,如水银,管子一端放扬声器,另一端放麦克风,扬声器发出脉冲时会产生压力波,压力波需要时间传播到另一端的麦克风,麦克风将压力波转换回电信号。我们可以用压力波的传播延迟来存储数据!假设有压力波代表 1,没有代表 0 ,扬声器可以输出 1010 0111 。压力波沿管子传播,过了一会儿,撞上麦克风,将信号转换回 1 和 0 ,如果加一个电路,连接麦克风和扬声器,再加一个放大器(amplifier)来弥补信号衰弱,就能做一个存储数据的循环。信号沿电线传播几乎是瞬时的,所以任何时间点只显示 1 bit 数据,但管子中可以存储多个位 (bit) 。忙完 eniac 后,eckert 和同事 john mauchly 着手做一个更大更好的计算机叫 edvac,使用了延迟线存储器,总共有 128 条延迟线,每条能存 352 位(bits),总共能存 45,000 位 (bit) ,对 1949 年来说还不错!这使得 edvac 成为最早的 \u0026ldquo;存储程序计算机\u0026rdquo; 之一,我们在第 10 集讨论过。\n但\u0026quot;延迟线存储器\u0026quot;的一大缺点是 - 每一个时刻只能读一位 (bit) 数据,如果想访问一个特定的 bit,比如第 112 位 (bit) 你得等待它从循环中出现,所以又叫 \u0026ldquo;顺序存储器\u0026quot;或\u0026quot;循环存储器\u0026rdquo;,而我们想要的是 \u0026ldquo;随机存取存储器\u0026rdquo; 可以随时访问任何位置。增加内存密度也是一个挑战,把压力波变得更紧密,意味着更容易混在一起,所以出现了其他类型的 \u0026ldquo;延迟线存储器\u0026rdquo;,如 \u0026ldquo;磁致伸缩延迟存储器\u0026rdquo;,用金属线的振动来代表数据,通过把线卷成线圈,1 英尺× 1 英尺的面积能存储大概 1000 位 (bit),然而,延迟线存储器在 1950 年代中期就基本过时了,因为出现了新技术,性能,可靠性和成本都更好 - \u0026ldquo;磁芯存储器\u0026rdquo; 🧲 ,用了像甜甜圈的小型磁圈,如果给磁芯绕上电线,并施加电流,可以将磁化在一个方向,如果关掉电流,磁芯保持磁化;如果沿相反方向施加电流,磁化的方向(极性)会翻转,这样就可以存 1 和 0!如果只存 1 位不够有用,所以把小甜甜圈排列成网格,有电线负责选行和列,也有电线贯穿每个磁芯,用于读写一位 (bit)。我手上有一块磁芯存储器,每个黄色方格 有 32 行 x32 列的磁芯 每个磁芯存 1 位数据,所以能存 1024 位 (bit) (32x32=1024),总共 9 个黄色方格,所以这块板子最多能存 9216 位 (bit) (1024x9=9216),换算过来大约是 9 千字节 (9216 bit ~= 9 kb)。磁芯内存的第一次大规模运用是 1953 年麻省理工学院的 whirlwind 1 计算机,磁芯排列是 32×32,用了 16 块板子,能存储大约 16000 位 (bit)。更重要的是,不像\u0026quot;延迟线存储器\u0026quot; 磁芯存储器能随时访问任何一位 (bit),这在当时非常了不起!\n\u0026ldquo;磁芯存储器\u0026rdquo; 从 1950 年代中期开始成为主流 流行了 20 多年,而且一般还是手工编织的!刚开始时,存储成本大约 1 美元 1 位 (bit) 到 1970 年代,下降到 1 美分左右,不幸的是,即使每位 1 美分也不够便宜,之前提过,现代手机随便拍张照片都有 5 mb,5mb 约等于 4000 万 bit。你愿意花 40 万美元在\u0026quot;磁芯存储器\u0026quot;上存照片吗?如果你有这么多钱,你知道 crash course 在 patreon 有赞助页吗?对吧?你懂的!🤑\n总之,当时对存储技术进行了大量的研究。到 1951 年,eckert 和 mauchly 创立了自己的公司,设计了一台叫 univac 的新电脑,最早进行商业销售的电脑之一。它推出了一种新存储:磁带,磁带是纤薄柔软的一长条磁性带子 卷在轴上,磁带可以在\u0026quot;磁带驱动器\u0026quot;内前后移动,里面有一个\u0026quot;写头\u0026quot;绕了电线,电流通过产生磁场,导致磁带的一小部分被磁化。电流方向决定了极性,代表 1 和 0 。还有一个\u0026quot;读头\u0026quot;,可以非破坏性地检测极性 。univac 用了半英寸宽,8 条并行的磁带,磁带每英寸可存 128 位数据,每卷有 1200 英尺长,意味着一共可以存 1500 万位左右 - 接近 2 兆字节!(2 mb) 虽然磁带驱动器很贵,但磁带又便宜又小,因此磁带至今仍用于存档。磁带的主要缺点是访问速度 - 磁带是连续的,必须倒带或快进到达特定位置,可能要几百英尺才能得到某个字节 (byte),这很慢!🐢!\n1950,60 年代,有个类似技术是 \u0026ldquo;磁鼓存储器\u0026rdquo;。有金属圆筒,盖满了磁性材料以记录数据,滚筒会持续旋转,周围有数十个读写头,等滚筒转到正确的位置读写头会读或写 1 位 (bit) 数据,为了尽可能缩短延迟,鼓轮每分钟上千转!到 1953 年,磁鼓技术飞速发展,可以买到存 80,000 位的\u0026quot;磁鼓存储器\u0026quot; - 也就是 10 kb 。但到 1970 年代 \u0026ldquo;磁鼓存储器\u0026rdquo; 不再生产,然而,磁鼓导致了硬盘的发展。硬盘和磁鼓很相似\nwhich are very similar, but use a different geometric configuration. 然而,磁鼓导致了硬盘的发展 硬盘和磁鼓很相似,不过硬盘用的是盘,不像磁鼓用圆柱体,因此得名。原理是一样的,磁盘表面有磁性,写入头和读取头,可以处理上面的 1 和 0 。硬盘的好处是薄,可以叠在一起,提供更多表面积来存数据。ibm 对世上第一台磁盘计算机就是这样做的 - ramac 305 ,顺便一说名字不错,它有 50 张 24 英寸直径的磁盘,总共能存 5 mb 左右…… 太棒啦!终于能存一张现代手机的照片了 🖼 !这年是 1956 年!要访问某个特定 bit ,一个读/写磁头会向上或向下移动,找到正确的磁盘,然后磁头会滑进去,就像磁鼓存储器一样,磁盘也会高速旋转,所以读写头要等到正确的部分转过来。ramac 305 访问任意数据,平均只要六分之一秒左右,也叫寻道时间。虽然六分之一秒对存储器来说算不错,但对内存来说还不够快,所以 ramac 305 还有\u0026quot;磁鼓存储器\u0026quot;和\u0026quot;磁芯存储器\u0026quot;。\n这是\u0026quot;内存层次结构\u0026quot;的一个例子,一小部分高速+昂贵的内存 ,一部分稍慢+相对便宜些的内存 ,还有更慢+更便宜的内存,这种混合在成本和速度间取得平衡。\n1970 年代,硬盘大幅度改进并变得普遍,如今的硬盘可以轻易容纳 1tb 的数据,能存 20 万张 5mb 的照片!网上最低 40 美元就可以买到,每 bit 成本 0.0000000005 美分,比磁芯内存 1 美分 1 bit 好多了!另外,现代硬盘的平均寻道时间低于 1/100 秒。我简单地提一下硬盘的亲戚,软盘,除了磁盘是软的,其他基本一样。你可能见过某些程序的保存图标是一个软盘,软盘曾经是真实存在的东西!软盘是为了便携,在 1970~1990 非常流行,如今当杯垫挺不错的。密度更高的软盘,如 zip disks,在 90 年代中期流行起来,但十年内就消失了。\n光学存储器于 1972 年出现,12 英寸的\u0026quot;激光盘\u0026quot;。你可能对后来的产品更熟:光盘(compact disk)(简称 cd),以及 90 年代流行的 dvd,功能和硬盘软盘一样,都是存数据,但用的不是磁性。光盘表面有很多小坑,造成光的不同反射,光学传感器会捕获到,并解码为 1 和 0。如今,存储技术在朝固态前进,没有机械活动部件。比如这个硬盘,以及 u 盘,里面是集成电路,我们在第 15 集讨论过。\n第一个 ram 集成电路出现于 1972 年,成本每比特 1 美分,使\u0026quot;磁芯存储器\u0026quot;迅速过时。如今成本下降了更多,机械硬盘 被 固态硬盘 逐渐替代,简称 ssd(solid state drives)。由于 ssd 没有移动部件,磁头不用等磁盘转,所以 ssd 访问时间低于 1/1000 秒,这很快!🐇 但还是比 ram 慢很多倍,所以现代计算机 仍然用存储层次结构。\n我们从 1940 年代到现在进步巨大,就像在第 14 集讨论过的 晶体管数量和摩尔定律,内存和存储技术也有类似的趋势,从早期每 mb 成本上百万美元,下滑到 2000 年只要几分钱,如今远远低于 1 分钱,完全没有打孔纸卡,你能想象 sega 的纸卡房间风一吹会怎样吗? 62,500 张卡 …… 我想都不敢想 😂\n\u003e 不同的介质,不同的方法,归其本质,都是为了区分两种显著的状态,分别标识 0 和 1 。有了 0 和 1 ,就有了整个数据世界。\r我们下周见。\n20. 文件系统 i.e. files \u0026amp; file systems\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讲了数据存储,磁带和硬盘这样的技术,可以在断电状态长时间存上万亿个位,非常合适存一整块有关系的数据,或者说\u0026quot;文件\u0026quot;。你肯定见过很多种文件,比如文本文件,音乐文件,照片和视频。今天,我们要讨论文件到底是什么 以及计算机怎么管理文件。\n随意排列文件数据完全没问题,但按格式排会更好,这叫 \u0026ldquo;文件格式\u0026rdquo; 。你可以发明自己的文件格式,程序员偶尔会这样做,但最好用现成标准,比如 jpeg 和 mp3 。来看一些简单文件格式,最简单的是文本文件,也叫 txt 文件,里面包含的是。.. 文字 (惊喜吧)。就像所有其它文件, 文本文件只是一长串二进制数 ,原始值看起来会像这样:\n可以转成十进制看,但帮助不大,解码数据的关键是 ascii 编码 - 一种字符编码标准,第 4 集讨论过。第一个值 72 在 ascii 中是大写字母 h ,以此类推解码其他数字 。\n来看一个更复杂的例子:波形 (wave) 文件,也叫 wav 它存音频数据。在正确读取数据前,需要知道一些信息,比如码率 (bit rate),以及是单声道还是立体声。关于数据的数据,叫 \u0026ldquo;元数据\u0026rdquo; (meta data) 。元数据存在文件开头,在实际数据前面,因此也叫 文件头 (header) 。wav 文件的前 44 个字节长这样:\n有的部分总是一样的,比如写着 wave 的部分,其他部分的内容,会根据数据变化。音频数据紧跟在元数据后面,是一长串数字,数字代表每秒捕获多次的声音幅度。如果想学声音的基础知识,可以看物理速成课(crash course physics)第 18 集,举个例子,看一下\u0026quot;你好\u0026quot;的波形,现在捕获到了一些声音,我们放大看一下。\n电脑和手机麦克风,每秒可以对声音进行上千次采样,每次采样可以用一个数字表示,声压越高数字越大,也叫\u0026quot;振幅\u0026quot;,wave 文件里存的就是这些数据!每秒上千次的振幅!播放声音文件时,扬声器会产生相同的波形 - \u0026ldquo;你好!\u0026rdquo;\n现在来谈谈位图 (bitmap),后缀 .bmp,它存图片,计算机上,图片由很多个叫\u0026quot;像素\u0026quot;的方块组成,每个像素由三种颜色组成:红,绿,蓝,叫\u0026quot;加色三原色\u0026quot;,混在一起可以创造其它颜色。就像 wav 文件一样,bmp 文件开头也是元数据有:图片宽度,图片高度,颜色深度,举例,假设元数据说图是 4 像素宽 x 4 像素高,颜色深度 24 位、8 位红色,8 位绿色,8 位蓝色,提醒一下,8 位 (bit) 和 1 字节 (byte) 是一回事。一个字节能表示的最小数是 0,最大 255 。图像数据看起来会类似这样:来看看第一个像素的颜色 - 红色是 255,绿色是 255,蓝色也是 255,这等同于全强度红色,全强度绿色和全强度蓝色,混合在一起变成白色,所以第一个像素是白色!下一个像素的红绿蓝值,或 rgb 值 255,255,0 是黄色! 下一个像素的红绿蓝值,或 rgb 值 255,255,0 是黄色! 下一个像素是 0,0,0 ,黑色 !下一个是黄色 !因为元数据说图片是 4x4 我们知道现在到了第一行结尾,所以换一行,下一个 rgb 值是 255,255,0 ,又是黄色!好,我们读完剩下的像素 - 一个低分辨率的吃豆人。\n刚才显然只是一个简单例子,但这张图片也可以用 bmp 存。我想再次强调,不管是文本文件,wav,bmp,或是我们没时间讨论的其他格式。 文件在底层全是一样的: 一长串二进制 。为了知道文件是什么,文件格式至关重要。\n现在你对文件更了解了 我们接下来讨论计算机怎么存文件。\n虽然硬件可能是磁带,磁鼓,磁盘或集成电路,通过软硬件抽象后,可以看成一排能存数据的桶。在很早期时,计算机只做一件事,比如算火炮射程表。整个储存器就像一整个文件,数据从头存到尾,直到占满,但随着计算能力和存储容量的提高存多个文件变得非常有用,最简单的方法是把文件连续存储。这样能用,但怎么知道文件开头和结尾在哪里?储存器没有文件的概念,只是存储大量位,所以为了存多个文件,需要一个特殊文件,记录其他文件的位置,这个特殊文件有很多名字,这里泛称 \u0026ldquo;目录文件\u0026rdquo; 🗃 。这个文件经常存在最开头,方便找 - 位置 0!目录文件里,存所有其他文件的名字,格式是 文件名 + 一个句点 + 扩展名 ,比如 bmp 或 wav 。\n扩展名帮助得知文件类型,目录文件还存文件的元数据,比如创建时间,最后修改时间,文件所有者是谁、是否能读/写、或读写都行。最重要的是,目录文件有文件起始位置和长度,如果要添加文件,删除文件,更改文件名等,必须更新目录文件。就像书的目录,如果缩短或移动了一个章节,要更新目录,不然页码对不上。\n\u003e 文件名 + 一个句点 + 扩展名 文件起始位置和长度\n创建时间\n最后修改时间\n文件所有者是谁\n是否能读/写、或读写都行\n……\n目录文件,以及对目录文件的管理,是一个非常简单的文件系统例子!文件系统专门负责管理文件!\n刚刚的例子叫\u0026quot;平面文件系统\u0026quot; 因为文件都在同一个层次,平的!当然,把文件前后排在一起有个问题。如果给 todo.txt 加一点数据,会覆盖掉后面 carrie.bmp 的一部分,所以现代文件系统会做两件事:\n把空间划分成一块块,导致有一些 \u0026ldquo;预留空间\u0026rdquo; 可以方便改动,同时也方便管理(用这样的方案,目录文件要记录文件在哪些块里); 拆分文件,存在多个块里。 假设打开 todo.txt 加了些内容、文件太大存不进一块里,我们不想覆盖掉隔壁的块,所以文件系统会分配 一个没使用的块,容纳额外的数据。目录文件会记录不止一个块,而是多个块,只要分配块,文件可以轻松增大缩小,如果你看了第 18 集 操作系统 这听起来很像\u0026quot;虚拟内存\u0026quot;,概念上讲的确很像!假设想删掉 carrie.bmp 只需要在目录文件删掉那条记录,让一块空间变成了可用,注意这里没有擦除数据,只是把记录删了,之后某个时候,那些块会被新数据覆盖,但在此之前,数据还在原处,所以计算机取证团队可以\u0026quot;恢复\u0026quot;数据,虽然别人以为数据已经\u0026quot;删了\u0026quot;, 狡猾!😈 假设往 todo.txt 加了更多数据,所以操作系统分配了一个新块,用了刚刚 carrie.bmp 的块,现在 todo.txt 在 3 个块里,隔开了,顺序也是乱的,这叫 碎片。碎片是增/删/改文件导致的,不可避免,对很多存储技术来说,碎片是坏事。如果 todo.txt 存在磁带上,读取文件要先读块 1, 然后快进到块 5,然后往回转到块 2 ,来回转个半天。现实世界中,大文件可能存在数百个块里,你可不想等五分钟才打开文件,答案是碎片整理!这个词听起来好像很复杂,但实际过程很简单。以前看计算机做碎片整理,真的很有趣!计算机会把数据来回移动,排列成正确的顺序,整理后 todo.txt 在 1 2 3,方便读取。\n目前只说了平面文件系统,文件都在同一个目录里。如果存储空间不多,这可能就够用了,因为只有十几个文件。但上集说过,容量爆炸式增长,文件数量也飞速增长,很快,所有文件都存在同一层变得不切实际。就像现实世界、相关文件放在同一个文件夹会方便很多,然后文件夹套文件夹,这叫 \u0026ldquo;分层文件系统\u0026rdquo; ,你的计算机现在就在用这个。\n实现方法有很多种,我们用之前的例子来讲重点好了,最大的变化是 目录文件不仅要指向文件,还要指向目录。我们需要额外元数据来区分开文件和目录,这个目录文件在最顶层,因此叫根目录,所有其他文件和文件夹,都在根目录下。\n图中可以看到根目录文件有 3 个文件 2 个子文件夹:\u0026ldquo;音乐\u0026quot;和\u0026quot;照片\u0026rdquo;,如果想知道\u0026quot;音乐\u0026quot;文件夹里有什么 必须去那边读取目录文件(格式和根目录文件一样),有很多好歌啊!\n除了能做无限深度的文件夹,这个方法也让我们可以轻松移动文件,如果想把 theme.wav 从根目录移到音乐目录,不用移动任何数据块,只需要改两个目录文件,一个文件里删一条记录,另一个文件里加一条记录,theme.wav 依然在块 5 。\n文件系统的几个重要概念,现在介绍完了。它提供了一层新抽象!\n文件系统使我们不必关心 文件在磁带或磁盘的具体位置,整理和访问文件更加方便。我们像普通用户一样直观操纵数据,比如打开和整理文件,接下来几集也会从用户角度看问题。\n下周见。\n21. 压缩 i.e. compression\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n上集我们讨论了文件格式,如何编码文字,声音,图片,还举了具体例子 .txt .wav .bmp 。这些格式虽然管用,而且现在还在用,但它们的简单性意味着效率不高。我们希望文件能小一点,这样能存大量文件,传输也会快一些。等邮件附件下载烦死人了,解决方法是 压缩,把数据占用的空间压得更小,用更少的位 (bit) 来表示数据。\n听起来像魔法,但其实是计算机科学!\n我们继续用上集的 吃豆人例子,图像是 4 像素 x4 像素。之前说过,图像一般存成一长串像素值,为了知道一行在哪里结束 图像要有元数据,写明尺寸等属性,但为了简单起见,今天忽略这些细节,如果红绿蓝都是 255 会得到白色,如果混合 255 红色和 255 绿色,会得到黄色,这个图像有 16 个像素 (4x4), 每个像素 3 个字节,总共占 48 个字节(16x3=48),但我们可以压缩到少于 48 个字节。\n一种方法是 减少重复信息,最简单的方法叫 游程编码 (run-length encoding),适合经常出现相同值的文件。比如吃豆人 有 7 个连续黄色像素,与其全存下来:黄色,黄色,黄色。.. 可以插入一个额外字节,代表有 7 个连续黄色像素,然后删掉后面的重复数据。为了让计算机能分辨哪些字节是\u0026quot;长度\u0026quot;,哪些字节是\u0026quot;颜色\u0026quot;,格式要一致,所以我们要给所有像素前面标上长度。有时候数据反而会变多,但就这个例子而言,我们大大减少了字节数,之前是 48 现在是 24 ,小了 50%!省了很多空间!还有,我们没有损失任何数据 我们可以轻易恢复到原来的数据,这叫 \u0026ldquo;无损压缩\u0026rdquo; ,没有丢失任何数据,解压缩后,数据和压缩前完全一样。\n我们来看另一种无损压缩,它用更紧凑的方式表示数据块,有点像 \u0026ldquo;别忘了变厉害\u0026rdquo; 简写成 dftba ,为此,我们需要一个字典,存储\u0026quot;代码\u0026quot;和\u0026quot;数据\u0026quot;间的对应关系。我们看个例子,我们可以把图像看成一块块,而不是一个个像素。为了简单,我们把 2 个像素当成 1 块(占 6 个字节),但你也可以定成其他大小。我们只有四对: 白黄 黑黄 黄黄 白白 ,我们会为这四对 生成紧凑代码 (compact codes) 。有趣的是,这些块的出现频率不同。1950 年代 大卫·霍夫曼 发明了一种高效编码方式叫 \u0026ldquo;霍夫曼树\u0026rdquo;(huffman tree),当时他是麻省理工学院的学生,算法是这样的。\n首先,列出所有块和出现频率,每轮选两个最低的频率,这里 黑黄 和 白白 的频率最低,它们都是 1 ,可以把它们组成一个树,总频率 2 ,现在完成了一轮算法。现在我们重复这样做,这次有 3 个可选,就像上次一样,选频率最低的两个,放在一起,并记录总频率。好,我们快完成了。这次很简单,因为只有 2 个选择,把它们组合成一棵树就完成了!\n现在看起来像这样,它有一个很酷的属性:按频率排列,频率低的在下面。现在有了一棵树,你可能在想 \u0026ldquo;怎么把树变成字典?\u0026rdquo;\n我们可以把每个分支用 0 和 1 标注,就像这样。现在可以生成字典,黄黄 编码成 0 ,白黄 编码成 10 ,黑黄 编码成 110 ,白白 编码成 111 。酷的地方是它们绝对不会冲突,因为树的每条路径是唯一的,意味着代码是\u0026quot;无前缀\u0026quot;的,没有代码是以另一个代码开头的,现在我们来压缩!注意是位 (bit)! 不是字节 (byte)! 14 位 (bit) 还不到 2 个字节 (byte)!但,先别急着开香槟!字典也要保存下来,否则 14 bit 毫无意义,所以我们把字典 加到 14 bit 前面,就像这样。\n现在加上字典,图像是 30 个字节 (bytes) 比 48 字节好很多。\n\u0026ldquo;消除冗余\u0026quot;和\u0026quot;用更紧凑的表示方法\u0026rdquo;,这两种方法通常会组合使用。几乎所有无损压缩格式都用了它们,比如 gif, png, pdf, zip 。\n游程编码 和 字典编码 都是无损压缩!压缩时不会丢失信息,解压后,数据和之前完全一样。无损对很多文件很重要,比如我给你发了个压缩的 word 文档你解压之后发现内容变了,这就很糟糕了。但其他一些文件,丢掉一些数据没什么关系,丢掉那些人类看不出区别的数据。大多数有损压缩技术,都用到了这点。实际细节比较复杂,所以我们讲概念就好。以声音为例,你的听力不是完美的,有些频率我们很擅长,其他一些我们根本听不见,比如超声波,除非你是蝙蝠。举个例子,如果录音乐,超声波数据都可以扔掉 因为人类听不到超声波,另一方面,人类对人声很敏感,所以应该尽可能保持原样。低音介于两者之间,人类听得到,但不怎么敏感,一般是感觉到震动。有损音频压缩利用这一点,用不同精度编码不同频段,听不出什么区别,不会明显影响体验,音乐发烧友估计要吐槽了!日常生活中你会经常碰到这类音频压缩,所以你在电话里的声音和现实中不一样,压缩音频是为了让更多人能同时打电话,如果网速变慢了,压缩算法会删更多数据,进一步降低声音质量,所以 skype 通话有时听起来像机器人,和没压缩的音频格式相比,比如 wav 或 flac ( 这下音乐发烧友满意了),压缩音频文件如 mp3,能小 10 倍甚至更多。省了超多空间!\n所以我的旧 ipod 上有一堆超棒的歌,别批判我!\n这种删掉人类无法感知的数据的方法,叫 \u0026ldquo;感知编码\u0026rdquo; 。它依赖于人类的感知模型,模型来自\u0026quot;心理物理学\u0026quot;领域,这是各种\u0026quot;有损压缩图像格式\u0026quot;的基础,最著名的是 jpeg 。就像听力一样,人的视觉系统也不是完美的。我们善于看到尖锐对比,比如物体的边缘,但我们看不出颜色的细微变化。jpeg 利用了这一点,把图像分解成 8x8 像素块,然后删掉大量高频率空间数据。举个例子,这是导演的狗,面面,超可爱!我们来看其中一个 8x8 像素,几乎每个像素都和相邻像素不同,用无损技术很难压缩,因为太多不同点了,很多小细节,但人眼看不出这些细节,因此可以删掉很多,用这样一个简单的块来代替。这看起来一样,但可能只占 10%的原始数据。我们可以对所有 8x8 块做一样的操作,图片依然可以认出是一只狗,只是更粗糙一些,以上例子比较极端,进行了高度压缩,只有原始大小的八分之一。通常你可以取得平衡,图片看起来差不多,但文件小不少。你看得出两张图的区别吗?估计看不出。\n但我想提一下,视频压缩也造成了影响。毕竟你现在在看视频啊,视频只是一长串连续图片 所以图片的很多方面也适用于视频,但视频可以做一些小技巧。因为帧和帧之间很多像素一样,但视频可以做一些小技巧 因为帧和帧之间很多像素一样,比如我后面的背景!这叫 时间冗余 ,视频里不用每一帧都存这些像素,可以只存变了的部分,当帧和帧之间有小小的差异时,比如后面这个频率发生器,很多视频编码格式,只存变化的部分,这比存所有像素更有效率 - 利用了帧和帧之间的相似性。更高级的视频压缩格式会更进一步,找出帧和帧之间相似的补丁,然后用简单效果实现,比如移动和旋转,变亮和变暗。如果我这样摆手,视频压缩器会识别到相似性,用一个或多个补丁代表我的手,然后帧之间直接移动这些补丁,所以你看到的是我过去的手(不是实时的),有点可怕,但数据量少得多。mpeg-4 是常见标准,可以比原文件小 20 倍到 200 倍,但用补丁的移动和旋转来更新画面。当压缩太严重时会出错,没有足够空间更新补丁内的像素。即使补丁是错的,视频播放器也会照样播放,导致一些怪异又搞笑的结果,你肯定见过这些。\n总的来说,压缩对大部分文件类型都有用\n从这个角度来讲,人类不完美的视觉和听觉 也算有用。学习压缩非常重要 因为可以高效存储图片,音乐,视频。如果没有压缩,在 youtube 看\u0026quot;明星拼车唱歌\u0026quot;几乎不可能,因为你的带宽可能不够(会很卡) 而且供应商不愿意免费传输那么多数据。现在你知道为什么打 skype 电话,有时像在和恶魔通话。\n下周见。\n22. 命令行界面 i.e. keyboards \u0026amp; command line interfaces\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n我们之前讨论过输入输出 ,但都是计算机组件互相输入输出,比如 ram 输出数据,或输指令进 cpu 。我们还没讲来自人类的输入,也没讲怎么从电脑中拿出信息,除了用打孔纸卡。当然,有很多种 \u0026ldquo;输入输出设备\u0026rdquo; , 让我们和计算机交互,它们在人类和机器间提供了界面。如今有整个学科专门研究这个,叫 \u0026ldquo;人机交互\u0026rdquo;。界面对用户体验非常重要,所以是我们接下来几集的重点。\n在系列开头的几集,我们提过,早期机械计算设备用齿轮,旋钮和开关等机械结构来输入输出,这些就是交互界面。甚至早期电子计算机,比如 colossus 和 eniac,也是用一大堆机械面板和线来操作,输入一个程序可能要几星期,还没提运行时间。运行完毕后想拿出数据,一般是打印到纸上。打印机超有用,甚至查尔斯·巴贝奇都给差分机专门设计了一个,那可是 1820 年代!\n然而,到 1950 年代,机械输入完全消失,因为出现了打孔纸卡和磁带,但输出仍然是打印到纸上,还有大量指示灯,在运行中提供实时反馈。那个时代的特点是 ,尽可能迁就机器 ,对人类好不好用是其次。打孔纸带就是个好例子,就是为了方便计算机读取,纸带是连续的,方便机器处理。纸孔可以方便地用机械或光学手段识别,纸孔可以编码程序和数据,当然,人类不是以纸孔方式思考的。所以负担放到了程序员身上,他们要花额外时间和精力转成计算机能理解的格式,一般需要额外人员和设备帮忙。要注意的是,基本上 1950 年前的早期计算机,\u0026ldquo;输入\u0026quot;的概念很原始,是的,的确是人类负责输入程序和数据,但计算机不会交互式回应。程序开始运行后会一直运行,直到结束。因为机器太贵了,不能等人类慢慢敲命令和给数据,要同时放入程序和数据。\n这在 1950 年代晚期开始发生变化。一方面,小型计算机变得足够便宜,让人类来回和计算机交互变得可以接受,交互式就是人和计算机之间来回沟通。而另一方面,大型计算机变得更快,能同时支持多个程序和多个用户,这叫\u0026quot;多任务\u0026quot;和\u0026quot;分时系统\u0026rdquo;。\n但交互式操作时,计算机需要某种方法来获得用户输入,所以借用了当时已经存在的数据录入机制:键盘!当时,打字机已经存在几个世纪了,但现代打字机是克里斯托弗·莱瑟姆·肖尔斯在 1868 年发明的,虽然到 1874 年才完成设计和制造,但之后取得了商业成功。肖尔斯的打字机用了不寻常的布局,qwerty ,名字来自键盘左上角按键,为什么这么设计?有很多猜测。最流行的理论是,这样设计是为了把常见字母放得远一些,避免按键卡住。这个解释虽然省事,但可能是错的,或至少不够全面。事实上,qwerty 把很多常见字母放在了一起,比如 th 和 er 。我们知道 肖尔斯和他的团队设计了很多版才进化到这个布局。总之,肖尔斯的打字机取得了成功 ,所以其它公司很快开始抄他的设计。\n这……\r过去一个世纪有不少新的键盘布局被发明,宣称各种好处,但人们已经熟悉了 qwerty 布局 ,根本不想学新布局,这是经济学家所说的 转换成本。所以现在都快 1 个半世纪了 ,我们还在用 qwerty 键盘布局。我应该提一下,qwerty 不是通用的,有很多变体,比如法国 azerty 布局,以及中欧常见的 qwertz 布局。有趣的是,肖尔斯根本没想到打字会比手写快,手写速度大约是每分钟 20 个,打字机主要为了易读性和标准化,而不是速度,然而随着打字机成为办公室标配 ,对快速打字的渴望越来越大。\n有两个重大进步解放了打字的潜力。\n1880 年左右,辛辛那提速记学院一名叫伊丽莎白·朗利的老师开始推广十指打字,比一个手指打字要移动的距离短得多,所以速度更快。几年后,弗兰克·爱德华·麦克格林 盐湖城的一位联邦法庭书记学会了盲打,打字时不用看键盘。1888 年,麦格高林赢了备受关注的打字速度比赛,之后\u0026quot;十指盲打\u0026quot;开始流行。专业打字员每分钟 100 字以上,比手写快多了!而且清晰又整洁!虽然人类擅长用打字机,但我们没法把打字机塞到计算机面前,让它打字,计算机又没有手指。所以早期计算机用了一种特殊打字机,是专门用来发电报的,叫电传打字机。这些打字机是强化过的,可以用电报线发送和接收文本,按一个字母,信号会通过电报线,发到另一端,另一端的电传打字机会打出来,使得两人可以长距离沟通,基本是个蒸汽朋克版聊天室。因为电传打字机有电子接口,稍作修改就能用于计算机,电传交互界面在 1960~1970 很常见,用起来很简单,输入一个命令,按回车,然后计算机会输回来。用户和计算机来回\u0026quot;对话\u0026quot;,这叫 \u0026ldquo;命令行界面\u0026rdquo; 。它是最主要的人机交互方式,一直到 1980 年代,用电传打字机的命令行交互,类似这样:\n\u0026gt; 用户可以输入各种命令\r\u0026gt; 我们来看几个命令,先看当前目录有什么文件\r\u0026gt; 输入命令 ls,名字来自 list 的缩写\r\u0026gt; 然后计算机会列出 当前目录里的所有文件\r\u0026gt; 如果想看 secretstartrekdiscoverycast.txt 有什么\r\u0026gt; 要用另一个命令 显示文件内容\r\u0026gt; unix 用 cat 命令显示文件内容 cat 是连接 (concatenate) 的缩写\r\u0026gt; 然后指定文件名,指定的方法是写在 cat 命令后面 传给命令的值叫 参数\r\u0026gt; \u0026gt; 如果同一个网络里有其他人\r\u0026gt; 你可以用 finger 命令找朋友 就像是个很原始的\u0026#34;找朋友\u0026#34; app\r…… 电传打字机直到 1970 年代左右都是主流交互方式,尽管屏幕最早出现在 1950 年代,但对日常使用太贵而且分辨率低,然而因为针对普通消费者的电视机开始量产,同时处理器与内存也在发展。到 1970 年代,屏幕代替电传打字机变得可行,但与其为屏幕专门做全新的标准,工程师直接用现有的电传打字机协议,屏幕就像无限长度的纸,除了输入和输出字,没有其它东西。 协议是一样的,所以计算机分不出是纸还是屏幕 ,这些\u0026quot;虚拟电传打字机\u0026quot;或\u0026quot;玻璃电传打字机\u0026quot;叫 终端 。到 1971 年,美国大约有 7 万台电传打字机 以及 7 万个终端,屏幕又好又快又灵活。如果删一个错别字,会立刻消失,所以到 1970 年代末,屏幕成了标配。你也许会想,命令行界面太原始了,做不了什么有意思的事。即便只有文字,程序员也找到了一些方法,让它变得有趣一些。\n早期的著名交互式文字游戏 zork ,出现于 1977 年。早期游戏玩家需要丰富的想象力,想像自己身在虚构世界,比如\u0026quot;四周漆黑一片,附近可能有怪物会吃掉你\u0026quot;。我们用命令行玩玩看,就像之前,我们可以用 ls 命令,看当前目录有什么,然后用 cd 命令,进入游戏文件夹 cd 的意思是 \u0026ldquo;改变文件夹\u0026rdquo;(change directory),再用 ls 看有哪些游戏。超棒!我们有\u0026quot;冒险旅程\u0026quot;!(adventure)。想运行这个程序,只需要输入它的名字。在程序自行停止或我们主动退出前,它会接管命令行。\n你现在看到的,是\u0026quot;巨大洞穴冒险\u0026quot;这款游戏的真实输出,由 will crowther 在 1976 年开发。游戏中,玩家可以输入 1 个词或 2 个词的命令来移动人物,和其他东西交互,捡物品等,然后游戏会像旁白一样,输出你的当前位置,告诉你能做什么动作,以及你的动作造成的结果,有些动作会导致死亡!原始版本只有 66 个地方可供探索,但它被广泛认为是最早的互动式小说。游戏后来从纯文字进化成多人游戏 简称 mud,或多人地牢游戏(multi-user dungeons),是如今 mmorpg 的前辈 (大型多人在线角色扮演游戏, massive, multiplayer online role playing games)。如果你想了解游戏史,我们有游戏速成课 主持人 andre meadows 。\n命令行界面虽然简单,但十分强大。\n编程大部分依然是打字活, 所以用命令行比较自然,因此,即使是现在大多数程序员工作中依然用命令行界面,而且用命令行访问远程计算机 是最常见的方式, 比如服务器在另一个国家。如果你用 windows, macos, linux ,你的计算机有命令行界面,但你可能从来没用过,你可以在 windows 搜索栏中输入 cmd,或在 mac 上搜 terminal ,然后你可以装 zork 玩!\n现在你知道了,早期计算机的发展是如何影响到现在的。\n想想要是手机没有 qwerty 键盘 ,在 instagram 给图片配标题可就麻烦了。但我们还有一个重要话题没讲,美妙的图形界面!这是下周的主题!\n下周见。\n23. 屏幕 \u0026amp; 2d 图形显示 i.e. screens\u0026amp;2d graphics\n(。・∀・)ノ゙嗨,我是 carrie anne,欢迎收看计算机科学速成课!\n这台 1960 年的 pdp-1 是一个早期图形计算机的好例子,你可以看到左边是柜子大小的电脑,中间是电传打字机,右边是一个圆形的屏幕,注意它们是分开的,因为当时文本任务和图形任务是分开的。事实上,早期的屏幕无法显示清晰的文字,而打印到纸上有更高的对比度和分辨率。早期屏幕的典型用途是跟踪程序的运行情况,比如寄存器的值,如果用打印机一遍又一遍打印出来没有意义,不仅费纸而且慢。另一方面,屏幕更新很快,对临时值简直完美。但屏幕很少用于输出计算结果,结果一般都打印到纸上,或其它更永久的东西上。\n但屏幕超有用!到 1960 年代,人们开始用屏幕做很多酷炫的事情。几十年间出现了很多显示技术,但最早最有影响力的是阴极射线管(crt,cathode ray tubes)。原理是把电子发射到有磷光体涂层的屏幕上,当电子撞击涂层时,会发光几分之一秒,由于电子是带电粒子,路径可以用磁场控制,屏幕内用板子或线圈把电子引导到想要的位置,上下左右都行。既然可以这样控制,有 2 种方法绘制图形 :\n引导电子束描绘出形状,这叫 \u0026ldquo;矢量扫描\u0026rdquo; 。因为发光只持续一小会儿,如果重复得足够快,可以得到清晰的图像; 按固定路径,一行行来,从上向下,从左到右,不断重复,只在特定的点打开电子束,以此绘制图形,这叫 \u0026ldquo;光栅扫描\u0026rdquo; 。用这种方法,可以用很多小线段绘制形状,甚至文字。 最后,因为显示技术的发展,我们终于可以在屏幕上显示清晰的点,叫\u0026quot;像素\u0026quot; 。液晶显示器,简称 lcd(liquid crystal displays),和以前的技术相当不同,但 lcd 也用光栅扫描,每秒更新多次,像素里红绿蓝的颜色。有趣的是,很多早期计算机不用像素 - 不是技术做不到,而是因为像素占太多内存。 200 像素×200 像素的图像,有 40,000 个像素,哪怕每个像素只用一个 bit 表示 代表黑色或白色,连灰度都没有!会占 40,000 bit ,内存比 pdp-1 全部内存的一半还多,所以计算机科学家和工程师,得想一些技巧来渲染图形,等内存发展到足够用。所以 早期计算机不存大量像素值,而是存符号,80x25 个符号最典型,总共 2000 个字符。如果每个字符用 8 位表示,比如用 ascii ,总共才 16000 位,这种大小更合理。为此,计算机需要额外硬件来从内存读取字符,转换成光栅图形,这样才能显示到屏幕上,这个硬件叫 \u0026ldquo;字符生成器\u0026rdquo;,基本算是第一代显卡。它内部有一小块只读存储器,简称 rom ,存着每个字符的图形,叫 \u0026ldquo;点阵图案\u0026rdquo;。如果图形卡看到一个 8 位二进制,发现是字母 k ,那么会把字母 k 的点阵图案光栅扫描显示到屏幕的适当位置。为了显示,\u0026ldquo;字符生成器\u0026rdquo; 会访问内存中一块特殊区域,这块区域专为图形保留,叫 屏幕缓冲区 。程序想显示文字时,修改这块区域里的值就行,这个方案用的内存少得多,但也意味着只能画字符到屏幕上。即使有这样限制 ,人们用 ascii 艺术发挥了很多创意!也有人用字符模仿图形界面,用下划线和加号来画盒子,线,和其他简单形状,但字符集实在太小,做不了什么复杂的事,因此对 ascii 进行了各种扩展,加新字符,比如上图的 ibm cp437 字符集,用于 dos。\n用来读取字符,转换成光栅图形的硬件 - 字符生成器(‘第一代显卡’) ,有一个 rom,存储着每个字符的图形(‘点阵图案’) - 为了显示,访问内存中的一块特殊区域(‘屏幕缓冲区’)。\r屏幕缓冲区,是不是类似于内存和屏幕显示光栅的映射 ❓ 也是一层抽象 ❓\n某些系统上可以用额外的 bit 定义字体颜色和背景颜色,做出这样的 dos 界面 这界面只用了刚刚提到的字符集。字符生成器是一种省内存的技巧,但没办法绘制任意形状。制任意形状很重要,因为电路设计,建筑平面图,地图,好多东西都不是文字!为了绘制任意形状,同时不吃掉所有内存,计算机科学家用 crt 上的\u0026quot;矢量模式\u0026quot;。概念非常简单:所有东西都由线组成,没有文字这回事,只有线条,没有别的。明白了吗?好,我们举个实例吧!\n假设这个视频是一个 笛卡尔平面 200 个单位宽,100 个单位高,原点 (0,0) 在左上角,我们可以画形状,用如下矢量命令,这些命令来自 vectrex,一个早期矢量显示系统。首先,reset ,这个命令会清空屏幕,把电子枪的绘图点移动到坐标 (0,0),并把线的亮度设为 0 , move_to 50 50 把绘图点移动到坐标 (50,50) ,intensity 100 把强度设为 100 ,现在亮度提高了,移动到 (100,50) 然后 (60,75) 然后 (50,50) ,最后把强度设回 0 。酷,我们画了一个三角形!这些命令占 160 bit 比存一个庞大的像素矩阵更好。就像之前的\u0026quot;字符生成器\u0026quot; 把内存里的字符转成图形一样,这些矢量指令也存在内存中,通过矢量图形卡画到屏幕上。数百个命令可以按序存在屏幕缓冲区,画出复杂图形,全是线段组成的!由于这些矢量都在内存中,程序可以更新这些值,让图形随时间变化 - 动画!\n最早的电子游戏之一,spacewar! 是 1962 年在 pdp-1 上用矢量图形制作的。它启发了许多后来的游戏,比如 爆破彗星 (asteroids),甚至第一个商业街机游戏:太空大战 。\n1962 年是一个大里程碑 - sketchpad 诞生,一个交互式图形界面,用途是计算机辅助设计 (cad,computer-aided design),它被广泛认为是第一个完整的图形程序,发明人伊万·萨瑟兰后来因此获得图灵奖。为了与图形界面交互 ,sketchpad 用了当时发明不久的输入设备 - 光笔,就是一个有线连着电脑的触控笔,笔尖用光线传感器,可以检测到显示器刷新,通过判断刷新时间,电脑可以知道笔的位置,有了光笔和各种按钮,用户可以画线和其他简单形状。sketchpad 可以让线条完美平行,长度相同,完美垂直 90 度,甚至动态缩放,这些在纸上很费力,在计算机上非常简单!用户还可以保存设计结果,方便以后再次使用,甚至和其他人分享。你可以有一整个库,里面有电子元件和家具之类的,可以直接拖进来用。从如今的角度来看,好像很普通,但在 1962 年 计算机还是吃纸带的大怪兽,有柜子般大小,sketchpad 和光笔让人大开眼界,它们代表了人机交互方式的关键转折点 - 电脑不再是关在门后负责算数的机器了,可以当助手帮人类做事。\n最早用真正像素的计算机和显示器出现于 1960 年代末,内存中的位 (bit) 对应屏幕上的像素,这叫 位图显示。现在我们可以绘制任意图形了,你可以把图形想成一个巨大像素值矩阵。就像之前,计算机把像素数据存在内存中一个特殊区域 叫 \u0026ldquo;帧缓冲区\u0026rdquo;。早期时,这些数据存在内存里,后来存在高速视频内存里,简称 vram 。vram 在显卡上,这样访问更快,如今就是这样做的。在 8 位灰度屏幕上,我们可用的颜色范围是 0 强度(黑色),到 255 强度(白色)。其实更像绿色或橙色 ,因为许多早期显示器不能显示白色。我们假设这个视频在低分辨率的位图屏幕上,分辨率 60x35 像素。如果我们想把 (10,10) 的像素设为白色 可以用这样的代码,…… ,如果想画一条线,假设从 (30,0) 到 (30,35) 可以用这样一个循环,……,把整列像素变成白色,如果想画更复杂的图形,比如矩形,那么需要四个值:\n1. 起始点 x 坐标 2. 起始点 y 坐标 3. 宽度 4. 高度 目前只试了白色,这次画矩形试下灰色,灰色介于 0 到 255 中间 所以我们用 127 (255/2=127.5),然后用两个循环,一个套另一个,这样外部每跑一次,内部会循环多次 ,可以画一个矩形。计算机绘图时会用指定的颜色 127 ,我们来包装成 \u0026ldquo;画矩形函数\u0026rdquo;,就像这样:\n……\n假设要在屏幕的另一边,画第二个矩形,这次可能是黑色矩形,可以直接调用 \u0026ldquo;画矩形函数\u0026rdquo;, 超棒!\n就像之前说的其他方案,程序可以操纵\u0026quot;帧缓冲区\u0026quot;中的像素数据,实现交互式图形。当然,程序员不会浪费时间从零写绘图函数 而是用预先写好的函数来做,画直线,曲线,图形,文字等\n一层新抽象!\n位图的灵活性,为交互式开启了全新可能,但它的高昂成本持续了十几年,上集提到,1971 年 整个美国也只有大约 7 万个电传打字机和 7 万个终端,令人惊讶的是 只有大约 1000 台电脑有交互式图形屏幕,这可不多!\nsketchpad 和 太空大战 这样的先驱,推动了图形界面发展,帮助普及了计算机显示器 由此,图形界面的曙光初现,帮助普及了计算机显示器 由此,图形界面的曙光初现。\n接下来讲图形界面。\n下周见。\n24. 冷战和消费主义 i.e. the cold war and consumerism\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n之前介绍了计算机历史 从人类文明的曙光开始 (第 1 集),一直到 1940 年代中期电子计算机诞生,过去 23 集里讲的很多东西,比如编程语言和编译器,算法和集成电路,软盘(floppy disks)和操作系统,电报机和屏幕,全都是 1940~1970 年代,大概这 30 年间里出现的。那时苹果和微软还不存在,也没有推特,谷歌或者 uber 。还没到个人电脑时代,而万维网,无人驾驶汽车,虚拟现实等主题,这个系列的后半部分会讲。\n今天,我们不管电路和算法 来聊聊这个影响力巨大的时代!\n我们会把重点放在 冷战,太空竞赛,全球化,消费主义的兴起。1945 年二战结束后不久,两个超级大国的关系越发紧张,美国和苏联开始了冷战,因此政府往科学和工程学 投入大量资金。计算机在战时已经证明了自身的价值, 比如曼哈顿计划 和 破解纳粹通讯加密,所以政府大量投入资源 各种雄心勃勃的项目得以进行,比如之前提过的 eniac, edvac, atlas, whirlwind ,这种高速发展,如果仅靠商业运作是根本无法做到的 - 要依靠销售收回开发成本。\n1950 年代,事情开始发生变化,特别是 univac 1,它是第一台取得商业成功的电脑,不像 eniac 或 atlas univanc 1 不是一台机器,而是一个型号,一共造了 40 多台,大部分 univac 去了政府或大公司,成为美国日益增长的军事工业综合体的一部分,因为政府有钱承担这些尖端科技。一个著名的例子是,一台给 美国原子能委员会 生产的 univac 1 ,被 cbs 用来预测 1952 年美国总统大选的结果,仅用 1%的选票,univac 1 正确预测了结果。艾森豪威尔 获得压倒性胜利,而专家预测 史蒂文森 会赢,这次事件把计算机推到了公众面前。\n计算机和以前的机器不一样,以前的机器 增强人类的物理能力,比如卡车能带更多东西,自动织布机更快,机床更精确 等等。这些东西代表了工业革命。而计算机增强的是人类智力,范内瓦·布什 看到了这种潜力。他在 1945 年发表了一篇文章,描述了一种假想计算设备叫 memex。可以用这个设备 存自己所有的书,其他资料 以及和别人沟通,而且数据是按照格式存储,所以可以快速查询,有很大灵活性,可以辅助我们的记忆。他还预测会出现新的百科全书形式,信息之间相互链接,听起来是不是很熟悉?(维基百科)\nmemex 启发了之后几个重要里程碑,比如上集 伊万·萨瑟兰 的 sketchpad(画板),以及后面很快会讲到 dough engelbart 的 on-line 系统(第 26 集)。\n范内瓦·布什 做过\u0026quot;美国科学研究与开发办公室\u0026quot;的头头,这个部门负责在二战期间 资助和安排科学研究。冷战时, 范内瓦·布什 到处游说,想建立一个职责类似,但是在和平时期运作的部门,因此 国家科学基金会 于 1950 年成立,至今,国家科学基金会 依然负责给科学研究 提供政府资金。美国的科技领先全球,主要原因之一就是这个机构。\n1950 年代,消费者开始买晶体管设备,其中值得注意的是 收音机,它又小又耐用,用电池就够了,而且便携,不像 1940 年代之前的收音机,用的是真空管。收音机非常成功,卖的像\u0026quot;菲比精灵\u0026quot;和 iphone 一样畅销。日本政府也在寻求工业机会,想振兴战后经济,他们很快动手从贝尔实验室 取得晶体管的授权,帮助振兴日本的半导体和电子行业。1955 年,索尼的第一款产品面世 - tr-55 晶体管收音机。他们把重心放在质量和价格,因此日本公司在短短 5 年内,就占有了美国便携式收音机市场的一半。这为日本成为美国的强大工业对手,埋下伏笔。\n1953 年,整个地球大概有 100 台计算机,苏联这时的计算机科技只比西方落后几年。苏联在 1950 年 ,完成了第一个可编程电子计算机,但苏联在太空竞赛远远领先。\n我们进入思想泡泡\n苏联在 1957 年 把第一个卫星送上轨道,史波尼克 1 号。不久,在 1961 年,苏联宇航员 尤里·加加林 第一个进入太空,美国民众对此不满,使得肯尼迪总统 在加加林太空任务一个月后,提出要登陆月球。登月很贵的!nasa 的预算增长了几乎十倍,在 1966 年达到顶峰,占了政府预算的 4.5% ,如今,nasa 的预算只占 0.5% 。nasa 用这笔钱资助各种科学研究,阿波罗计划花的钱最多,雇了 40 万人左右,而且有 2 万多家大学和公司参与。其中一个挑战是 怎样在太空中导航,nasa 需要电脑计算复杂的轨道来引导太空船,因此,他们造了 \u0026ldquo;阿波罗导航计算机\u0026rdquo;,有 3 个重要要求:\n计算机要快,这在意料之中; 计算机要又小又轻。太空船里的空间不多,而且要飞去月球,能轻一点是一点; 要超级可靠。 这对太空船非常重要,因为太空中有很多震动,辐射,极端温度变化。如果东西坏掉了,可没办法去\u0026quot;百思买\u0026quot;买新的。那时的主流科技 真空管和晶体管 无法胜任这些要求,所以 nasa 用全新科技:集成电路。\n我们几集前聊过,阿波罗导航计算机 首先使用了集成电路,nasa 是唯一负担得起集成电路的组织。最初,一个芯片差不多 50 美金,导航计算机需要上千个芯片,但美国也因此成功登月,打败苏联。\n谢了 思想泡泡\n虽然人们经常把集成电路的发展 归功于阿波罗导航计算机,但它们的产量很低,一共只有 17 次阿波罗任务。实际上是军事 大大推进了集成电路发展,特别是洲际导弹和核弹,使集成电路大规模生产。美国建造强大计算机时,也进一步推进了集成电路,一般叫\u0026quot;超级计算机\u0026quot;,因为它们经常比全球最快电脑还快 10 倍以上,但 cdc,cray,ibm 制造的计算机非常昂贵,几乎只有政府负担得起,这些计算机用于政府机构,比如美国国家安全局,以及实验室比如 劳伦斯·利弗莫尔 实验室 、 洛斯·阿拉莫斯 国家实验室。\n最初,美国的半导体行业 靠高利润政府合同起步,因此忽略了消费者市场,因为利润小。因此日本半导体行业在 1950 和 1960 年代 靠低利润率占领了消费者市场,日本人投入大量资金,大量制造以达到规模经济,同时研究技术,提高质量和产量 以及用自动化来降低成本。1970 年代,太空竞赛和冷战逐渐消退 高利润的政府合同变少,美国的半导体和电子设备公司发现更难竞争了。虽然很多计算机组件商品化了,但并没有什么帮助。dram 就是 dram ,能从日立买便宜的,干嘛要从英特尔买贵的? 1970 年代 美国公司开始缩小,合并,或直接倒闭 。1974 年 英特尔不得不裁员三分之一 ,知名的仙童半导体也在 1979 年濒临倒闭 ,被其他公司收购了。为了生存,很多公司把生产外包出去,降低成本。英特尔不再把精力放在 内存集成电路, 而是把精力放在处理器,这个决定最后挽救了公司。美国公司的无力 ,导致 夏普 和 卡西欧 这样的日本公司占领了 1970 年代的主流产品 - 手持计算器。因为集成电路,计算机又小又便宜。,取代了办公室里昂贵的桌面计算器。对大多数人 这是他们第一次不必用纸笔和计算尺来做计算,手持计算机因此大卖,进一步降低了集成电路的成本,使得微处理器被广泛使用,比如之前讨论过的 intel 4004 。intel 在 1971 年 应日本计算器公司 busicom 的要求做了这个芯片,很快,日本电子产品到处都是,从电视到手表到随身听,而廉价的微处理器,也催生了全新的产品,比如街机游戏。1972 年诞生了 pong,1976 年诞生了打砖块。因为成本不断下降,很快,普通人也买得起计算机了,这段期间,第一批家用电脑开始出现,比如 1975 年的 altair 8800,以及第一款家用游戏机,比如 1977 年的 atari 2600 。家用!我再说一遍 家用!如今没什么大不了的,但那时是计算机的全新时代!\n在短短三十年内,计算机从大到人类可以在 cpu 里走来走去(当然,你要有政府许可你这样做),发展到小到小孩都能拿住的手持玩具,而且微处理器还快得多。这种巨大变化是由两种力量推动的:政府和消费者!政府资金,比如冷战期间美国投入的钱,推动了计算机的早期发展,并且让计算机行业活得足够久,使得技术成熟到可以商用。然后是公司,最后是消费者,把计算机变成了主流。冷战虽然结束了,但这种关系今天仍在继续。政府依然在资助科学研究,情报机构依然在超级计算机,人类仍然被发射到太空里,而你依然在买电视,xbox,playstation,笔记本电脑和手机。\n因此,计算机会继续飞速发展。\n我们下周见。\n25. 个人计算机革命 i.e. the personal computer revolution\n(。・∀・)ノ゙嗨,我是 carrie anne 欢迎收看计算机科学速成课!\n上周说过\u0026quot;个人计算机\u0026quot;的概念 ,在计算机发展的头 30 年难以想象,如果只让一个人用,成本实在太高。但到 70 年代初,各种组件的成本都下降了 可以做出低成本 同时性能足够强大的计算机。不是玩具级计算机,是真正能用的计算机。这个转变中 最有影响力的是 单芯片 cpu 的出现,强大 + 体积小 + 便宜 ,集成电路的进步,也提供了低成本固态存储器,可以用于计算机的 ram 和 rom 。忽然间 把整台计算机做到一张电路板上成为可能,大大地降低了制造成本,而且,那时有便宜可靠的储存介质, 比如磁带和软盘,最后是 低成本的显示器 ,通常是电视机稍作改装而成。如果在 1970 年代 将这四种原料混在一起,就得到了\u0026quot;微型计算机\u0026quot;。因为和那个时代的\u0026quot;普通\u0026quot;计算机相比 ,这些计算机很小,\u0026ldquo;普通\u0026quot;计算机就是公司或大学里的那种。但比大小更重要的是成本,这是有史以来第一次,计算机的价格足够低,\u0026ldquo;一个人专用\u0026quot;的想法变得可行,不用划分时间和别人公用计算机,没有多用户登录,计算机只属于一个人,只有一个用户,个人计算机时代到来!\n计算机成本下降+性能提升,让个人计算机成为可能,但这个时间点很难准确定义,并没有一个具体时间点,因此\u0026quot;第一台个人计算机\u0026quot;这个名号,有很多竞争者,比如 kenback-1 和 mcm/70 。不过第一台取得商业成功的个人计算机 争议较小:altair 8800,首次亮相在 1975 年《popular electronics》封面,售价 $439 美元,需要自己组装。计算通货膨胀后,相当如今的 2000 美元左右,不算小钱,但比起 1975 年的其它计算机,算是非常便宜了!\n各种需要自己组装的组件包卖给了计算机爱好者,因为买的人多,很快相关产品出现了,比如内存,纸带读取器,甚至电传接口,让你可以从纸带上读取更长更复杂的程序,然后用电传终端交互,但程序还是要用 机器码 写,写起来很麻烦,即使计算机爱好者也讨厌写,这没有吓跑年轻的比尔·盖茨和保罗·艾伦!他们当时是 19 岁和 22 岁,他们联系了制造 altair 8800 的 mits 公司,建议说,如果能运行 basic 程序 会对爱好者更有吸引力。basic 是一门更受欢迎更简单的编程语言,为此,他们需要一个程序 把 basic 代码转成可执行机器码,这叫 解释器 (interpreter)。\u0026ldquo;解释器\u0026quot;和\u0026quot;编译器\u0026quot;类似,区别是\u0026quot;解释器\u0026quot;运行时转换, 而\u0026quot;编译器\u0026quot;提前转换。\n让我们进入思想泡泡!\nmits 表示感兴趣,同意与 bill 和 paul 见个面,让他们演示一下。问题是,他们还没写好解释器,所以他们花了几个星期赶工 ,而且还不是在 altair 8800 上写的,最后在飞机上完成了代码。他们在墨西哥 阿尔伯克基(城市) 的 mits 总部做演示时,才知道代码可以成功运行。幸运的是进展顺利 mits 同意在计算机上搭载他们的软件,altair basic 成了微软的第一个产品。\n虽然 1975 年之前就有计算机爱好者,但 altair 8800 大量催生了更多计算机爱好者,爱好者们组成各种小组 分享知识,软件,以及对计算机的热爱,最具传奇色彩的小组是\u0026quot;家酿计算机俱乐部\u0026rdquo;。第一次小组聚会在 1975 年 3 月,看一台第一批运来加州的 altair 8800 。第一次聚会上,24 岁的 steve wozniak 被 altair 8800 大大激励,开始想设计自己的计算机。1976 年 5 月,他向小组展示了原型机,并且把电路图分享给感兴趣的其他会员,他的设计不同寻常 要连到电视显示,并提供文本界面,在低成本计算机上还是第一次见。同是俱乐部成员和大学朋友的 史蒂夫·乔布斯 建议说与其免费分享设计,不如直接出售装好的主板,但用户依然需要自己加键盘,电源和机箱。1976 年 7 月开始发售,价格 $666.66 美元,它叫 apple-i ,苹果计算机公司的第一个产品。\n谢了 思想泡泡\n就像 altair 8800 一样,apple-i 也是作为套件出售,apple-i 吸引了业余爱好者 不介意机器买回来自己组装,但个人消费者和公司对 apple-i 不感兴趣。\n这在 1977 年发生变化 市场上有了三款开箱即用的计算机。\n第一款是 apple-ii ,苹果公司第一个提供全套设备的产品,设计和制造工艺都是专业的,它还提供了简单彩色图形和声音输出,这些功能对低成本机器非常了不起。apple-ii 卖了上百万套,把苹果公司推到了个人计算机行业的前沿。第二款是\u0026quot;trs-80 1 型\u0026rdquo;,由 tandy 公司生产,由 radioshack 销售,所以叫 trs。虽然不如 apple-ii 先进 但因为价格只有一半,所以卖得很火爆。最后一款是 commodore pet 2001 ,有一体化设计,集成了计算机,显示器,键盘和磁带驱动器,目标是吸引普通消费者。\n计算机和家用电器之间的界限开始变得模糊,这 3 台计算机被称为 1977 年的\u0026quot;三位一体\u0026rdquo; 。它们都自带了 basic 解释器,让不那么精通计算机的人也能用 basic 写程序,针对消费者的软件行业 开始腾飞。市场上出现了各种 针对个人计算机的游戏和生产力工具,比如计算器和文字处理器,最火的是 1979 年的 visicalc - 第一个电子表格程序,比纸好无数倍,是微软 excel 和 google sheets 的老祖先。\n但这些计算机带来的最大影响 也许是他们的营销策略,它们针对普通消费者, 而不是企业和爱好者。这是第一次大规模地,计算机出现在家庭,小公司,以及学校中,这引起了全球最大计算机公司 ibm 的注意,其市场份额从 1970 年的 60% 在 1980 年降到了 30%左右,因为 ibm 忽略了增长的\u0026quot;微型计算机\u0026quot;市场,这个市场每年增长约 40% 。随着微型计算机演变成个人计算机 ibm 知道他们需要采取行动,但要做到这一点 公司要从根本上重新思考战略和设计 。1980 年 ibm 最便宜的计算机 \u0026ldquo;5120\u0026quot;的价格大概是一万美元,永远也没法和 apple-ii 这样的计算机竞争,意味着要从头开始。一个由十二名工程师组成的精干团队(后来叫\u0026quot;肮脏十二人\u0026rdquo;),被派往佛罗里达州的 博卡拉顿(boca raton)办公室,让他们独立工作。不受 ibm 内部的政治斗争干扰 他们想怎么设计怎么设计,没用 ibm 的 cpu,选了 intel 的芯片,也没用 ibm 的首选操作系统 cp/m ,而是用了微软的 dos 。依此类推,从屏幕到打印机都这样自由选择 。ibm 第一次不得不与外部公司竞争,来给新计算机做硬件和软件,这和 ibm 的传统做法不同:自己做硬件来节省成本,然后和其它公司合作,经过短短一年,ibm 个人计算机发布了,简称 ibm pc,产品立马取得了成功。长期信任 ibm 品牌的企业买了很多,但最有影响力的是 它使用 \u0026ldquo;开放式架构\u0026rdquo;,有良好的文档和扩展槽,使得第三方可以做硬件/外设 - 包括显卡,声卡,外置硬盘,游戏控制杆 以及无数其它组件,这刺激了创新,激发了竞争,产生了巨大的生态系统,这个开放架构叫 ibm compatible\u0026quot;( ibm 兼容 ),意味着如果买了\u0026quot;ibm 兼容\u0026quot;的计算机,你可以用庞大生态系统中的其它软硬件。开放架构也意味着 竞争对手公司可以遵循这个标准,做出自己的\u0026quot;ibm 兼容\u0026quot;计算机。很快,康柏和戴尔也开始卖 pc ,微软很乐意把 ms-dos 授权给他们,使 dos 迅速成为最受欢迎的 pc 操作系统。仅在前三年 ibm 就卖出了 200 万台 pc ,超过了苹果。有了庞大用户群,软件和硬件开发人员 把精力放在\u0026quot;ibm 兼容\u0026quot;平台,因为潜在用户更多,同时,想买计算机的人 也会看哪种计算机的软硬件选择更多,就像雪球效应一样,而那些生产非\u0026quot;ibm 兼容\u0026quot;计算机的公司 (一般性能更好),都失败了。只有苹果公司在没有\u0026quot;ibm 兼容\u0026quot;的情况下 保持了足够市场份额,苹果公司最终选了相反的方式:\u0026ldquo;封闭架构\u0026rdquo;,即自己设计一切,用户一般无法加新硬件到计算机中,意味着苹果公司要做自己的计算机,自己的操作系统,还有自己的外围设备,如显示器,键盘和打印机。通过控制整个范围,从硬件到软件,苹果能控制用户体验并提高可靠性。不同的商业策略是 \u0026ldquo;mac vs pc 谁更好\u0026rdquo; 这种争论的起源,这些争论如今还存在 不过\u0026quot;mac vs pc\u0026quot;用词不对,因为它们都是个人计算机!但是随便啦!\n为了在低成本个人计算机的竞争冲击下生存下来,苹果需要提高自身水平 提供比 pc 和 dos 更好的用户体验,他们的答案是 macintosh,于 1984 年发布 - 一台突破性 价格适中的一体式计算机 ,用的不是命令行界面,而是图形界面!\n我们下周讨论图形界面。到时见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/15/","summary":"\u003cblockquote\u003e\n\u003cp\u003e后续章节为概念性章节,统一汇总在该章节内。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc15 - 其他 1"},{"content":"i.e. data structures\n上集讲了一些经典算法,比如给数组排序,找图的最短路径,而上集没讲的是 算法处理的数据 存在内存里的格式是什么。\n你肯定不想数据像 john green 的大学宿舍一样乱 ,到处都是食物,衣服和纸,我们希望数据是结构化的,方便读取,因此计算机科学家发明了 \u0026ldquo;数据结构\u0026rdquo;!\n上集已经介绍了一种基本数据结构:数组(array),也叫列表(list)或向量(vector)(在其它编程语言里)。数组的值一个个连续存在内存里,所以不像之前,一个变量里只存一个值(比如 j = 5),我们可以把多个值存在数组变量里。为了拿出数组中某个值,我们要指定一个下标(index),大多数编程语言里,数组下标都从 0 开始,用方括号 [ ] 代表访问数组。如果想相加数组 j 的第一个和第三个元素,把结果存在变量 a,可以写上图这样一行代码。\n数组存在内存里的方式十分易懂。\n为了简单,假设编译器从内存地址 1000 开始存数组,数组有 7 个数字,像上图一样按顺序存。写 j[0],会去内存地址 1000 加 0 个偏移,得到地址 1000,拿值:5 ,如果写 j[5],会去内存地址 1000 加 5 个偏移,得到地址 1005,拿值: 4 。很容易混淆 \u0026ldquo;数组中第 5 个数\u0026rdquo; 和 \u0026ldquo;数组下标为 5 的数\u0026rdquo;,它们不是一回事,记住,下标 5 其实是数组中第 6 个数,因为下标是从 0 开始算的。\n数组的用途广泛。所以几乎所有编程语言都自带了很多函数来处理数组,举例,数组排序函数很常见,只需要传入数组,就会返回排序后的数组,不需要写排序算法。\n数组的亲戚是 字符串 (string),其实就是字母,数字,标点符号等 组成的数组。第 4 集讨论过计算机怎么存储字符,写代码时 用引号括起来就行了 j = \u0026quot;stan rocks\u0026quot; 。虽然长的不像数组,但的确是数组,幕后看起来像这样。\n注意,字符串在内存里以 0 结尾。不是\u0026quot;字符 0\u0026quot;,是\u0026quot;二进制值 0\u0026quot;,这叫字符\u0026quot;null\u0026quot;,表示字符串结尾。这个字符非常重要,如果调用 print 函数,print 在屏幕上输出字符串,会从开始位置,逐个显示到屏幕,但得知道什么时候停下来!否则会把内存里所有东西 都显示出来,0 告诉函数何时停下。\n因为计算机经常处理字符串,所以有很多函数专门处理字符串,比如连接字符串的 strcat,strcat 接收两个字符串,把第二个放到第一个结尾。\n我们可以用数组做一维列表(one dimensional lists),但有时想操作二维数据,比如电子表格,或屏幕上的像素,那么需要 矩阵(matrix)- 可以把矩阵看成 数组的数组!\n一个 3x3 矩阵就是一个长度为 3 的数组 ,数组里每个元素都是一个长度为 3 的数组。可以这样初始化,内存里是这样排列的。为了拿一个值,需要两个下标,比如 j[2][1] ,告诉计算机在找数组 2 里,位置是 1 的元素,得到数字 12 。\n矩阵酷的地方是,不止能做 3x3 的矩阵,任何维度都行,可以做一个 5 维矩阵,然后这样访问 a = j[2][0][18][18][3] 。现在你知道了,怎么读一个 5 维矩阵,快去告诉你的朋友!\n目前我们只存过单个数字/字符,存进数组或矩阵,但有时,把几个有关系的变量存在一起,会很有用。比如银行账户号和余额,多个变量打包在一起叫 结构体 (struct)。现在多个不同类型数据,可以放在一起,甚至可以做一个数组,里面放很多结构体,这些数据在内存里会自动打包在一起。如果写 j[0],能拿到 j[0] 里的结构体,然后拿银行账户和余额。存结构体的数组,和其它数组一样,创建时就有固定大小,不能动态增加大小,还有,数组在内存中 按顺序存储,在中间插入一个值很困难。\n结构体可以创造更复杂的数据结构,消除这些限制,我们来看一个结构体,叫 节点 (node),它存一个变量 - 一个指针(pointer)。\u0026ldquo;指针\u0026rdquo; 是一种特殊变量,指向一个内存地址,因此得名。用 节点 可以做 链表(linked list),链表是一种灵活数据结构,能存很多个 节点 (node),灵活性是通过每个节点 指向 下一个节点实现的。\n假设有三个节点,在内存地址 1000,1002, 1008,隔开的原因 可能是创建时间不同,它们之间有其他数据。可以看到第一个节点,值是 7,指向地址 1008,代表下一个节点,位于内存地址 1008,现在来到下一个节点,值是 112,指向地址 1002,如果跟着它,会看到一个值为 14 的节点,这个节点 指回地址 1000,也就是第一个节点,这叫 循环链表 。\n但链表也可以是非循环的,最后一个指针是 0 - \u0026ldquo;null\u0026rdquo;,代表链表尽头。当程序员用链表时,很少看指针具体指向哪里,而是用链表的抽象模型,就像上图,更容易看懂。\n数组大小需要预先定好,链表大小可以动态增减。可以创建一个新节点,通过改变指针值,把新节点插入链表;链表也很容易重新排序,两端缩减,分割,倒序等。超方便!\n链表也适合上集的排序算法,因为灵活,很多复杂数据结构 都用链表,最出名的是 队列(queue)和 栈。\u0026ldquo;队列\u0026rdquo; 就像邮局排队,谁先来就排前面,虽然你可能只想买邮票,而前面的人要寄 23 个包裹,这叫 先进先出(fifo)。我指队列,不是指那 23 个包裹。想象有个指针叫\u0026quot;邮局队列\u0026quot;,指向链表第一个节点。第一个节点是 hank,服务完 hank 之后 读取 hank 的指针,把\u0026quot;邮局队列\u0026quot;指向下一个人,这样就把 hank \u0026ldquo;出队\u0026rdquo;(dequeue)了。如果我们想把某人\u0026quot;入队\u0026quot;(enqueue) - 意思是加到队列里,要遍历整个链表到结尾,然后把结尾的指针,指向新人(nick)。\n只要稍作修改,就能用链表做 栈,栈是后进先出 (lifo)。可以把\u0026quot;栈\u0026quot;想成一堆松饼。做好一个新松饼,就堆在之前上面,吃的时候,是从最上面开始。美味!栈就不叫\u0026quot;入队\u0026quot;\u0026ldquo;出队\u0026quot;了,叫\u0026quot;入栈\u0026rdquo;(push) \u0026ldquo;出栈\u0026rdquo;(pop)。对,这些是正确术语!\n如果节点改一下,改成 2 个指针,就能做 树(tree)。很多算法用了 \u0026ldquo;树\u0026rdquo; 这种数据结构。同样,程序员很少看指针的具体值,而是把\u0026quot;树\u0026quot;抽象成这样:最高的节点叫\u0026quot;根节点\u0026quot;(root),\u0026ldquo;根节点\u0026quot;下的所有节点都叫\u0026quot;子节点\u0026rdquo;(children)。任何子节点的直属上层节点,叫\u0026quot;母节点\u0026quot;(parent node)。没有任何\u0026quot;子节点\u0026quot;的节点,也就是\u0026quot;树\u0026quot;结束的地方,叫\u0026quot;叶节点\u0026quot;(leaf)\n在这里的例子中,节点最多只可以有 2 个子节点,因此叫 二叉树(binary tree)。但你可以随便改,弄成 3 个,4 个,或更多,甚至节点 可以用链表存所有子节点。\u0026ldquo;树\u0026quot;的一个重要性质是(不管现实中还是数据结构中),\u0026ldquo;根\u0026quot;到\u0026quot;叶\u0026quot;是 单向 的。如果根连到叶,叶连到根,就很奇怪。\n如果数据随意连接,包括循环,可以用\u0026quot;图\u0026quot;表示,还记得上集用路连接城市的\u0026quot;图\u0026quot;吗?这种结构可以用有多个指针的节点表示,因此没有 根、叶、子节点、父节点这些概念,可以随意指向!\n以上概述了计算机科学中,最主要的一些数据结构,这些基本结构之上,程序员做了各种新变体,有不同性质。比如\u0026quot;红黑树\u0026quot;和\u0026quot;堆\u0026rdquo;,我们没时间讲。\n不同数据结构适用于不同场景,选择正确数据结构会让工作更简单,所以花时间考虑用什么数据结构是值得的。幸运的是,大多数编程语言自带了预先做好的数据结构。比如,c++有\u0026quot;标准模板库\u0026rdquo;,java 有\u0026quot;java 类库\u0026quot;,程序员不用浪费时间从零写,时间可以花在更有趣的事情。\n又提升了一层抽象!\n下周见!\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/14/","summary":"\u003cp\u003ei.e. Data Structures\u003c/p\u003e\n\u003cp\u003e上集讲了一些经典算法,比如给数组排序,找图的最短路径,而上集没讲的是 算法处理的数据 存在内存里的格式是什么。\u003c/p\u003e\n\u003cp\u003e你肯定不想数据像 John Green 的大学宿舍一样乱 ,到处都是食物,衣服和纸,我们希望数据是结构化的,方便读取,因此计算机科学家发明了 \u0026ldquo;数据结构\u0026rdquo;!\u003c/p\u003e","title":"cscc14 - 数据结构"},{"content":"i.e. intro to algorithms\n前两集,我们\u0026quot;初尝\u0026quot;了高级编程语言(比如 python 和 java),我们讨论了几种语句 - 赋值语句,if 语句,循环语句,以及把代码打包成 \u0026ldquo;函数\u0026rdquo;,比如算指数。重要的是,之前写的指数函数只是无数解决方案的一种,还有其它方案 - 用不同顺序写不同语句,也能得到一样结果。\n不同的是\u0026quot;算法\u0026quot;,意思是:解决问题的具体步骤,即使结果一致,有些算法会更好。 一般来说,所需步骤越少越好,不过有时我们也会关心其他因素,比如占多少内存。\n\u0026ldquo;算法\u0026rdquo; 一词来自 波斯博识者 阿尔·花拉子密, 1000 多年前的代数之父之一 ,如何想出高效算法 - 是早在计算机出现前就有的问题,诞生了专门研究计算的领域,然后发展成一门现代学科。\n你猜对了!计算机科学!\n记载最多的算法之一是\u0026quot;排序\u0026quot; ,比如给名字、数字排序。排序到处都是,找最便宜的机票、按最新时间排邮件、按姓氏排联系人,这些都要排序。你可能想\u0026quot;排序看起来不怎么难… 能有几种算法呢?\u0026ldquo;答案是超多!\n计算机科学家花了数十年发明各种排序算法。还起了酷酷的名字,\u0026ldquo;冒泡排序\u0026rdquo; (bubble sort)、\u0026ldquo;意面排序\u0026rdquo;(spaghetti sort),我们来试试排序!\n选择排序 试想有一堆机票价格,都飞往印第安纳波利斯 (美国地名),数据具体怎么在内存中表示 下周再说。\n上图的这样一组数据 叫\u0026quot;数组\u0026rdquo;(array)。\n来看看怎么排序,先从一种简单算法开始。\n- 先找到最小数,从最上面的 307 开始,因为现在只看了这一个,所以它是最小数\r- 下一个是 239,比 307 小,所以新的最小数变成 239\r- 下一个是 214 ,新的最小数\r- 250 不是,384, 299, 223, 312 都不是\r- 现在扫完了所有数字,214 是最小的\r- 为了升序排列(从小到大排序),把 214 和最上面的数字,交换位置 好棒!刚排序了一个数字!现在重复同样的过程!\n- 这次不从最上面开始,从第 2 个数开始,先看到 239,我们当作是 \u0026#34;最小数\u0026#34;\r- 扫描剩下的部分,发现 223 最小,所以把它和第 2 位交换\r- 重复这个过程,从第 3 位数字开始,让 239 和 307 互换位置\r- 重复直到最后一个数字 瞧,数字排好了,可以买机票了!\n刚刚这种方法,或者说算法,叫 选择排序 - 非常基础的一种算法。\n以下是\u0026quot;伪代码\u0026quot;(pseudo-code)。\n这个函数可以排序 8 个,80 个或 8 千万个数字,函数写好了就可以重复使用,这里用循环遍历数组,每个数组位置都跑一遍循环,找最小数然后互换位置。可以在代码中看到这一点 (一个 for 循环套另一个 for 循环),这意味着,大致来说,如果要排 n 个东西,要循环 n 次,每次循环中再循环 n 次,共 n*n 。\n算法的 输入大小 和 运行步骤 之间的关系,叫算法的 复杂度 ,表示运行速度的量级。计算机科学家们把算法复杂度叫 - 没开玩笑 - 大 o 表示法 。\n算法复杂度 o(n) 效率不高。前面的例子有 8 个元素(n=8), 8 = 64,如果 8 个变 80 个,运行时间变成 80 = 6400,虽然大小只增长了 10 倍(8 到 80),但运行时间增加了 100 倍!(64 到 6400 )。随着数组增大,对效率的影响会越来越大,这对大公司来说是个问题,比如 谷歌,要对几十亿条信息排序。\n= 数据量上来了,一切都会变得复杂了!*\n作为未来的计算机科学家你可能会问:有没有更高效的排序算法?\n归并排序 回到未排序的数组,试另一个算法 \u0026ldquo;归并排序\u0026rdquo;。\n- 第一件事是检查数组大小是否 \u0026gt; 1\r- 如果是,就把数组分成两半\r- 因为数组大小是 8,所以分成两个数组,大小是 4\r- 但依然大于 1,所以再分成大小是 2 的数组\r- 最后变成 8 个数组,每个大小为 1\r- 现在可以\u0026#34;归并\u0026#34;了,\u0026#34;归并排序\u0026#34;因此得名\r- - 从前两个数组开始,读第一个(也是唯一一个)值\r- 307 和 239\r- 239 更小,所以放前面\r- 剩下的唯一数字是 307 ,所以放第二位\r- 成功合并了两个数组 重复这个过程,按序排列,然后再归并一次。\n- 同样,取前两个数组,比较第一个数\r- 239 和 214 - 214 更小,放前面\r- - 再看两个数组里的第一个数:239 和 250\r- 239 更小,所以放下一位\r- - 看剩下两个数:307 和 250\r- 250 更小,所以放下一位\r- - 最后剩下 307 ,所以放最后\r- - 每次都以 2 个数组开始\r- 然后合并成更大的有序数组 我们把刚隐藏起来的,下面的数组也这样做。\n现在有两个大小是 4 的有序数组,就像之前,比较两个数组的第一个数,取最小数,重复这个过程,直到完成,就排好了!\n但坏消息是:无论排多少次,你还是得付 214 美元到印第安纳波利斯。总之,\u0026ldquo;归并排序\u0026quot;的算法复杂度是 o(n * log n),\nn 是需要 比较+合并 的次数,和数组大小成正比 log n 是合并步骤的次数 例子中把大小是 8 的数组,分成四个数组,然后分成 2 个,最后分成 1 个,分了 3 次。重复切成两半,和数量成对数关系\n相信我!\nlog_2 8=3\n如果数组大小变成 16 - 之前的两倍,也只要多分割 1 次,因为 log_2 16=4 ,即使扩大一千倍,从 8 到 8000,分割次数也不会增大多少 - log_2 8000≈13 ,13 比 3 只是 4 倍多一点,然而排序的元素多得多,因此\u0026quot;归并排序\u0026quot;比\u0026quot;选择排序\u0026quot;更有效率。\n这下我收藏的陶瓷猫 可以更快排序了!\n有好几十种排序算法,但没时间讲。\n图搜索 所以我们来谈一个经典算法问题:图搜索(graph search)。\n\u0026ldquo;图\u0026rdquo; 是用线连起来的一堆 \u0026ldquo;节点\u0026rdquo;,可以想成地图,每个节点是一个城市,线是公路。一个城市到另一个城市,花的时间不同,可以用 成本 (cost) 或 权重 (weight) 来代称,代表要几个星期。假设想找\u0026quot;高庭\u0026quot;到\u0026quot;凛冬城\u0026quot;的最快路线,最简单的方法是尝试每一条路,计算总成本,这是蛮力方法。假设用蛮力方法 来排序数组,尝试每一种组合,看是否排好序,这样的时间复杂度是 o(n!),n 是节点数,n! 是 n 乘 n-1 乘 n-2\u0026hellip; 一直到 1,比 o(n ) 还糟糕。\n我们可以更聪明些!\n图搜索问题的经典算法发明者是理论计算机科学的伟人 edsger dijkstra,所以叫 \u0026ldquo;dijkstra 算法\u0026rdquo;。从\u0026quot;高庭\u0026quot;开始,此时成本为 0,把 0 标在节点里,其他城市标成问号,因为不知道成本多少,dijkstra 算法总是从成本最低的节点开始,目前只知道一个节点 \u0026ldquo;高庭\u0026rdquo;, 所以从这里开始,跑到所有相邻节点,记录成本,完成了一轮算法,但还没到\u0026quot;凛冬城\u0026rdquo;,所以再跑一次 dijkstra 算法,\u0026ldquo;高庭\u0026rdquo; 已经知道了,下一个成本最低的节点,是 \u0026ldquo;君临城\u0026rdquo;。就像之前,记录所有相邻节点的成本,到\u0026quot;三叉戟河\u0026quot;的成本是 5,然而我们想记录的是,从\u0026quot;高庭\u0026quot;到这里的成本,所以\u0026quot;三叉戟河\u0026quot;的总成本是 8+5=13 周,现在走另一条路到\u0026quot;奔流城\u0026quot;,成本高达 25 ,总成本 33,但 \u0026ldquo;奔流城\u0026rdquo; 中最低成本是 10,所以无视新数字,保留之前的成本 10,现在看了\u0026quot;君临城\u0026quot;的每一条路,还没到\u0026quot;凛冬城\u0026quot; 所以继续。下一个成本最低的节点,是\u0026quot;奔流城\u0026quot;,要 10 周,先看 \u0026ldquo;三叉戟河\u0026rdquo; 成本: 10+2=12,比之前的 13 好一点,所以更新 \u0026ldquo;三叉戟河\u0026rdquo; 为 12,\u0026ldquo;奔流城\u0026quot;到\u0026quot;派克城\u0026quot;成本是 3,10+3=13,之前是 14,所以更新 \u0026ldquo;派克城\u0026rdquo; 为 13。\u0026ldquo;奔流城\u0026quot;出发的所有路径都走遍了, 你猜对了,再跑一次 dijkstra 算法。下一个成本最低的节点,是\u0026quot;三叉戟河\u0026rdquo;,从\u0026quot;三叉戟河\u0026quot;出发,唯一没看过的路,通往\u0026quot;凛冬城\u0026rdquo;!成本是 10,加\u0026quot;三叉戟河\u0026quot;的成本 12,总成本 22。再看最后一条路,\u0026ldquo;派克城\u0026quot;到\u0026quot;凛冬城\u0026rdquo;,成本 31。现在知道了最低成本路线,让军队最快到达,还绕过了\u0026quot;君临城\u0026quot;!\ndijkstra 算法的原始版本,构思于 1956 年,算法复杂度是 o(n )。前面说过这个效率不够好,意味着输入不能很大,比如美国的完整路线图,幸运的是,dijkstra 算法几年后得到改进,变成 o(n log n + l)。\n\u0026gt; n 是节点数,l 是多少条线\n虽然看起来更复杂,但实际更快一些。用之前的例子,可以证明更快(6 个节点 9 条线),从 36 减少到 14 左右。\n就像排序,图搜索算法也有很多,有不同优缺点。每次用谷歌地图时,类似 dijkstra 的算法就在服务器上运行,找最佳路线,算法无处不在,现代世界离不开它们。\n这集只触及了算法的冰山一角。\n但成为计算机科学家的核心,是根据情况合理决定,用现有算法,还是自己写新算法。\n希望这集的小例子能让你体会到这点。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/13/","summary":"\u003cp\u003ei.e. Intro to Algorithms\u003c/p\u003e\n\u003cp\u003e前两集,我们\u0026quot;初尝\u0026quot;了高级编程语言(比如 Python 和 Java),我们讨论了几种语句 - 赋值语句,if 语句,循环语句,以及把代码打包成 \u0026ldquo;函数\u0026rdquo;,比如算指数。重要的是,之前写的指数函数只是无数解决方案的一种,还有其它方案 - 用不同顺序写不同语句,也能得到一样结果。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e不同的是\u0026quot;算法\u0026quot;,意思是:解决问题的具体步骤,即使结果一致,有些算法会更好。\u003c/strong\u003e 一般来说,所需步骤越少越好,不过有时我们也会关心其他因素,比如占多少内存。\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;算法\u0026rdquo; 一词来自 波斯博识者 阿尔·花拉子密, 1000 多年前的代数之父之一 ,如何想出高效算法 - 是早在计算机出现前就有的问题,诞生了专门研究计算的领域,然后发展成一门现代学科。\u003c/p\u003e\n\u003cp\u003e你猜对了!计算机科学!\u003c/p\u003e","title":"cscc13 - 算法入门"},{"content":"i.e. programming basics - statements \u0026amp; functions\n上集讲到用机器码写程序,还要处理那么多底层细节,对写大型程序是个巨大障碍。为了脱离底层细节,开发了编程语言,让程序员专心解决问题,不用管硬件细节。\n= 底层各类太多了,编写麻烦,调试麻烦,改动麻烦……*\n语句 今天我们讨论大多数编程语言都有的基本元素,就像口语一样,编程语言有\u0026quot;语句\u0026quot;。语句表达单个完整思想,比如\u0026quot;我想要茶\u0026quot;或者\u0026quot;在下雨\u0026quot;,用不同词汇可以代表不同含义,比如\u0026quot;我想要茶\u0026quot;变成\u0026quot;我想要独角兽\u0026quot;,但没法把\u0026quot;我想要茶\u0026quot;改成\u0026quot;我想要雨\u0026quot;- 语法毫无意义。\n规定句子结构的一系列规则叫 语法 (syntax),英语有语法,所有编程语言也都有语法。 a = 5 是一个编程语言语句,意思是创建一个叫 a 的变量,把数字 5 放里面,这叫\u0026quot;赋值语句\u0026quot;,把一个值赋给一个变量。\n为了表达更复杂的含义,需要更多语句,比如 a = 5 b = 10 c = a + b ,意思是,变量 a 设为 5,变量 b 设为 10 ,把 a 和 b 加起来,把结果 15 放进变量 c 。注意,变量名可以随意取。除了 a b c,也可以叫苹果、梨、水果。计算机不在乎你取什么名,只要不重名就行,当然取名最好还是有点意义,方便别人读懂。\n程序由一个个指令组成 ,有点像菜谱:烧水、加面,等 10 分钟,捞出来就可以吃了。程序也是这样,从第一条语句开始,一句一句运行到结尾。\n刚才我们只是把两个数字加在一起,无聊,我们来做一款游戏吧 !🤖🔫 当然,现在这个学习阶段,来编写一整个游戏还太早了,所以我们只写一小段一小段的代码,来讲解一些基础知识。\n假设我们在写一款老派街机游戏:grace hopper 拍虫子,阻止虫子飞进计算机造成故障,关卡越高,虫子越多。grace 要在虫子损坏继电器之前抓住虫子,好消息是她有几个备用继电器。\n开始编写时,我们需要一些值来保存游戏数据,比如当前关卡数、分数、剩余虫子数、grace 还剩几个备用继电器,所以我们要\u0026quot;初始化\u0026quot;变量 ,\u0026ldquo;初始化\u0026quot;的意思是设置最开始的值。\n关卡=1 分数=0 虫子数=5 备用继电器=4 玩家名=andre 为了做成交互式游戏,程序的执行顺序要更灵活,不只是从上到下执行,因此用 \u0026ldquo;控制流语句\u0026rdquo;。控制流语句有好几种,最常见的是 if 语句,可以想成是 \u0026ldquo;如果 x 为真,那么执行 y\u0026rdquo;,用英语举例就是 \u0026ldquo;如果累了,就去喝茶\u0026rdquo;,如果 \u0026ldquo;累了\u0026rdquo; 为真,就去喝茶,如果 \u0026ldquo;累了\u0026rdquo; 为假,就不喝茶。\nif 语句就像岔路口,走哪条路取决于 \u0026ldquo;表达式\u0026rdquo; 的真假,因此这些表达式又叫 \u0026ldquo;条件语句\u0026rdquo;。在大多数编程语言中,if 语句看起来像这样: if [条件], then [一些代码],结束 if 语句 。比如, if [第一关],then [分数设为 0] ,因为玩家才刚开始游戏,同时把虫子数设为 1,让游戏简单些。\n注意,依赖于 if 条件的代码,要放在 if 和 end if 之间,当然,条件表达式 可以改成别的,比如: \u0026quot;分数 \u0026gt;10\u0026quot; 或者 \u0026quot;虫子数 \u0026lt;1\u0026quot; 。\nif 还可以和 else 结合使用,条件为假会执行 else 里的代码。如果不是第 1 关,else 里的指令就会被执行,grace 要抓的虫子数,是当前关卡数 * 3 ,所以第 2 关有 6 个虫子,第 3 关有 9 个虫子,以此类推。else 中没有改分数,所以 grace 的分数不会变。\n这里列了一些热门编程语言 if-then-else 的具体语法。具体语法略有不同,但主体结构一样。\nif 语句 根据条件执行一次。如果希望根据条件执行多次,需要\u0026quot;条件循环\u0026rdquo;。比如 while 语句,也叫 \u0026ldquo;while 循环\u0026rdquo;,当 while 条件为真,代码会重复执行。不管是哪种编程语言,结构都是这样。假设到达一定分数会冒出一个同事,给 grace 补充继电器,棒极了!把继电器补满到最大数 4 个 , 我们可以用 while 语句来做。\n来过一遍代码。\n假设同事入场时, grace 只剩一个继电器。当执行 while 循环,第一件事是检查条件 - 继电器数量\u0026lt;4? ,继电器数量现在是 1,所以是真,进入循环!\n碰到这一行: 继电器数量 = 继电器数量 + 1 。看起来有点怪,变量的赋值用到了自己。\n我们讲下这个,总是从等号右边开始,\u0026quot;继电器数量+1\u0026quot; 是多少? 当前值是 1,所以 1+1=2 ,结果存到\u0026quot;继电器数量\u0026quot;,覆盖旧的值,所以现在继电器数量是 2 。\n现在到了结尾,跳回开始点。和之前一样,先判断条件,看要不要进入循环 - 继电器数量\u0026lt;4? 。是,继电器数量是 2,所以再次进入循环! 2+1=3 ,3 存入\u0026quot;继电器数量\u0026quot; 。回到开头 。3\u0026lt;4? 是!进入循环。 3+1=4 ,4 存入\u0026quot;继电器数量\u0026quot;,回到开头。 4\u0026lt;4? ,不!现在条件为假,退出循环,执行后面的代码。\nwhile 循环就是这样运作的!\n另一种常见的叫 \u0026ldquo;for 循环\u0026rdquo;,不判断条件,判断次数,会循环特定次数,看起来像上图。现在放些真正的值进去,上图例子会循环 10 次,因为设了变量 i ,从 1 开始,一直到 10 。for 的特点是,每次结束, i 会 +1 ,当 i 等于 10,就知道循环了 10 次,然后退出。我们可以用任何数字,10, 42, 10 亿 。\n假设每关结束后给玩家一些奖励分,奖励分多少取决于继电器剩余数量,随着难度增加,剩下继电器会越来越难。因此奖励分会根据当前关卡数,指数级增长,我们要写一小段代码来算指数。指数是一个数乘自己,乘特定次数。用循环来实现简直完美!\n首先,创建一个叫\u0026quot;奖励分\u0026quot;的新变量,设为 1 (看上图),然后 for 循环,从 1 到 [当前关卡数] ,[奖励分] x [继电器剩余数],结果存入 [奖励分] ,比如继电器数是 2,关卡数是 3 ,for 会循环 3 次,奖励分会乘 继电器数量 x 继电器数量 x 继电器数量 ,也就是 1×2×2×2,奖励分是 8,2 的 3 次方。这个指数代码很实用,其他地方可能会用到。如果每次想用就复制粘贴,会很麻烦,每次都要改变量名。如果代码发现问题,要补漏洞时,要把每一个复制黏贴过的地方都找出来改,而且会让代码更难懂。\n少即是多!\n我们想要某种方法,把代码\u0026quot;打包\u0026quot;,可以直接使用,得出结果,不用管内部复杂度。\n这又提升了一层抽象!\n函数 为了隐藏复杂度,可以把代码打包成 \u0026ldquo;函数\u0026rdquo;,也叫 \u0026ldquo;方法\u0026rdquo; 或 \u0026ldquo;子程序\u0026rdquo;(有些编程语言这么叫)。其他地方想用这个函数,直接写函数名就可以了。\n现在我们把指数代码变成函数。\n第一步,取名。叫什么都行,比如\u0026quot;快乐独角兽\u0026quot;,但因为是算指数,直接叫\u0026quot;指数\u0026quot;合适一些。\n还有,与其用特定变量名,比如 \u0026ldquo;继电器\u0026rdquo; 和 \u0026ldquo;关卡数\u0026rdquo;,用更通用的名字,比如 底数 (base) 和 指数 (exp),base 和 exp 的初始值需要外部传入,剩余代码和之前一样。现在完成了,有函数名和新变量名。\n最后,我们还需要把结果 交给使用这个函数的代码,所以用 return 语句,指明返回什么。\n完整版代码是这样!\n现在可以随意用这个函数,只需要写出名字 然后传入 2 个数字就可以了。如果要算 2 的 44 次方,写 exponent(2,44) ,结果是 18 万亿左右。幕后原理是,2 和 44 存进 base 和 exp ,跑循环,然后返回结果。我们来用这个新函数 算奖励分,首先,奖励分初始化为 0 ,然后用 if 语句,看剩不剩继电器(看上图的 \u0026gt; 0),如果还剩,用指数函数,传入 [继电器数] 和 [关卡数] ,它会算 [继电器数] 的 [关卡数] 次方,存入奖励分。这段算奖励分的代码,之后可能还会用,也打包成一个函数。没错,这个函数 (calcbonus) 会调用另一个函数 (exponent) 。还有!这个 calcbonus 函数,可以用在其他更复杂的函数 。\n我们来写一个函数,每一关结束后都会调用,叫 levelfinished (关卡结束)- 需要传入 [剩余继电器数] 、 [关卡数] 、[当前分] ,这些数据必须传入。里面用 calcbonus 算奖励分,并加进总分,还有,如果 当前分 \u0026gt; 游戏最高分 ,把新高分和玩家名 存起来,现在代码变得蛮\u0026quot;花哨\u0026quot;了。函数调函数调函数。\n我们写这样一行代码时,复杂度都隐藏起来了,不需要知道内部的循环和变量,只知道结果会像魔术一样返回,总分 53 。 但是这不是魔术,是抽象的力量! 如果你理解了这个例子,就明白了函数的强大之处和现代编程的核心。\n比如浏览器这样的复杂程序,用一长串语句来写是不可能的,会有几百万行代码,没人能理解。所以现代软件由上千个函数组成,每个负责不同的事。如今超过 100 行代码的函数很少见,如果多于 100 行,应该有东西可以拆出来做成一个函数。模块化编程不仅可以让单个程序员独立制作 app,也让团队协作可以写更大型的程序。不同程序员写不同函数,只需要确保自己的代码工作正常,把所有人的拼起来,整个程序也应该能正常运作!\n现实中,程序员不会浪费时间写指数函数这种东西,现代编程语言有很多预先写好的函数集合,叫 \u0026ldquo;库\u0026rdquo; ,由专业人员编写,不仅效率高,而且经过了仔细检查。几乎做所有事情都有库,网络、图像、声音。\n我们之后会讲这些主题。\n但在此之前,我们先讲算法。好奇吗?应该!\n下周见\n= 该章节其实没有多少内容,简单介绍了语句和函数,其中的例子用语言描述起来简直‘累’,还是动画来的直观。*\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/12/","summary":"\u003cp\u003ei.e. Programming Basics - Statements \u0026amp; Functions\u003c/p\u003e\n\u003cp\u003e上集讲到用机器码写程序,还要处理那么多底层细节,对写大型程序是个巨大障碍。为了脱离底层细节,开发了编程语言,让程序员专心解决问题,不用管硬件细节。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 底层各类太多了,编写麻烦,调试麻烦,改动麻烦……*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc12 - 编程原理-语句和函数"},{"content":"i.e. the first programming languages\n之前我们把重点放在硬件 - 组成计算机的物理组件,比如电,电路,寄存器,ram,alu,cpu ,但在硬件层面编程非常麻烦。\n所以程序员想要一种更通用的方法编程 - 一种\u0026quot;更软的\u0026quot;媒介。\n没错,我们要讲软件!\n第 8 集我们一步步讲了一个简单程序,第一条指令在内存地址 0:0010 1110 ,之前说过,前 4 位是操作码 ,简称 opcode 。对于这个假设 cpu,0010 代表 load_a 指令 - 把值从内存复制到寄存器 a ,后 4 位是内存地址,1110 是十进制的 14 ,所以这 8 位表达的意思是 \u0026ldquo;读内存地址 14,放入寄存器 a\u0026rdquo;。\n只是用了两种不同语言,可以想成是英语和摩尔斯码的区别 - \u0026ldquo;你好\u0026rdquo; 和 \u0026ldquo;\u0026hellip;. . .-.. .-.. \u0026mdash;\u0026rdquo; 是一个意思:你好。只是编码方式不同,英语和摩尔斯码的复杂度也不同,英文有 26 个字母以及各种发音,摩尔斯码只有\u0026quot;点\u0026quot;和\u0026quot;线\u0026quot;,但它们可以传达相同的信息,计算机语言也类似。\n机器语言 计算机能处理二进制,二进制是处理器的\u0026quot;母语\u0026quot;,事实上,它们只能理解二进制,这叫 \u0026ldquo;机器语言\u0026rdquo; 或 \u0026ldquo;机器码\u0026rdquo;。\n在计算机早期阶段,必须用机器码写程序。具体来讲,会先在纸上用英语写一个\u0026quot;高层次版\u0026quot;。举例:\u0026ldquo;从内存取下一个销售额,然后加到天、周、年的总和,然后算税\u0026rdquo;,等等。.. 这种对程序的高层次描述,叫 \u0026ldquo;伪代码\u0026rdquo; 。在纸上写好后,用\u0026quot;操作码表\u0026quot;把伪代码转成二进制机器码,翻译完成后,程序可以喂入计算机并运行。\n汇编语言 你可能猜到了,很快人们就厌烦了。所以在 1940~1950 年代,程序员开发出一种新语言, 更可读、更高层次,每个操作码分配一个简单名字,叫\u0026quot;助记符\u0026quot;,\u0026ldquo;助记符\u0026quot;后面紧跟数据,形成完整指令。与其用 1 和 0 写代码,程序员可以写\u0026quot;load_a 14\u0026rdquo;,我们在第 8 集用过这个助记符,因为容易理解得多!\n当然,cpu 不知道 load_a 14 是什么,它不能理解文字,只能理解二进制。所以程序员想了一个技巧,写二进制程序来帮忙,它可以读懂文字指令,自动转成二进制指令,这种程序叫 - 汇编器(assembler)。汇编器读取用\u0026quot;汇编语言\u0026quot;写的程序,然后转成\u0026quot;机器码\u0026quot; ,\u0026ldquo;load_a 14\u0026rdquo; 是一个汇编指令的例子。\n= 对的,把翻译工作交给了 ‘汇编器’ - 一个二进制程序。🤣*\n随着时间推移,汇编器有越来越多功能,让编程更容易。\n其中一个功能是自动分析 jump 地址。\n这里有一个第 8 集用过的例子:注意,jump negative 指令跳到地址 5 ,jump 指令跳到地址 2 ,问题是,如果在程序开头多加一些代码,所有地址都会变,更新程序会很痛苦!所以汇编器不用固定跳转地址,而是让你插入可跳转的标签,当程序被传入汇编器,汇编器会自己搞定跳转地址。程序员可以专心编程,不用管底层细节。\n隐藏不必要细节来做更复杂的工作。\n我们又提升了一层抽象!🔆\n然而,即使汇编器有这些厉害功能,比如自动跳转。汇编只是修饰了一下机器码,一般来说,一条汇编指令对应一条机器指令,所以汇编码和底层硬件的连接很紧密。汇编器仍然强迫程序员思考用什么寄存器和内存地址,如果你突然要一个额外的数,可能要改很多代码。\n让我们进入思考泡泡\n葛丽丝·霍普博士 也遇到了这个问题,作为美国海军军官,她是哈佛 1 号计算机的首批程序员之一,这台机器我们在第 2 集提过。这台巨大机电野兽在 1944 年战时建造完成,帮助盟军作战,程序写在打孔纸带上,放进计算机执行。顺便一说,如果程序里有漏洞,真的就 直接用胶带来补\u0026quot;漏洞\u0026quot;。\n\u0026gt; 补‘漏洞’\nmark 1 的指令集非常原始,甚至没有 jump 指令,如果代码要跑不止一次,得把带子的两端连起来做成循环。换句话说,给 mark 1 编程简直是噩梦!\n战后,霍普继续在计算机前沿工作,为了释放电脑的潜力,她设计了一个高级编程语言,叫\u0026quot;算术语言版本 0\u0026quot;,简称\u0026quot;a-0\u0026quot; 。\n汇编与机器指令是一一对应的,但一行高级编程语言 可能会转成几十条二进制指令。为了做到这种复杂转换 hopper 在 1952 年创造了第一个编译器,编译器专门把高级语言 转成低级语言,比如汇编或机器码(cpu 可以直接执行机器码)。\n谢了 思想泡泡\n尽管\u0026quot;使编程更简单\u0026quot;很诱人,但很多人对霍普的点子持怀疑态度。她曾说 \u0026ldquo;我有能用的编译器,但没人愿意用,他们告诉我计算机只能做算术,不能运行程序\u0026rdquo;。\n但这个点子是好的,不久,很多人尝试创造新编程语言 - 如今有上百种语言!\n可惜的是,没有任何 a-0 的代码遗留下来,所以我们用 python 举例(一门现代编程语言)。\n假设我们想相加两个数字,保存结果。记住,如果用汇编代码,我们得从内存取值,和寄存器打交道,以及其他底层细节,但同样的程序可以用 python 这样写:\n不用管寄存器或内存位置 - 编译器会搞定这些细节,不用管底层细节。\n程序员只需要创建 代表内存地址的抽象,叫 \u0026ldquo;变量\u0026rdquo; ,给变量取名字,现在可以把两个数 存在变量里,这里取名 a 和 b, 实际编程时你可以随便取名;然后相加两个数,把结果存在变量 c 。底层操作时,编译器可能把变量 a 存在寄存器 a ,但我不需要知道这些!眼不见心不烦 !😼 这是个重要历史里程碑,但 a-0 和之后的版本没有广泛使用。\n高级程序语言 fortran,名字来自 \u0026ldquo;公式翻译\u0026rdquo;(formula translation),这门语言数年后由 ibm 在 1957 年发布,主宰了早期计算机编程。\njohn backus, the fortran project director, fortran 项目总监 john backus 说过,\u0026ldquo;我做的大部分工作都是因为懒,我不喜欢写程序,所以我写这门语言,让编程更容易\u0026rdquo;\n你懂的,典型的\u0026quot;懒人\u0026quot;,😎 创造自己的编程语言。\n平均来说,fortran 写的程序,比等同的手写汇编代码短 20 倍,然后 fortran 编译器会把代码转成机器码。人们怀疑性能是否比得上手写代码,但因为能让程序员写程序更快,所以成了一个更经济的选择,运行速度慢一点点,编程速度大大加快。\n当时 ibm 在卖计算机,因此最初 fortran 代码只能跑在 ibm 计算机上。1950 年代大多数编程语言和编译器只能运行在一种计算机上,如果升级电脑,可能要重写所有代码!因此工业界,学术界,政府的计算机专家在 1959 年组建了一个联盟 - 数据系统语言委员会,grace hopper 担任顾问,开发一种通用编程语言,可以在不同机器上通用,最后诞生了一门高级,易于使用,\u0026ldquo;普通面向商业语言\u0026rdquo;,简称 cobol(common business-oriented language) 。\n为了兼容不同底层硬件,每个计算架构需要一个 cobol 编译器。最重要的是,这些编译器都可以接收相同 cobol 代码,不管是什么电脑,这叫\u0026quot;一次编写,到处运行\u0026quot; 。如今大多数编程语言都是这样,不必接触 cpu 特有的汇编码和机器码,减小了使用门槛。\n在高级编程语言出现之前,编程只是计算机专家和爱好者才会做的事,而且通常是主职,但现在,科学家,工程师,医生,经济学家,教师等等,都可以把计算机用于工作。\n感谢这些语言!\n计算机科学从深奥学科 变成了大众化工具,同时,编程的抽象也让计算机专家,现在叫\u0026quot;专业程序员\u0026quot;,制作更复杂的程序,如果用汇编写可能要上百万行。\n当然,计算机的历史没有在 1959 年结束,编程语言设计的黄金时代才刚刚开始,和硬件一起飞速发展。\nin the 1960s, we had languages like algol, lisp and basic. 在 1960 年代,有 algol, lisp 和 basic 等语言,70 年代有:pascal,c 和 smalltalk ,80 年代有:c++,objective-c 和 perl ,90 年代有:python,ruby 和 java ,新千年 swift, c#, go 在崛起 。 有些语言你可能听起来耳熟 - 很多现在还存在,你现在用的浏览器很可能是 c++ 或 objective-c 写的。\n我刚才说的编程语言名字只是冰山一角,新的编程语言在不断诞生。新语言想用更聪明的抽象,让某些方面更容易或更强大,或利用新技术和新平台带来的优势,让更多人能快速做出美妙的事情。许多人认为编程的\u0026quot;圣杯\u0026quot;是直接用英文,直接对计算机说话,然后它会理解并执行,这种智能系统目前只存在于科幻小说,\u0026ldquo;2001:太空漫游\u0026rdquo; 的粉丝可能没什么意见。\n现在你理解了编程语言,接下来几集我们会深入了解编程语言和用语言写的软件,是怎么做到那些酷事。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/11/","summary":"\u003cp\u003ei.e. The First Programming Languages\u003c/p\u003e\n\u003cp\u003e之前我们把重点放在硬件 - 组成计算机的物理组件,比如电,电路,寄存器,RAM,ALU,CPU ,但在硬件层面编程非常麻烦。\u003c/p\u003e\n\u003cp\u003e所以程序员想要一种更通用的方法编程 - 一种\u0026quot;更软的\u0026quot;媒介。\u003c/p\u003e\n\u003cp\u003e没错,我们要讲软件!\u003c/p\u003e","title":"cscc11 - 编程语言发展史"},{"content":"i.e. early programming\n前几集我们把重点放在计算机的原理,怎么从内存读写数据,执行操作,比如把两个数字加在一起,还简单讲了下指令的执行,也就是计算机程序。\n但我们还没讲的是:程序如何\u0026quot;进入\u0026quot;计算机。你应该记得在第 7, 8 集,我们一步步讲了例子程序,当时为了简单,我们假设程序已经魔法般在内存里了,但事实是,程序需要加载进内存,这不是魔法,是计算机科学!\n给机器编程这个需求,早在计算机出现之前就有了。\n最著名的例子来自纺织业,如果你只想织一块红色大桌布,可以直接放红线进织布机,但如果想要图案怎么办?比如条纹或者方格。工人要每隔一会儿调整一次织布机,因为非常消耗劳动力,所以图案纺织品很贵。特定位置有没有穿孔,决定了线是高是低,横线是从上/从下穿过,为了让每行图案不同,纸卡连成长条,形成连续指令,听起来很熟?\n\u0026gt; 雅卡尔织布机\n很多人认为雅卡尔织布机是最早的编程。\n事实证明,穿孔纸卡便宜、可靠、也易懂。近一个世纪后,穿孔纸卡用于 1890 年美国人口普查,我们在第一集提过,一张卡存一个人的信息,比如种族、婚姻状况、子女数量、出生国家等等。针对每个问题,人口普查工作者会在对应位置打孔,当卡片插入汇总机,孔会让对应总和值+1 ,可以插入整个国家人口的卡片,在结束后得到各个总值。值得注意的是,早期汇总机不算计算机,因为它们只做一件事 - 汇总数据,操作是固定的,不能编程,穿孔纸卡存的是数据,不是程序。\n\u0026gt; 穿孔纸卡 \u0026amp; 汇总机\n之后 60 年,这些机器被加强,可以做减、乘、除,甚至可以做一些小决定,决定何时执行某指令。为了正确执行不同计算,程序员需要某种控制面板,面板有很多小插孔,程序员可以插电线,让机器的不同部分互相传数据和信号,因此也叫 \u0026ldquo;插线板\u0026rdquo;。\n\u0026gt; 插线板\n不幸的是,这意味着运行不同程序要重新接线,所以到 1920 年代,控制面板变成了可拔插,让编程更方便,可以给机器插入不同程序。比如,一个插线板算销售税,另一个算工资单,但给插线板编程很复杂,图中乱成一团的线负责算盈亏总额,用于 ibm 402 核算机。在 1940 年代这样做很流行,用插线板编程不只在机电计算机流行。世上第一台通用电子计算机,eniac,完成于 1946 年,用了一大堆插线板,程序在纸上设计好之后,给 eniac 连线,最多可能花三个星期。因为早期计算机非常昂贵,停机几个星期只为换程序完全无法接受。人们急需更快、更灵活的新方式来编程。\n幸运的是,到 1940 年代晚期 1950 年代初,内存变得可行,价格下降,容量上升。与其把程序存在插线板,存在内存变得可行,这样程序易于修改、方便 cpu 快速读取,这类机器叫 \u0026ldquo;存储程序计算机\u0026rdquo;。如果内存足够,不仅可以存要运行的程序,还可以存程序需要的数据,包括程序运行时产生的新数据。\n程序和数据都存在一个地方,叫 \u0026ldquo;冯诺依曼结构\u0026rdquo; ,命名自 约翰·冯·诺依曼 - 杰出的数学家和物理学家 ,参与了曼哈顿计划和早期电子计算机项目。他曾说:我在思考比炸弹重要得多的东西 - 计算机 。\n冯诺依曼计算机的标志是,一个处理器(有算术逻辑单元) + 数据寄存器 + 指令寄存器 + 指令地址寄存器 + 内存(负责存数据和指令) 。\n希望这听起来很耳熟,因为第 7 集我们造了一个冯诺依曼计算机。\n第一台冯诺依曼架构的\u0026quot;储存程序计算机\u0026quot;,由曼彻斯特大学于 1948 年建造完成,绰号\u0026quot;宝宝\u0026quot;,甚至你现在看视频的计算机,也在用一样的架构。虽然有内存很棒,但程序和数据依然需要某种方式输入计算机,所以用穿孔纸卡。\n让我们进入思维泡泡\n到 1980 年代,几乎所有的计算机都有穿孔纸卡读取器,可以吸入一张卡片,把卡片内容写进内存。如果放了一叠卡片,读取器会一个个写进内存,一旦程序和数据写入完毕,电脑会开始执行,即便简单程序也有几百条指令,要用一叠纸卡来存。如果不小心摔倒弄撒了,要花上几小时、几天、甚至几周来整理。\n有个小技巧是在卡片侧面画对角线,如果弄散了,整理起来会方便很多。\n用纸卡的最大型程序是美国空军的 sage 防空系统,于 1955 年完成。据称顶峰时期雇佣了世上 20% 程序员,主控制程序用了 62500 张穿孔纸卡,等同于大约 5mb 的数据,以如今的标准,不值一提。穿孔纸卡不仅可以往计算机放数据,还可以取出数据,程序运行到最后,结果可以输到纸卡上,方式嘛,当然是打孔,然后人可以分析结果,或者再次放进计算机,做进一步计算。\n谢了 思维泡泡\n穿孔纸卡的亲戚是纸带。基本是一回事,只不过更连续,不是一张张卡。当然我们还没提硬盘,只读光盘,dvd, u 盘等等,以后我们会讲这些更先进的存储方法。\n\u0026gt; 纸带\n最后,除了插线板和穿孔纸卡,在 1980 年代前,还有一种常见编程方式,面板编程。与其插一堆线到插线板,可以用一大堆开关和按钮,做到一样的效果,面板上有指示灯,代表各种函数的状态和内存中的值。50 和 60 年代的计算机,一般都有这样巨大的控制台,很少有人只用开关来输入一整个程序,但技术上是可行的。早期针对计算机爱好者的家用计算机,大量使用了开关,因为大多数家庭用户负担不起昂贵的外围设备,比如穿孔纸卡读取器。\n\u0026gt; 面板编程\n第一款取得商业成功的家用计算机是 altair 8800 ,有两种版本可以买:1. 预先装好的整机 ;2. 需要组装的组件。计算机爱好者 喜欢买组件版,售价极低,在 1975 年卖 400 美元左右,相当于 2017 年的 2000 美元。为了给 8800 编程,你要拨动面板上的开关,输入二进制操作码,然后按 \u0026ldquo;存储键\u0026rdquo; 把值存入内存,然后会到下一个内存位置,你可以再次拨开关,写下一个指令,重复这样做,把整个程序都写入内存之后,可以推动开关,回到内存地址 0 ,然后按运行按钮,灯会闪烁,这就是 1975 年的家用计算机,哇。\n\u0026gt; altair 8800\n不管是插线板、开关或穿孔纸卡,早期编程都是专家活。不管是全职还是技术控,都要非常了解底层硬件,比如 操作码,寄存器等,才能写程序,所以编程很难,很烦。哪怕工程师和科学家都无法 完全发挥计算机的能力,我们需要一种更简单方式告诉计算机要做什么。\n一种更简单的编程方式!\n这带领我们到下一个话题 - 编程语言,我们下集会讲。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/10/","summary":"\u003cp\u003ei.e. Early Programming\u003c/p\u003e\n\u003cp\u003e前几集我们把重点放在计算机的原理,怎么从内存读写数据,执行操作,比如把两个数字加在一起,还简单讲了下指令的执行,也就是计算机程序。\u003c/p\u003e\n\u003cp\u003e但我们还没讲的是:程序如何\u0026quot;进入\u0026quot;计算机。你应该记得在第 7, 8 集,我们一步步讲了例子程序,当时为了简单,我们假设程序已经魔法般在内存里了,但事实是,程序需要加载进内存,这不是魔法,是计算机科学!\u003c/p\u003e","title":"cscc10 - 早期的编程方式"},{"content":"i.e. advanced cpu designs\n随着本系列进展,我们知道计算机进步巨大,从 1 秒 1 次运算,到现在有千赫甚至兆赫的 cpu ,你现在看视频的设备八成也有 ghz 速度 ,1 秒十亿条指令 ,这是很大的计算量!\n早期计算机的提速方式是减少晶体管的切换时间 ,晶体管组成了逻辑门,alu 以及前几集的其他组件,但这种提速方法最终会碰到瓶颈,所以处理器厂商发明各种新技术来提升性能,不但让简单指令运行更快,也让它能进行更复杂的运算。\n上集我们写了个做除法的程序,给 cpu 执行,方法是做一连串减法,比如 16 除 4 会变成 - 16-4 -4 -4 -4 ,碰到 0 或负数才停下 。但这种方法要多个时钟周期,很低效。所以现代 cpu 直接在硬件层面设计了除法,可以直接给 alu 除法指令,这让 alu 更大也更复杂一些,但也更厉害 。 复杂度 vs 速度的平衡 在计算机发展史上经常出现。\n举例,现代处理器有专门电路来处理图形操作,解码压缩视频,加密文档 等等。如果用标准操作来实现,要很多个时钟周期。你可能听过某些处理器有 mmx, 3dnow, sse ,它们有额外电路做更复杂的操作,用于游戏和加密等场景。指令不断增加,人们一旦习惯了它的便利就很难删掉,所以为了兼容旧指令集,指令数量越来越多。\n英特尔 4004,第一个集成 cpu,有 46 条指令,足够做一台能用的计算机,但现代处理器有上千条指令,有各种巧妙复杂的电路,超高的时钟速度带来另一个问题 - 如何快速传递数据给 cpu 。就像有强大的蒸汽机,但无法快速加煤。ram 成了瓶颈,ram 是 cpu 之外的独立组件,意味着数据要用线来传递,叫\u0026quot;总线\u0026quot;,总线可能只有几厘米,别忘了电信号的传输接近光速,但 cpu 每秒可以处理上亿条指令,很小的延迟也会造成问题。ram 还需要时间找地址取数据,配置,输出数据,一条\u0026quot;从内存读数据\u0026quot;的指令可能要多个时钟周期,cpu 空等数据。\n缓存 解决延迟的方法之一是给 cpu 加一点 ram - 叫 \u0026ldquo;缓存\u0026rdquo; ,因为处理器里空间不大,所以缓存一般只有 kb 或 mb ,而 ram 都是 gb 起步。缓存提高了速度,cpu 从 ram 拿数据时,ram 不用传一个,可以传一批,虽然花的时间久一点,但数据可以存在缓存,这很实用,因为数据常常是一个个按顺序处理。\n\u0026gt; cpu 缓存\n举个例子,算餐厅的当日收入,先取 ram 地址 100 的交易额,ram 与其只给 1 个值,直接给一批值,把地址 100 到 200 都复制到缓存。当处理器要下一个交易额时,地址 101,缓存会说:\u0026ldquo;我已经有了,现在就给你\u0026rdquo;,不用去 ram 取数据,因为缓存离 cpu 近,一个时钟周期就能给数据 - cpu 不用空等!比反复去 ram 拿数据快得多。如果想要的数据已经在缓存,叫 缓存命中 ,如果想要的数据不在缓存,叫 缓存未命中 。缓存也可以当临时空间,存一些中间值,适合长/复杂的运算。继续餐馆的例子,假设 cpu 算完了一天的销售额,想把结果存到地址 150,就像之前,数据不是直接存到 ram ,而是存在缓存,这样不但存起来快一些,如果还要接着算,取值也快一些。\n但这样带来了一个有趣的问题,缓存和 ram 不一致了 😈。这种不一致必须记录下来,之后要同步,因此缓存里每块空间有一个特殊标记,叫 \u0026ldquo;脏位\u0026rdquo; (dirty bit) - 这可能是计算机科学家取的最贴切的名字。同步一般发生在 当缓存满了而 cpu 又要缓存时,在清理缓存腾出空间之前,会先检查 \u0026ldquo;脏位\u0026rdquo;。如果是\u0026quot;脏\u0026quot;的,在加载新内容之前,会把数据写回 ram 。\n指令流水线 另一种提升性能的方法叫 \u0026ldquo;指令流水线\u0026rdquo; 。\n想象下你要洗一整个酒店的床单,但只有 1 个洗衣机,1 个干燥机,选择 1:按顺序来,放洗衣机等 30 分钟洗完,然后拿出湿床单,放进干燥机等 30 分钟烘干,这样 1 小时洗一批;另外一说:如果你有 30 分钟就能烘干的干燥机,请留言告诉我是什么牌子,我的至少要 90 分钟。即使有这样的神奇干燥机,我们可以用\u0026quot;并行处理\u0026quot;进一步提高效率。就像之前,先放一批床单到洗衣机,等 30 分钟洗完,然后把湿床单放进干燥机,但这次,与其干等 30 分钟烘干,可以放另一批进洗衣机,让两台机器同时工作,30 分钟后,一批床单完成,另一批完成一半,另一批准备开始,效率 x2 !🎉\n处理器也可以这样设计。\n第 7 集,我们演示了 cpu 按序处理 - 取指 → 解码 → 执行,不断重复。这种设计,三个时钟周期执行 1 条指令,但因为每个阶段用的是 cpu 的不同部分,意味着可以并行处理(parallelize)!\u0026ldquo;执行\u0026quot;一个指令时,同时\u0026quot;解码\u0026quot;下一个指令,\u0026ldquo;读取\u0026quot;下下个指令,不同任务重叠进行,同时用上 cpu 里所有部分。\n这样的流水线每个时钟周期执行 1 个指令,吞吐量 x 3 。\n和缓存一样,这也会带来一些问题。\n第一个问题是指令之间的依赖关系,举个例子,你在读某个数据,而正在执行的指令会改这个数据,也就是说拿的是旧数据,因此流水线处理器要先弄清数据依赖性,必要时停止流水线,避免出问题。高端 cpu,比如笔记本和手机里那种,会更进一步,动态排序有依赖关系的指令,最小化流水线的停工时间,这叫 \u0026ldquo;乱序执行\u0026rdquo; 。和你猜的一样,这种电路非常复杂,但因为非常高效,几乎所有现代处理器都有流水线。\n第二个问题是 \u0026ldquo;条件跳转\u0026rdquo;,比如上集的 jump negative ,这些指令会改变程序的执行流。简单的流水线处理器,看到 jump 指令会停一会儿,等待条件值确定下来,一旦 jump 的结果出了,处理器就继续流水线。因为空等会造成延迟,所以高端处理器会用一些技巧,可以把 jump 想成是 \u0026ldquo;岔路口\u0026rdquo;,高端 cpu 会猜哪条路的可能性大一些,然后提前把指令放进流水线,这叫 \u0026ldquo;推测执行\u0026rdquo; 。当 jump 的结果出了,如果 cpu 猜对了,流水线已经塞满正确指令,可以马上运行,如果 cpu 猜错了,就要清空流水线,就像走错路掉头。让 gps 不要再!叫!了!为了尽可能减少清空流水线的次数,cpu 厂商开发了复杂的方法,来猜测哪条分支更有可能,叫 \u0026ldquo;分支预测\u0026rdquo; ,现代 cpu 的正确率超过 90% 。\n理想情况下,流水线一个时钟周期完成 1 个指令,然后\u0026quot;超标量处理器\u0026quot;出现了,一个时钟周期完成多个指令。即便有流水线设计,在指令执行阶段,处理器里有些区域还是可能会空闲。比如,执行一个 \u0026ldquo;从内存取值\u0026rdquo; 指令期间, alu 会闲置,所以一次性处理多条指令(取指令+解码) 会更好。如果多条指令要 alu 的不同部分,就多条同时执行。我们可以再进一步,加多几个相同的电路,执行出现频次很高的指令。举例,很多 cpu 有四个,八个甚至更多完全相同的 alu ,可以同时执行多个数学运算。\n好了,目前说过的方法,都是优化 1 个指令流的吞吐量。\n另一个提升性能的方法是同时运行多个指令流,用多核处理器。你应该听过双核或四核处理器,意思是一个 cpu 芯片里,有多个独立处理单元,很像是有多个独立 cpu,但因为它们整合紧密,可以共享一些资源,比如缓存,使得多核可以合作运算,但多核不够时,可以用多个 cpu 。高端计算机,比如现在给你传视频的 youtube 服务器,需要更多马力,让上百人能同时流畅观看,2 个或 4 个 cpu 是最常见的。\n\u0026gt; 多核处理器\n但有时人们有更高的性能要求,所以造了超级计算机!如果要做怪兽级运算,比如模拟宇宙形成,你需要强大的计算能力,给普通台式机加几个 cpu 没什么用,你需要很多处理器! 不…不…还要更多,更多。截止至视频发布,世上最快的计算机在中国无锡的国家超算中心 - 神威·太湖之光有 40960 个 cpu,每个 cpu 有 256 个核心,总共超过 1 千万个核心,每个核心的频率是 1.45ghz ,每秒可以进行 9.3 亿亿次浮点数运算,也叫 每秒浮点运算次数 (flops)。相信我,这个速度很可怕,没人试过跑最高画质的《孤岛危机》但我估计没问题。\n\u0026gt; 神威·太湖之光\n长话短说,这些年处理器不但大大提高了速度,而且也变得更复杂,用各种技巧,榨干每个时钟周期 做尽可能多运算。我们的任务是利用这些运算能力,做又酷又实用的事。\n编程就是为了这个,我们下集说。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/09/","summary":"\u003cp\u003ei.e. Advanced CPU Designs\u003c/p\u003e\n\u003cp\u003e随着本系列进展,我们知道计算机进步巨大,从 1 秒 1 次运算,到现在有千赫甚至兆赫的 CPU ,你现在看视频的设备八成也有 GHz 速度 ,1 秒十亿条指令 ,这是很大的计算量!\u003c/p\u003e\n\u003cp\u003e早期计算机的提速方式是减少晶体管的切换时间 ,晶体管组成了逻辑门,ALU 以及前几集的其他组件,但这种提速方法最终会碰到瓶颈,所以处理器厂商发明各种新技术来提升性能,不但让简单指令运行更快,也让它能进行更复杂的运算。\u003c/p\u003e","title":"cscc09 - 高级 cpu 设计"},{"content":"i.e. instructions \u0026amp; programs\n上集我们把 alu, 控制单元,ram, 时钟 结合在一起,做了个基本,但可用的\u0026quot;中央处理单元\u0026quot;, 简称 cpu ,它是计算机的核心。\n我们已经用电路做了很多组件,这次我们给 cpu 一些指令来运行! cpu 之所以强大,是因为它是可编程的(programmable)- 如果写入不同指令,就会执行不同任务。cpu 是一块硬件,可以被软件控制!\n我们重新看一下上集的简单程序,内存里有这些值,每个地址可以存 8 位数据。因为我们的 cpu 是假设的,这里前 4 位是\u0026quot;操作码\u0026quot;,后 4 位指定一个内存地址,或寄存器,内存地址 0 是 0010 1110 ,前 4 位代表 load_a 指令 - 意思是:把后 4 位指定的内存地址的值,放入寄存器 a ,后 4 位是 1110,十进制的 14 ,我们来把 0010 1110 看成 \u0026ldquo;load_a 14\u0026rdquo; 指令,这样更好理解!也更方便说清楚!可以对内存里剩下的数也这样转换,这里,我们的程序只有 4 个指令,还有数字 3 和 14 。\n现在一步步看:\n\u0026ldquo;load_a 14\u0026rdquo; 是从地址 14 中拿到数字 3,放入寄存器 a ; \u0026ldquo;load_b 15\u0026rdquo; 是从地址 15 中拿到数字 14,放入寄存器 b ; 下一个是 add 指令 - \u0026ldquo;add b a\u0026rdquo; 告诉 alu 把寄存器 b 和寄存器 a 里的数字加起来,(b 和 a 的)顺序很重要,因为结果会存在第二个寄存器 - 也就是寄存器 a ; 最后一条指令是 \u0026ldquo;store_a 13\u0026rdquo; ,把寄存器 a 的值存入内存地址 13 。 好棒!我们把 2 个数加在了一起!毕竟只有 4 个指令,也只能做这个了 。\n加多一些指令吧!\n\u0026gt; 更多的指令\nsub 是减法,和 add 一样也要 2 个寄存器来操作。\n还有 jump(跳转)- 让程序跳转到新位置,如果想改变指令顺序,或跳过一些指令,这个很实用。举例,jump 0 可以跳回开头。 jump 在底层的实现方式 - 是把指令后 4 位代表的内存地址的值覆盖掉 \u0026ldquo;指令地址寄存器\u0026rdquo; 里的值 。 还有一个特别版的 jump 叫 jump_negative,它只在 alu 的 \u0026ldquo;负数标志\u0026rdquo; 为真时,进行 jump ,第 5 集讲过,算术结果为负,\u0026ldquo;负数标志\u0026quot;才是真,结果不是负数时,\u0026ldquo;负数标志\u0026quot;为假。如果是假,jump_negative 就不会执行,程序照常进行。\n我们之前的例子程序,其实应该是这样,才能正确工作。否则跑完 store_a 13 之后, cpu 会不停运行下去,处理后面的 0 ,因为 0 不是操作码,所以电脑会崩掉 !\n我还想指出一点,指令和数据都是存在同一个内存里的,它们在根本层面上毫无区别 - 都是二进制数 。halt 很重要,能区分指令和数据。\n好,现在用 jump 让程序更有趣一些,我们还把内存中 3 和 14 两个数字,改成 1 和 1 ,现在来从 cpu 的视角走一遍程序。\n首先 load_a 14,把 1 存入寄存器 a (因为地址 14 里的值是 1);然后 load_b 15,把 1 存入寄存器 b (因为地址 15 里的值也是 1);然后 add b a 把寄存器 b 和 a 相加 结果放到寄存器 a 里,现在寄存器 a 的值是 2 (当然是以二进制存的);然后 store_a 13 指令,把寄存器 a 的值存入内存地址 13。\n现在遇到 jump 2 指令 ,cpu 会把\u0026quot;指令地址寄存器\u0026quot;的值,现在是 4,改成 2 ,因此下一步不再是 halt ,而是读内存地址 2 里的指令,也就是 add b a 。\n我们跳转了!\n寄存器 a 里是 2,寄存器 b 里是 1 ,1+2=3,寄存器 a 变成 3 ,存入内存 ,又碰到 jump 2,又回到 add b a 。 1+3=4 ,现在寄存器 a 是 4 。\n发现了吗?每次循环都+1 ,不断增多 ,酷! 但没法结束啊 ,永远不会碰到 halt ,总是会碰到 jump ,这叫无限循环 - 这个程序会永远跑下去。.. 下去。.. 下去。.. 下去。.. 为了停下来,我们需要有条件的 jump , 只有特定条件满足了,才执行 jump 。比如 jump negative 就是条件跳转的一个例子,还有其他类型的条件跳转,比如、 jump if equal(如果相等)、jump if greater(如果更大)。\n现在把代码弄花哨一点,再过一遍代码。\n就像之前,程序先把内存值放入寄存器 a 和 b 。寄存器 a 是 11,寄存器 b 是 5 ;sub b a,用 a 减 b,11-5=6 ,6 存入寄存器 a ;jump negative 出场,上一次 alu 运算的结果是 6 ,是正数,所以 \u0026ldquo;负数标志\u0026rdquo; 是假 ,因此处理器不会执行 jump 。\n继续下一条指令 - jump 2 , jump 2 没有条件,直接执行!又回到寄存器 a-b,6-5=1 ,a 变成 1 ;\n下一条指令又是 jump negative ,因为 1 还是正数,因此 jump negative 不会执行 ;来到下一条指令,jump 2 ,又来减一次 ,这次就不一样了 1-5=-4 ,这次 alu 的 \u0026ldquo;负数标志\u0026rdquo; 是真,现在下一条指令, jump negative 5,cpu 的执行跳到内存地址 5 ,跳出了无限循环!\n现在的指令是 add b a,-4+5=1,1 存入寄存器 a ;下一条指令 store_a 13,把 a 的值存入内存地址 13 ,最后碰到 halt 指令,停下来。\n虽然程序只有 7 个指令,但 cpu 执行了 13 个指令,因为在内部循环了 2 次。\n这些代码其实是算余数的,11 除 5 余 1 。\n如果加多几行指令,我们还可以跟踪循环了多少次,11 除 5,循环 2 次,余 1 。当然,我们可以用任意 2 个数,7 和 81,18 和 54,什么都行,这就是软件的强大之处。\n软件还让我们做到硬件做不到的事,alu 可没有除法功能,是程序给了我们这个功能。别的程序也可以用我们的除法程序,来做其他事情。\n这意味着一层新抽象!\n我们这里假设的 cpu 很基础,所有指令都是 8 位,操作码只占了前面 4 位,即便用尽 4 位,也只能代表 16 个指令,而且我们有几条指令,是用后 4 位来指定内存地址。因为 4 位最多只能表示 16 个值,所以我们只能操作 16 个地址,这可不多。我们甚至不能 jump 17 ,因为 4 位二进制无法表示数字 17 ,因此,真正的现代 cpu 用两种策略: 最直接的方法是用更多位来代表指令,比如 32 位或 64 位 - 这叫 \u0026ldquo;指令长度\u0026rdquo; ;第二个策略是 \u0026ldquo;可变指令长度\u0026rdquo; 。\n举个例子,比如某个 cpu 用 8 位长度的操作码,如果看到 halt 指令,halt 不需要额外数据,那么会马上执行。如果看到 jump,它得知道位置值,这个值在 jump 的后面,这叫 \u0026ldquo;立即值\u0026rdquo;。这样设计,指令可以是任意长度,但会让读取阶段复杂一点点。要说明的是,我们拿来举例的 cpu 和指令集都是假设的,是为了展示核心原理。\n所以我们来看个真的 cpu 例子。1971 年,英特尔发布了 4004 处理器。这是第一次把 cpu 做成一个芯片,给后来的英特尔处理器打下了基础。它支持 46 个指令,足够做一台能用的电脑,它用了很多我们说过的指令,比如 jump add sub load ,它也用 8 位的\u0026quot;立即值\u0026quot;来执行 jump, 以表示更多内存地址。处理器从 1971 年到现在发展巨大,现代 cpu, 比如英特尔酷睿 i7, 有上千个指令和指令变种,长度从 1 到 15 个字节。\n举例,光 add 指令就有很多变种!指令越来越多,是因为给 cpu 设计了越来越多功能。\n下集我们会讲。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/08/","summary":"\u003cp\u003ei.e. Instructions \u0026amp; Programs\u003c/p\u003e\n\u003cp\u003e上集我们把 ALU, 控制单元,RAM, 时钟 结合在一起,做了个基本,但可用的\u0026quot;中央处理单元\u0026quot;, 简称 CPU ,它是计算机的核心。\u003c/p\u003e\n\u003cp\u003e我们已经用电路做了很多组件,这次我们给 CPU 一些指令来运行!\nCPU 之所以强大,是因为它是可编程的(programmable)- 如果写入不同指令,就会执行不同任务。CPU 是一块硬件,可以被软件控制!\u003c/p\u003e","title":"cscc08 - 指令和程序"},{"content":"i.e. the central processing unit(cpu)\n今天我们讲 处理器(processors),提示下 - 这集可能是最难的一集,所以一旦你理解了,就会变得超厉害 der~ 😈\n我们已经做了一个算术逻辑单元(alu,arithmetic and logic unit),输入二进制,它会执行计算。我们还做了两种内存:寄存器(registers) - 很小的一块内存,能存一个值;之后我们增大做出了 ram ,ram 是一大块内存,能在不同地址存大量数字。 现在是时候把这些放在一起,组建计算机的 \u0026ldquo;心脏\u0026rdquo; 了,但这个 \u0026ldquo;心脏\u0026rdquo; 不会有任何包袱,比如人类情感。\n= 这一部分的描述真好,对上个章节的迷惑内容做了一个很不错的总结!*\n计算机的心脏是\u0026quot;中央处理单元\u0026quot;,简称 \u0026ldquo;cpu\u0026rdquo;(central processing unit)。\ncpu 负责执行程序,比如 office,safari 浏览器,你最爱的《半条命 2》。程序由一个个操作组成,这些操作叫 \u0026ldquo;指令\u0026rdquo; (instruction),因为它们\u0026quot;指示\u0026quot;计算机要做什么。如果是数学指令,比如加/减(add/subtract),cpu 会让 alu 进行数学运算,也可能是内存指令,cpu 会和内存通信,然后读/写值。\ncpu 里有很多组件,所以我们一边说一边建。我们把重点放在功能,而不是一根根线具体怎么连。当我们用一条线连接两个组件时,这条线只是所有必须线路的一个抽象,这种高层次视角叫 \u0026ldquo;微体系架构\u0026rdquo; (microarchitecture)。\n好,我们首先要一些内存,把上集做的 ram 拿来就行。为了保持简单,假设它只有 16 个位置,每个位置存 8 位;再来四个 8 位寄存器,叫 a,b,c,d,寄存器用来 临时存数据 和 操作数据。\n我们已经知道数据是以二进制值存在内存里,程序也可以存在内存里。我们可以给 cpu 支持的所有指令,分配一个 id。\n在这个假设的例子,我们用前四位存 \u0026ldquo;操作代码\u0026rdquo; (operation code),简称 \u0026ldquo;操作码\u0026rdquo; (opcode);后四位代表数据来自哪里 - 可以是寄存器或内存地址。\n我们还需要两个寄存器,来完成 cpu:\n一个寄存器追踪程序运行到哪里了,我们叫它 \u0026ldquo;指令地址寄存器\u0026rdquo; (instruction address register),顾名思义,存当前指令的内存地址; 另一个寄存器存当前指令,叫 \u0026ldquo;指令寄存器\u0026rdquo; (instruction register)。 当启动计算机时,所有寄存器从 0 开始。\n\u0026gt; 初始状态\n为了举例,我们在 ram 里放了一个程序,我们今天会过一遍。\ncpu 的第一个阶段叫 \u0026ldquo;取指令阶段\u0026rdquo; (fetch phase),负责拿到指令。首先,将 \u0026ldquo;指令地址寄存器\u0026rdquo; 连到 ram,寄存器的值为 0,因此 ram 返回地址 0 的值, 0010 1110 会复制到 \u0026ldquo;指令寄存器\u0026rdquo; 里,现在指令拿到了。\n\u0026gt; 取指令阶段\n要弄清是什么指令,才能执行(execute),而不是杀死(kill)它,这是 \u0026ldquo;解码阶段\u0026rdquo; 。\n\u0026gt; 解码阶段\n前 4 位 0010 是 load a 指令,意思是,把 ram 的值放入寄存器 a ;后 4 位 1110 是 ram 的地址,转成十进制是 14 。接下来,指令由 \u0026ldquo;控制单元\u0026rdquo; 进行解码,就像之前的所有东西,\u0026ldquo;控制单元\u0026rdquo; 也是逻辑门组成的 。比如,为了识别 \u0026ldquo;load a\u0026rdquo; 指令,需要一个电路,检查操作码是不是 0010 ,我们可以用很少的逻辑门来实现。\n\u0026gt; 检查操作码是否为 load a 的电路\n现在知道了是什么指令,就可以开始执行了,开始 \u0026ldquo;执行阶段\u0026rdquo; ,用 \u0026ldquo;检查是否 load_a 指令的电路\u0026rdquo;,可以打开 ram 的 \u0026ldquo;允许读取线\u0026rdquo;, 把地址 14 传过去,ram 拿到值, 0000 0011 ,十进制的 3 。因为是 load_a 指令,我们想把这个值只放到寄存器 a,其他寄存器不受影响,所以需要一根线,把 ram 连到 4 个寄存器,用 \u0026ldquo;检查是否 load_a 指令的电路\u0026rdquo; 启用寄存器 a 的 \u0026ldquo;允许写入线\u0026rdquo;,这就成功了 - 把 ram 地址 14 的值,放到了寄存器 a 。\n\u0026gt; 执行阶段\n既然指令完成了,我们可以关掉所有线路,去拿下一条指令,我们把 \u0026ldquo;指令地址寄存器\u0026rdquo;+1,\u0026ldquo;执行阶段\u0026quot;就此结束。\nload_a 只是 cpu 可以执行的各种指令之一, 不同指令由不同逻辑电路解码 ,这些逻辑电路会配置 cpu 内的组件来执行对应操作。具体分析这些解码电路太繁琐了,既然已经看了 1 个例子,干脆把 \u0026ldquo;控制单元 \u0026ldquo;包成一个整体,简洁一些。\n\u0026gt; 抽象了的‘控制单元’\n没错,一层新抽象。\n控制单元就像管弦乐队的指挥,\u0026ldquo;指挥\u0026rdquo; cpu 的所有组件, \u0026ldquo;取指令→解码→执行\u0026rdquo; 完成后。现在可以再来一次,从 \u0026ldquo;取指令\u0026rdquo; 开始,\u0026ldquo;指令地址寄存器\u0026rdquo; 现在的值是 1,所以 ram 返回地址 1 里的值:0001 1111 ;到 \u0026ldquo;解码\u0026rdquo; 阶段!0001 是 load b 指令,从 ram 里把一个值复制到寄存器 b ,这次内存地址是 1111,十进制的 15;现在到 \u0026ldquo;执行阶段\u0026rdquo;!\u0026ldquo;控制单元\u0026rdquo; 叫 ram 读地址 15,并配置寄存器 b 接收数据,成功,我们把值 0000 1110 ,也就是十进制的 14 存到了寄存器 b ;最后一件事是 \u0026ldquo;指令地址寄存器\u0026rdquo; +1 ,我们又完成了一个循环。 🥳\n下一条指令有点不同,来取它吧。\n1000 0100 1000 是 add 指令,这次后面的 4 位不是 ram 地址, 而是 2 位 2 位,分别代表 2 个寄存器。2 位可以表示 4 个值,所以足够表示 4 个寄存器。第一个地址是 01, 代表寄存器 b ,第二个地址是 00, 代表寄存器 a ,因此,1000 0100,代表把寄存器 b 的值,加到寄存器 a 里 。\n\u0026gt; add b 到 a\n为了执行这个指令,我们要整合第 5 集的 alu ,\u0026ldquo;控制单元\u0026rdquo; 负责选择正确的寄存器作为输入,并配置 alu 执行正确的操作。对于 \u0026ldquo;add\u0026rdquo; 指令, \u0026ldquo;控制单元\u0026rdquo; 会启用寄存器 b,作为 alu 的第一个输入,还启用寄存器 a,作为 alu 的第二个输入。之前说过,alu 可以执行不同操作,所以控制单元必须传递 add 操作码告诉它要做什么,最后,结果应该存到寄存器 a ,但不能直接写入寄存器 a ,这样新值会进入 alu ,不断和自己相加,因此,控制单元用一个自己的寄存器暂时保存结果,关闭 alu,然后把值写入正确的寄存器。这里 3+14=17,二进制是 0001 0001 ,现在存到了寄存器 a ,和之前一样,最后一件事是把指令地址 + 1 ,这个循环就完成了。\n好,来看最后一个指令:0100 1101 ,解码得知是 store a 指令(把寄存器 a 的值放入内存), ram 地址 13 ,接下来,把地址传给 ram ,但这次不是 \u0026ldquo;允许读取\u0026rdquo; ,而是 \u0026ldquo;允许写入\u0026rdquo; 。同时,打开寄存器 a 的 \u0026ldquo;允许读取\u0026rdquo; ,这样就可以把寄存器 a 里的值,传给 ram 。\n\u0026gt; 存储 a 到 ram\n恭喜,我们刚运行了第一个电脑程序!它从内存中加载两个值,相加,然后把结果放回内存。\n刚刚是我一步步来讲的,我们人工切换 cpu 的状态 \u0026ldquo;取指令→解码→执行\u0026rdquo; ,但不是每台电脑里都有一个迷你 carrie anne ,其实是 \u0026ldquo;时钟\u0026rdquo; 来负责管理 cpu 的节奏。时钟以精确的间隔触发电信号,控制单元会用这个信号,推进 cpu 的内部操作,确保一切按步骤进行 - 就像罗马帆船的船头,有一个人负责按节奏的击鼓,让所有划船的人同步。.. 就像节拍器一样。节奏不能太快,因为就算是电也要一定时间来传输。cpu \u0026ldquo;取指令→解码→执行\u0026rdquo; 的速度叫 \u0026ldquo;时钟速度\u0026rdquo; ,单位是赫兹 - 赫兹是用来表示频率的单位,1 赫兹代表一秒 1 个周期。因为我花了大概 6 分钟,给你讲了 4 条指令 - 读取→读取→相加→存储 ,所以我的时钟速度大概是 0.03 赫兹,我承认我算数不快,但哪怕有人算数很快,最多也就是一秒一次,或 1 赫兹。\n\u0026gt; ‘时钟’ 哦\n第一个单芯片 cpu 是 \u0026ldquo;英特尔 4004\u0026rdquo; ,1971 年发布的 4 位 cpu ,它的微架构很像我们之前说的 cpu 。\n\u0026gt; 英特尔 4004 的微架构\n\u0026gt; 英特尔 4004\n虽然是第一个单芯片的处理器,但它的时钟速度达到了 740 千赫兹 - 每秒 74 万次,你可能觉得很快,但和如今的处理器相比不值一提,一兆赫兹是 1 秒 1 百万个时钟周期,你现在看视频的电脑或手机,肯定有几千兆赫兹 - 1 秒 10 亿次时钟周期。你可能听过有人会把计算机超频(overclocking),意思是修改时钟速度,加快 cpu 的速度 - 就像罗马帆船要撞另一艘船时,鼓手会加快敲鼓速度。芯片制造商经常给 cpu 留一点余地,可以接受一点超频,但超频太多会让 cpu 过热或产生乱码,因为信号跟不上时钟。你可能很少听说降频,但降频其实很有用,有时没必要让处理器全速运行,可能用户走开了,或者在跑一个性能要求较低的程序,把 cpu 的速度降下来,可以省很多电。省电对用电池的设备很重要,比如笔记本和手机。为了尽可能省电,很多现代处理器可以按需求加快或减慢时钟速度,这叫 \u0026ldquo;动态调整频率\u0026rdquo; ,加上时钟后,cpu 才是完整的。\n现在可以放到盒子里,变成一个独立组件。\n\u0026gt; 抽象的 cpu\n对!一层新的抽象!\nram,上集说过,是在 cpu 外面的独立组件,cpu 和 ram 之间用 \u0026ldquo;地址线\u0026rdquo;、\u0026ldquo;数据线\u0026rdquo; 和 \u0026ldquo;允许读/写线\u0026rdquo; 进行通信。\n虽然今天我们设计的 cpu 是简化版的,但我们提到的很多机制,依然存在于现代处理器里。\n下一集,我们要加强 cpu,给它扩展更多指令,同时开始讲软件。\n下周见。\n","date":"2022-08-26","permalink":"https://loveminimal.github.io/posts/cscc/07/","summary":"\u003cp\u003ei.e. The Central Processing Unit(CPU)\u003c/p\u003e\n\u003cp\u003e今天我们讲 处理器(processors),提示下 - 这集可能是最难的一集,所以一旦你理解了,就会变得超厉害 der~ 😈\u003c/p\u003e\n\u003cp\u003e我们已经做了一个算术逻辑单元(ALU,Arithmetic and Logic Unit),输入二进制,它会执行计算。\u003cstrong\u003e我们还做了两种内存:寄存器(Registers) - 很小的一块内存,能存一个值;之后我们增大做出了 RAM ,RAM 是一大块内存,能在不同地址存大量数字。\u003c/strong\u003e 现在是时候把这些放在一起,组建计算机的 \u0026ldquo;心脏\u0026rdquo; 了,但这个 \u0026ldquo;心脏\u0026rdquo; 不会有任何包袱,比如人类情感。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 这一部分的描述真好,对上个章节的迷惑内容做了一个很不错的总结!*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"cscc07 - 中央处理器"},{"content":"i.e. registers and ram\n上集,我们用逻辑门做了个简单 alu,它能执行算术 (arithmetic) 和逻辑 (logic) 运算 ,alu 里的 a 和 l 因此得名。当然,算出来之后如果扔掉就没什么意义了,得找个方法存起来,可能还要进行多个连续操作,这就用到计算机内存了。\n= 算出来,存起来。\n如果你在主机上打过一场长时间的对局,或玩困难模式的 \u0026ldquo;扫雷\u0026rdquo;(minesweeper),然后狗跑过来,被电源线绊倒,把插头拔了出来,你知道失去进度的痛苦 😈 真同情你 :(\n你损失数据的原因是,电脑用的是\u0026quot;随机存取存储器\u0026quot;,简称\u0026quot;ram\u0026quot;(random access memory),它只能在有电的情况下存储东西,比如游戏状态,另一种存储 (memory) 叫持久存储,电源关闭时数据也不会丢失,它用来存其他东西,我们之后会讨论存储 (memory) 的持久性问题。\n今天我们从简单开始 - 做只能存储 1 位的电路,之后再扩大,做出我们的内存模块,下次和 alu 结合起来,做出 cpu !\n只能存储 0 和 1 的电路 我们至今说过的电路都是单向的 - 总是向前流动,比如上集的 8 位 \u0026ldquo;脉动进位加法器\u0026rdquo;。但也可以做回向电路,把输出连回输入。\n我们拿一个 or 门试试,把输出连回输入,看看会发生什么。\n首先,两个输入都设为 0 ,\u0026ldquo;0 or 0\u0026rdquo; 是 0,所以电路输出 0 ;如果将 a 变成 1 ,\u0026ldquo;1 or 0\u0026rdquo; 为 1,所以输出 1 ;一转眼的功夫,输出回到 b ,or 门看到两个输入都是 1 ,\u0026ldquo;1 or 1\u0026rdquo; 仍然为 1,所以输出不变 。 如果将 a 变成 0,or 门依然输出 1 ,现在我们有个电路能记录 \u0026ldquo;1\u0026rdquo; ,然而有个小问题:这是永久的 ! 无论怎么试,都没法从 1 变回 0 。\n我们换成 and 门看看会怎样。\n开始时,a 和 b 都设 1 ,\u0026ldquo;1 and 1\u0026rdquo; 永远输出 1 ;如果之后 a 设为 0,由于是 and 门,输出会变成 0 ,这个电路能记录 0,和之前那个相反。就像之前,无论 a 设什么值,电路始终输出 0 。\n现在有了能存 0 和 1 的电路。\n= 或门存 1 ,与门存 0\n锁存器 为了做出有用的存储 (memory),我们把两个电路结合起来,这叫 \u0026ldquo;and-or 锁存器\u0026rdquo; 。它有两个输入:\u0026ldquo;设置\u0026quot;输入,把输出变成 1;\u0026ldquo;复位\u0026quot;输入,把输出变成 0 。如果\u0026quot;设置\u0026quot;和\u0026quot;复位\u0026quot;都是 0,电路会输出最后放入的内容,也就是说,它存住了 1 位的信息!\n\u0026gt; and-or 锁存器\n存储!这叫\u0026quot;锁存\u0026rdquo;(latch), 因为它\u0026quot;锁定\u0026quot;了一个值。放入数据的动作叫 \u0026ldquo;写入\u0026rdquo;,拿出数据的动作叫 \u0026ldquo;读取\u0026rdquo;。现在我们终于有办法存一个位了!超棒 !\n麻烦的是,用两条线 \u0026ldquo;设置\u0026quot;和\u0026quot;复位\u0026rdquo; 来输入,有点难理解。\n⬇️ ⬇️ ⬇️\n为了更容易用,我们希望只有一条输入线,将它设为 0 或 1 来存储值;还需要一根线来\u0026quot;启用\u0026quot;内存,启用时允许写入,没启用时就 \u0026ldquo;锁定\u0026rdquo; - 这条线叫 \u0026ldquo;允许写入线\u0026rdquo;。加一些额外逻辑门,可以做出这个电路,这叫 \u0026ldquo;门锁\u0026rdquo;,因为门可以打开和关上。\n1. 写什么 (0、1) 2. 是否允许写入\r现在有点复杂了,我们不想关心单独的逻辑门,所以我们提升一层抽象。\n把 \u0026ldquo;门锁\u0026rdquo; 放到盒子里 - 这个盒子能存一个 bit 。\n\u0026gt; 门锁\n我们来测一下新组件!一切从 0 开始,数据输入从 0 换到 1, 从 1 换到 0,什么也不会发生 - 输出依然是 0,因为 \u0026ldquo;允许写入线\u0026rdquo; 是关闭的,所以内容不会变化。所以要给 \u0026ldquo;允许写入线\u0026rdquo; 输入 1, \u0026ldquo;打开\u0026rdquo; 门,现在往 \u0026ldquo;数据线\u0026rdquo; 放 1,1 就能存起来了,注意输出现在是 1 了,成功!现在可以关掉 \u0026ldquo;允许写入线\u0026rdquo; ,输出会保持 1,现在不管给 \u0026ldquo;数据线\u0026rdquo; 什么值,输出都不会变。值存起来了!现在又打开 \u0026ldquo;允许写入线\u0026rdquo; \u0026ldquo;数据线\u0026rdquo; 设为 0,完成,\u0026ldquo;允许写入线\u0026rdquo; 关闭,输出 0,成功了!\n= 不难发现,允许写入线关闭的时候,上面的 and 门输出总为 0 。\n寄存器 当然,只能存 1 bit 没什么大用,肯定玩不了游戏,或做其它事情,但我们没限制只能用一个锁存器。如果我们并排放 8 个锁存器,可以存 8 位信息,比如一个 8 bit 数字,一组这样的锁存器叫 \u0026ldquo;寄存器\u0026rdquo; 。寄存器能存一个数字,这个数字有多少位,叫 \u0026ldquo;位宽\u0026rdquo; 。\n= 寄存器的‘位宽’就是你放了多少个‘锁存器’~~\n\u0026gt; 8 位寄存器 -\u0026gt; d 数据输入 q 数据输出 e 允许写入线\n早期电脑用 8 位寄存器,然后是 16 位,32 位,如今许多计算机都有 64 位宽的寄存器。写入寄存器前,要先启用里面所有锁存器,我们可以用一根线连接所有 \u0026ldquo;允许写入线\u0026rdquo;, 把它设为 1 ,然后用 8 条数据线发数据,然后将 \u0026ldquo;允许写入线\u0026rdquo; 设回 0 ,现在 8 位的值就存起来了。\n如果只有很少的位 (bits),把锁存器并排放置,也勉强够用了。\n64 位寄存器要 64 根数据线,64 根连到输出端。幸运的是,我们只要 1 根线启用所有锁存器,但加起来也有 129 条线了。如果存 256 位要 513 条线!而这需要不少的钱 💵,怎么办?解决方法是矩阵!\n= 哈,二维化!\n\u0026gt; 16*16 门锁矩阵\n在矩阵中,我们不并列排放锁存器,而是做成网格,存 256 位,我们用 16x16 网格的锁存器,有 16 行 16 列,要启用某个锁存器,就打开相应的 行线 和 列线。\n放大看看怎么做的。\n\u0026gt; 锁存器\n我们只想打开交叉处锁存器的 \u0026ldquo;允许写入线\u0026rdquo;,所有其他锁存器,保持关闭,我们可以用 and 门!只有 行线和列线 均为 1 ,and 门才输出 1,所以可以用选择单个锁存器。这种行/列排列法,用一根 \u0026ldquo;允许写入线\u0026rdquo; 连所有锁存器,为了让锁存器变成 \u0026ldquo;允许写入\u0026rdquo;, 行线,列线和 \u0026ldquo;允许写入线\u0026rdquo; 都必须是 1 ,每次只有 1 个锁存器会这样。代表我们可以只用一根 \u0026ldquo;数据线\u0026rdquo; 连所有锁存器来传数据。因为只有一个锁存器会启用,只有那个会存数据,其他锁存器会忽略数据线上的值,因为没有 \u0026ldquo;允许写入\u0026rdquo;。我们可以用类似的技巧,做\u0026quot;允许读取线\u0026quot;来读数据,从一个指定的锁存器,读取数据。所以对于 256 位的存储,只要 35 条线 - 1 条\u0026quot;数据线\u0026rdquo;, 1 条\u0026quot;允许写入线\u0026quot;, 1 条\u0026quot;允许读取线\u0026quot;,还有 16 行 16 列的线用于选择锁存器 (16+16=32, 32+3=35),这省了好多线!🤑\n💡 看,只需要这些线 :\r\u0026gt; 1 条数据线\r\u0026gt; 1 条允许写入线\r\u0026gt; 1 条允许读取线\r\u0026gt; 16 条行线\r\u0026gt; 16 条列线 但我们需要某种方法来唯一指定交叉路口。\n我们可以想成城市,你可能想和别人在第 12 大道和第 8 街的交界碰面 - 这是一个交叉点的地址,我们刚刚存了一位的地址是 \u0026ldquo;12 行 8 列\u0026rdquo;,由于最多 16 行,用 4 位就够了,12 用二进制表示为 1100 ,列地址也可以这样: 8 用二进制表示为 1000 ,刚才说的\u0026quot;12 行 8 列\u0026quot;可以写成 11001000 。\n为了将地址转成行和列,我们需要 \u0026ldquo;多路复用器\u0026rdquo; - 这个名字起码比 alu 酷一点,多路复用器有不同大小,因为有 16 行,我们需要 1 到 16 多路复用器。工作方式是:输入一个 4 位数字,它会把那根线,连到相应的输出线,如果输入 0000,它会选择第一列,如果输入 0001,会选择下一列,依此类推……\n\u0026gt; 会‘寻址’的多路复用器\n一个多路复用器处理行 (row) ,另一个多路复用器处理列 (column)。\n好吧,开始有点复杂了,那么把 256 位内存当成一个整体好了。 又提升了一层抽象! 它输入一个 8 位地址 :4 位代表列,4 位代表行(纵横经纬,用来唯一确定启动的锁存器位置)。我们还需要 \u0026ldquo;允许写入线\u0026rdquo; 和 \u0026ldquo;允许读取线\u0026rdquo;,最后,还需要一条数据线,用于读/写数据 。\n= 💡 提示,这个 ‘8 位地址’ 的含义是 - 定位使用 256 个锁存器中的哪一个。也就是说,虽然我们的 256 位‘寄存器’包含 256 个锁存器,但是同一时间,我们只能使用其中地址相对应的那一个而已。即同一时间,只能存储 1bit 的数。\n= ❓ 这里可能会产生疑惑?既然只能存 1bit 的数,为什么不直接使用 1 个锁存器来存储???\n= 关于‘寄存器’和‘内存’的区别,需要留意一下。‘寄存器’一般是指 cpu 上的高速存储,造价不菲,一般不大(128m 都算很大的了),‘内存’你可以理解为内存条,即 ram ,造价相对便宜,常见单条有 8g 、16g 了。\n可寻址内存 不幸的是,256 位的内存也没法做什么事,所以还要扩大规模,把它们并排放置,就像寄存器一样。一行 8 个,可以存一个 8 位数字,8 位也叫一个字节(byte)。为了存一个 8 位数字,我们同时给 8 个 256 位内存一样的地址,每个地址存 1 位,意味着这里总共能存 256 个字节 (byte)。\n\u0026gt; 看,我们获得了一个 1 byte(8 bit) 大小的可寻址内存了\n= 这里,我们将 8 个 ‘256 位的寄存器’并排连接,就可以存储 8 bit 的数。这个数的每一位,分别存在 ‘8 位地址’对应的 8 个 256 位寄存器的相应的锁存器中。\n= ❕ 也就是说,上图中的 ‘8 个并排寄存器’ ,共有 256 个 8 位地址,每个地址可以存储 1 byte(8 bit) 的数,最多可以存储 256 byte 的数。看,锁存器还是得到了充分利用的,这就解答了上面的疑问。\n= 好的吧,其实这里我是有点震惊的!原来一个 8 位可寻址内存就需要用 8 个 256 内存,每个内存的相同行列处(相同地址)的锁存器启用 - 存储 1 bit 。\n再次,为了简单,我们不管内部,不看作是一堆独立的存储模块和电路,而是看成一个整体的 可寻址内存 。我们有 256 个地址,每个地址能读或写一个 8 位值。\n\u0026gt; 256 byte 大小的内存 - 又抽象了\n= 上图其实就是前面 8 个并排‘256 位寄存器’的抽象。\n我们下集做 cpu 时会用到这个内存。\n现代计算机的内存扩展到上兆字节(mb)和千兆字节(gb)的方式,和我们这里做的一样,不断把内存打包到更大规模。随着内存地址增多,内存地址也必须增长。8 位最多能代表 256 个内存地址(1111 1111 是 255,0~255 一共 256 个数字),只有这么多。要给千兆或十亿字节的内存寻址,需要 32 位的地址。\n= 额,指数级增长,就离谱!指数 🐮 🍺 !\n内存的一个重要特性是:可以随时访问任何位置,因此叫 \u0026ldquo;随机存取存储器\u0026rdquo; ,简称 ram 。\n当你听到有人说 ram 有多大,他的意思是内存有多大。ram 就像人类的短期记忆,记录当前在做什么事。比如吃了午饭没,或有没有交电话费。\n这是一条真的内存,上面焊了 8 个内存模块。\n如果打开其中一个,然后放大,会看到 32 个内存方块。\n\u0026gt; 其中一个芯片\n放大其中一个方块,可以看到有 4 个小块。\n如果再放大,可以看到存一个\u0026quot;位\u0026quot;的矩阵,这个矩阵是 128 位 x 64 位,总共 8192 位。\n= 哈哈,这里很好的解答了阅览前方内容时产生的一个疑惑,是矩阵,而不一定是方阵。*\n每个方格 4 个矩阵,所以一个方格有 32768 个位 (8192 x 4 = 32768),而一共 32 个方格。总而言之,1 个芯片大约存 100 万位。ram 有 8 个芯片,所以总共 800 万位,也就是 1 兆字节(1 mb)。\n1 mb 如今不算大 - 这是 1980 年代的 ram。\n= 现在基本是 8g 、16g 大小的了,那么一个 16g 大小的内存就有 16 * 1024 * 1024 * 8 = 2^27 = 134,217,728 位。*\n= 目前最大为单条 64g,也就是 2^29 位,理论上目前电脑支持的最大运行内存为 128g (2^30)。\n如今你可以买到千兆字节(gb)的 ram,那可是数十亿字节的内存。\n今天,我们用锁存器做了一块 sram(静态随机存取存储器),还有其他类型的 ram,如 dram,闪存和 nvram ,它们在功能上与 sram 相似,但用不同的电路存单个位 - 比如用不同的逻辑门,电容器,电荷捕获或忆阻器。但根本上,这些技术都是矩阵层层嵌套,来存储大量信息。 就像计算机中的很多事情,底层其实都很简单,让人难以理解的,是一层层精妙的抽象。\n像一个越来越小的俄罗斯套娃。\n下周见。\n= 该部分的内存相对来说是有些抽象的,尤其是一些相关的概念可能会混淆 - 如‘寄存器’和‘内存’,它们的原理是一样的,只有‘寄存器’用的材料更好更贵,当然速度也更快。文章的后半部分,我们在纸上模拟出了一块可以存储 256 byte 的内存(8 个并排的 256 位寄存器组成 - 同样,这里用寄存器来描述好像是不太准确)。在真实的示例中,我们也看到了可以存储 1mb 的内存条组成。只能说,学无止境哦!不过基本原理相对来说是简单的,不得不说,现在的许多计算机课程跑偏的挺厉害的,基础原理和应用才是永远的神!*\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/06/","summary":"\u003cp\u003ei.e. Registers and RAM\u003c/p\u003e\n\u003cp\u003e上集,我们用逻辑门做了个简单 ALU,它能执行算术 (Arithmetic) 和逻辑 (Logic) 运算 ,ALU 里的 A 和 L 因此得名。当然,算出来之后如果扔掉就没什么意义了,得找个方法存起来,可能还要进行多个连续操作,这就用到计算机内存了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 算出来,存起来。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果你在主机上打过一场长时间的对局,或玩困难模式的 \u0026ldquo;扫雷\u0026rdquo;(Minesweeper),然后狗跑过来,被电源线绊倒,把插头拔了出来,你知道失去进度的痛苦 😈 真同情你 :(\u003c/p\u003e","title":"cscc06 - 寄存器 \u0026 内存"},{"content":"i.e. how computers calculate-the alu\n上集,我们谈了如何用二进制表示数字,比如二进制 00101010 是十进制的 42,表示和存储数字是计算机的重要功能。但真正的目标是计算(computation),有意义的处理数字,比如把两个数字相加,这些操作由计算机的 \u0026ldquo;算术逻辑单元 (arithmetic and logic unit)\u0026ldquo;处理,但大家会简称:alu。\nalu 是计算机的数学大脑,等你理解了 alu 的设计和功能之后,你就理解了现代计算机的基石。\nalu 就是计算机里负责运算的组件,基本其他所有部件都用到了它,先来看看这个美人。这可能是最著名的 alu,英特尔 74181 。1970 年发布时,它是第一个封装在单个芯片内的完整 alu ,这在当时是惊人的工程壮举。\n\u0026gt; 英特尔 74181\n今天我们用上周学的布尔逻辑门,做一个简单的 alu 电路,功能和 74181 一样。然后接下来几集,用它从头做出一台电脑,所以会有点复杂,但我觉得你们搞的定。\nalu 有 2 个单元,1 个算术单元和 1 个逻辑单元。\n算术单元 我们先讲\u0026quot;算术单元\u0026rdquo;,它负责计算机里的所有数字操作。比如加减法,它还做很多其他事情,比如给某个数字+1,这叫增量运算,我们之后会说。\n今天的重点是一切的根本 - \u0026ldquo;把两个数字相加\u0026rdquo; 。\n我们可以用单个晶体管一个个拼,把这个电路做出来,但很快就会复杂的难以理解,所以与其用晶体管,我们会像第 3 集 - 用更高层的抽象,用逻辑门来做。我们会用到 and,or,not 和 xor 逻辑门,最简单的加法电路, 是拿 2 个 bit 加在一起(bit 是 0 或 1),有 2 个输入:a 和 b, 1 个输出:就是两个数字的和。需要注意的是:a, b, 输出 ,这 3 个都是单个 bit ( 0 或 1 )。\n输入只有四种可能:\n前三个是 0 + 0 = 0 、 1 + 0 = 1 、0 + 1 = 1 。记住二进制里,1 与 true 相同,0 与 false 相同,这组输入和输出,和 xor 门的逻辑完全一样,所以我们可以把 xor 用作 1 位加法器(adder)。\n但第四个输入组合,1+1 是个特例 1 + 1 = 2(显然),但二进制里没有 2,上集说过,二进制 1+1 的结果是 0,1 进到下一位,和是 10 (二进制)。\n\u0026gt; 1 + 1 = 10 呢 ?\nxor 门的输出,只对了一部分, 1+1 输出 0 ,但我们需要一根额外的线代表 \u0026ldquo;进位\u0026rdquo;,只有输入是 1 和 1 时,进位才是 \u0026ldquo;true\u0026rdquo; ( 也就是 1 )。因为算出来的结果用 1 个 bit 存不下,方便的是,我们刚好有个逻辑门能做这个事!没那么复杂 - 就两个逻辑门而已。\n\u0026gt; 1 + 1 = 10 了(carry = 1, sum = 0)\n让我们抽象化。\n把 \u0026ldquo;半加器\u0026rdquo; 封装成一个单独组件 - 两个输入 a 和 b 都是 1 位 ,两个输出 \u0026ldquo;总和\u0026rdquo; 与 \u0026ldquo;进位\u0026rdquo;,这进入了另一层抽象。\n\u0026gt; 抽象出了‘半加器’\n如果想处理超过 1+1 的运算,我们需要\u0026quot;全加器\u0026rdquo;,半加器输出了进位,意味着,我们算下一列的时候,还有之后的每一列,我们得加 3 个位在一起,并不是 2 个。\n全加器复杂了一点点。\n全加器表格,有 3 个输入:a, b, c (都是 1 个 bit),所以最大的可能是 1 + 1 + 1,\u0026ldquo;总和\u0026quot;1 \u0026ldquo;进位\u0026quot;1 ,所以要两条输出线: \u0026ldquo;总和\u0026quot;和\u0026quot;进位\u0026rdquo; 。\n我们可以用 半加器 做 全加器,我们先用半加器将 a 和 b 相加,然后把 c 输入到第二个半加器,最后用一个 or 门检查进位是不是 true,这样就做出了一个全加器!\n\u0026gt; emm... ‘全加器’的诞生\n我们可以再提升一层抽象,把全加器作为独立组件。全加器会把 a,b,c 三个输入加起来,输出 \u0026ldquo;总和\u0026rdquo; 和 \u0026ldquo;进位\u0026rdquo;。\n现在有了新组件,我们可以相加两个 8 位数字。叫两个数字叫 a 和 b 好了,我们从 a 和 b 的第一位开始,叫 a0 和 b0 好了,现在不用处理任何进位,因为是第一次加法,所以我们可以用半加器,来加这 2 个数字,输出叫 sum0 ;现在加 a1 和 b1,因为 a0 和 b0 的结果有可能进位,所以这次要用全加器,除了 a1 和 b1,还要连上进位,输出叫 sum1 ;然后,把这个全加器的进位连到下个全加器的输入,处理 a2 和 b2;以此类推,把 8 个 bit 都搞定…… 注意每个进位是怎么连到下一个全加器的,所以叫 \u0026ldquo;8 位行波进位加法器\u0026rdquo; ,注意最后一个全加器有 \u0026ldquo;进位\u0026rdquo; 的输出。如果第 9 位有进位,代表着 2 个数字的和太大了,超过了 8 位,这叫 \u0026ldquo;溢出\u0026rdquo; (overflow)。一般来说 \u0026ldquo;溢出\u0026rdquo; 的意思是,两个数字的和太大了,超过了用来表示的位数,这会导致错误和不可预期的结果。\n\u0026gt; 8 位行波进位加法器\n著名的例子是,吃豆人用 8 位存当前关卡数,如果你玩到了第 256 关( 8 位 bit 最大表示 255),alu 会溢出,造成一连串错误和乱码(glitches),使得该关卡无法进行,这个 bug 成了厉害吃豆人玩家的代表。如果想避免溢出,我们可以加更多全加器,可以操作 16 或 32 位数字,让溢出更难发生,但代价是更多逻辑门,另外一个缺点是,每次进位都要一点时间,当然时间不久,因为电子移动的很快,但如今的量级是每秒几十亿次运算,所以会造成影响。所以,现代计算机用的加法电路有点不同,叫 \u0026ldquo;超前进位加法器\u0026rdquo;。它更快,做的事情是一样的 - 把二进制数相加。\n\u0026gt; 牛‘爆’的吃豆人\nalu 的算术单元,也能做一些其他数学运算,一般支持这 8 个操作 - 见下图。\n= 哪 8 个 ❓\n就像加法器一样,这些操作也是由逻辑门构成的。有趣的是,你可能注意到没有乘法和除法。因为简单的 alu 没有专门的电路来处理,而是把乘法用多次加法来实现。假设想算 12 x 5 ,这和把 \u0026ldquo;12\u0026rdquo; 加 5 次是一样的,所以要 5 次 alu 操作来实现这个乘法。很多简单处理器(processors)都是这样做的,比如恒温器,电视遥控器和微波炉,慢是慢,但是搞的定。然而笔记本和手机有更好的处理器,有专门做乘法的算术单元,你可能猜到了,乘法电路比加法复杂 - 没什么魔法,只是更多逻辑门,所以便宜的处理器没有。\n= 是的,只是逻辑门而已 ✔️\n好了,我们现在讲 alu 的另一半:逻辑单元。\n逻辑单元 逻辑单元执行逻辑操作,比如之前讨论过的 and,or 和 not 操作,它也能做简单的数值测试,比如一个数字是不是负数。\n例如,这是检查 alu 输出是否为 0 的电路,它用一堆 or 门检查其中一位是否为 1,哪怕只有一个 bit (位) 是 1,我们就知道那个数字肯定不是 0,然后用一个 not 门取反,所以只有输入的数字是 0,输出才为 1 。\n\u0026gt; 检查 alu 输出是否为 0 的电路\n以上就是 alu 的一个高层次概括。\nalu 我们甚至从零做了几个主要组件,比如行波进位加法器(ripple adder),它们只是一大堆逻辑门巧妙的连在一起而已。\n让我们回到视频开始时的 alu,英特尔 74181,和我们刚刚做的 8 位 alu 不同,74181 只能处理 4 位输入。也就是说,你刚做了一个比英特尔 74181 还好的 alu !\n其实 差不多啦。我们虽然没有全部造出来,但你理解了整体概念。\n74181 用了大概 70 个逻辑门,但不能执行乘除。但它向小型化迈出了一大步,让计算机可以更强大更便宜。4 位 alu 已经要很多逻辑门了,但我们的 8 位 alu 会需要数百个逻辑门。工程师不想在用 alu 时去想那些事情,所以想了一个特殊符号来代表它,看起来像一个大 \u0026ldquo;v\u0026rdquo; 。\n又一层抽象!\n我们的 8 位 alu 有两个输入,a 和 b,都是 8 位 (bits)。\n我们还需要告诉 alu 执行什么操作 ,例如加法或减法,所以我们用 4 位的操作代码。我们之后的视频会再细说。简言之,\u0026ldquo;1000\u0026quot;可能代表加法命令,\u0026ldquo;1100\u0026quot;代表减法命令,操作代码告诉 alu 执行什么操作,输出结果是 8 位的。alu 还会输出一堆标志(flag),\u0026ldquo;标志\u0026quot;是 1 位的,代表特定状态。比如相减两个数字,结果为 0 ,我们的零测试电路(前面做的)会将 零标志 设为 true(1),如果想知道两个数字是否相等,这个非常有用。如果想知道: a 是否小于 b,可以用 alu 来算 a 减 b,看 负标志 是否为 true 。如果是 true,我们就知道 a 小于 b 。最后,还有一条线连到加法器的进位,如果有溢出,我们就知道,这叫 溢出标志 。\n高级 alu 有更多标志,但这 3 个标志是 alu 普遍用的。其实,我们之后的视频会用到它们。\n现在你知道了,计算机是怎样在没有齿轮或杠杆的情况下进行运算。\n接下来两集,我们会用 alu 做 cpu ,但在此之前,计算机需要一些 \u0026ldquo;记忆\u0026rdquo; !\n我们下周会讲。\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/05/","summary":"\u003cp\u003ei.e. How Computers Calculate-the ALU\u003c/p\u003e\n\u003cp\u003e上集,我们谈了如何用二进制表示数字,比如二进制 \u003ccode\u003e00101010\u003c/code\u003e 是十进制的 42,表示和存储数字是计算机的重要功能。但\u003cstrong\u003e真正的目标是计算(computation)\u003c/strong\u003e,有意义的处理数字,比如把两个数字相加,这些操作由计算机的 \u0026ldquo;算术逻辑单元 (Arithmetic and Logic Unit)\u0026ldquo;处理,但大家会简称:ALU。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eALU 是计算机的数学大脑,等你理解了 ALU 的设计和功能之后,你就理解了现代计算机的基石。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eALU 就是计算机里负责运算的组件,基本其他所有部件都用到了它,先来看看这个美人。这可能是最著名的 ALU,英特尔 74181 。1970 年发布时,它是第一个封装在单个芯片内的完整 ALU ,这在当时是惊人的工程壮举。\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/05/imgs/94bce3a3e3a075eae098b8bf3b522246e0fd78c936596fb1e2a9084e9514019b.png\" width=\"300\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; 英特尔 74181\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e今天我们用上周学的布尔逻辑门,做一个简单的 ALU 电路,功能和 74181 一样。然后接下来几集,用它从头做出一台电脑,所以会有点复杂,但我觉得你们搞的定。\u003c/p\u003e","title":"cscc05 - 算术逻辑单元"},{"content":"i.e. representing numbers and letters with binary\n今天,我们讲计算机如何存储和表示数字,所以会有一些数学,不过别担心,你们的数学水平绝对够用了。\n上集我们讲了,怎么用晶体管(transistors)做逻辑门(logic gates),逻辑门可以判断布尔语句,布尔代数只有两个值:true 和 false 。\n但如果只有两个值,我们怎么表达更多东西 ❓ 这就需要数学了!\n进制和运算 上集提到,1 个二进制值可以代表 1 个数,我们可以把真和假 ,当做 1 和 0 。如果想表示更多东西,加位数就行了。和我们熟悉的十进制(decimal)一样,十进制只有 10 个数(0 到 9),要表示大于 9 的数,加位数就行了。\n二进制也可以这样玩。\n拿 263 举例,这个数字 \u0026ldquo;实际\u0026rdquo; 代表什么?2 个 100、6 个 10、3 个 1,加在一起,就是 263。注意每列有不同的乘数 - 100、10、1 ,每个乘数都比右边大十倍,因为每列有 10 个可能的数字(0 到 9),如果超过 9,要在下一列进 1 ,因此叫 \u0026ldquo;基于十的表示法\u0026rdquo; 或 十进制。\n二进制也一样,只不过是基于 2 而已。因为二进制只有两个可能的数, 1 和 0 ,意味着每个乘数必须是右侧乘数的两倍,就不是之前的 100、10、1 ,而是 4、2、1 。\n拿二进制数 101 举例,意味着有 1 个 \u0026ldquo;4\u0026rdquo;、 0 个 \u0026ldquo;2 、1 个 \u0026ldquo;1\u0026rdquo;,加在一起,得到十进制的 5 。\n为了表示更大的数字,二进制需要更多位数。拿二进制数 10110111 举例,我们可以用相同的方法转成十进制 - 1 x 128 ,0 x 64 ,1 x 32 ,1 x 16,0 x 8 ,1 x 4 ,1 x 2 ,1 x 1 ,加起来等于 183。\n二进制数的计算也不难。\n以十进制数 183 加 19 举例,首先 3 + 9,得到 12,然后位数记作 2,向前进 1,现在算 8+1+1=10,所以位数记作 0,再向前进 1,最后 1+1=2,位数记作 2,所以和是 202。\n二进制也一样。\n和之前一样,从个位开始,1+1=2,在二进制中也是如此,但二进制中没有 2,所以位数记作 0 ,进 1。就像十进制的例子一样,1+1,再加上进位的 1,等于 3,用二进制表示是 11。所以位数记作 1,再进 1,以此类推,最后得到这个数字,跟十进制 202 是一样的。\n= 二进制中,逢二进一。\n位和字节 二进制中,一个 1 或 0 叫一 \u0026ldquo;位\u0026rdquo;(bit)。\n上个例子我们用了 8 位 , 8 位能表示的最小数是 0, 8 位都是 0,最大数是 255,8 位都是 1,能表示 256 个不同的值,2 的 8 次方。你可能听过 8 位机,8 位图像,8 位音乐,意思是计算机里大部分操作都是 8 位 8 位这样处理的。但 256 个值不算多,意味着 8 位游戏只能用 256 种颜色。\n8 位是如此常见,以至于有专门的名字: 字节(byte) ❗\n1 字节 = 8 位 1 bytes = 8 bits 如果有 10 个字节,意味着有 80 位。你听过 千字节(kb)兆字节(mb)千兆字节(gb)等等。不同前缀代表不同数量级,就像 1 千克 = 1000 克,1 千字节 = 1000 字节,或 8000 位。mega 是百万字节(mb), giga 是十亿字节(gb)。如今你可能有 1 tb 的硬盘,8 万亿个 1 和 0。等等,我们有另一种计算方法 - 二进制里,1 千字节 = 2 的 10 次方 = 1024 字节。1000 也是千字节(kb)的正确单位,1000 和 1024 都对。\n你可能听过 32 位 或 64 位计算机,你现在用的电脑几乎肯定是其中一种,意思是 一块块处理数据,每块是 32 位或 64 位,这可是很多位。32 位能表示的最大数,是 43 亿左右 - 也就是 32 个 1,所以 instagram 照片很清晰,它们有上百万种颜色,因为如今都用 32 位颜色。当然,不是所有数都是正数,比如我上大学时的银行账户 t_t ……\n= cpu 一次性读取的位数。\n数的表示 我们需要有方法表示正数(positive)和负数(negative),大部分计算机用第一位表示正负 :1 是负,0 是正。用剩下 31 位来表示符号外的数值,能表示的数的范围大约是正 20 亿到负 20 亿。虽然是很大的数,但许多情况下还不够用,全球有 70 亿人口,美国国债近 20 万亿美元。所以 64 位数很有用,64 位能表达最大数大约是 9.2 × 10 ^ 18,希望美国国债在一段时间内不会超过这个数!\n重要的是(我们之后的视频会深入讲),计算机必须给内存(memory)中每一个位置,做一个 \u0026ldquo;标记\u0026rdquo;,这个标记叫 \u0026ldquo;地址\u0026rdquo;(addresses)(也叫‘位址’), 目的是为了方便存取(store and retrieve)数据。如今硬盘已经增长到 gb 和 tb,上万亿个字节!内存地址也应该有 64 位。\n除了负数和正数,计算机也要处理非整数(not whole numbers),比如 12.7 和 3.14,或\u0026quot;星历 43989.1\u0026rdquo; - 这叫 浮点数(floating point numbers)。因为小数点可以在数字间浮动,有好几种方法 表示浮点数,最常见的是 ieee 754 标准,你以为只有历史学家取名很烂吗?它用类似科学计数法的方法,来存十进制值,例如,625.9 可以写成 0.6259 × 10 ^ 3 ,这里有两个重要的数:.6259 叫 \u0026ldquo;有效位数\u0026rdquo;(significand) , 3 是指数(exponent)。\n\u0026gt; 计算机中浮点数的表示方法\n在 32 位浮点数中,第 1 位表示数的符号 —— 正或负,接下来 8 位存指数,剩下 23 位存有效位数。\n= 以上为正、负 \u0026amp; 整数、浮点数在计算机中的表示,‘数’的表示任务已达成 🎉\n字符的表示 好了,聊够数了,但你的名字是字母(letters)组成的 !所以我们也要表示文字(text),与其用特殊方式来表示字母,计算机可以用数表示字母 - 最直接的方法是给字母编号:a 是 1,b 是 2,c 是 3,以此类推。\n\u0026gt; 人才啊·培根\n著名英国作家 弗朗西斯·培根(francis bacon),曾用 5 位序列 来编码英文的 26 个字母。在十六世纪传递机密信件,五位(bit)可以存 32 个可能值(2^5) - 这对 26 个字母够了,但不能表示 标点符号,数字和大小写字母。\n= 在内存中,字符也是用数字表示的,本质上是一张数字与字符一一对应的编码表!\nascii,美国信息交换标准代码(the american standard code for information interchange),发明于 1963 年,ascii 是 7 位代码,足够存 128 个不同值。范围扩大之后,可以表示大写字母,小写字母,数字 0 到 9, @ 这样的符号,以及标点符号(punctuation marks)。举例,小写字母 a 用数字 97 表示,大写字母 a 是 65,: (colon)是 58, ) 是 41。\na a : ) 97 65 58 41 ascii 甚至有特殊命令符号,比如换行符(newline),用来告诉计算机换行。在老计算机系统中,如果没换行符,文字会超出屏幕。\n因为 ascii 是个很早的标准,所以它被广泛使用,让不同公司制作的计算机,能互相交换数据,这种通用交换信息的能力叫 \u0026ldquo;互操作性\u0026rdquo;。但有个限制:它是为英语设计的。\n幸运的是,一个字节有 8 位,而不是 7 位,128 到 255 的字符渐渐变得常用,这些字符以前是空的,是给各个国家自己 \u0026ldquo;保留使用的\u0026rdquo; 。在美国,这些额外的数字主要用于编码附加符号,比如数学符号,图形元素和常用的重音字符。另一方面,虽然拉丁字符被普遍使用,在俄罗斯,他们用这些额外的字符表示西里尔字符,而希腊电脑用希腊字母,等等。这些保留下来给每个国家自己安排的空位,对大部分国家都够用,问题是,如果在土耳其电脑上打开拉脱维亚语 写的电子邮件,会显示乱码。\n\u0026gt; 8 位远远不够啊。..\n随着计算机在亚洲兴起,这种做法彻底失效了,中文和日文这样的语言有数千个字符,根本没办法用 8 位来表示所有字符!为了解决这个问题,每个国家都发明了多字节编码方案,但相互不兼容,日本人总是碰到编码问题,以至于专门有词来称呼:\u0026ldquo;mojibake\u0026rdquo; 意思是 乱码。\n所以 unicode 诞生了 - 统一所有编码的标准。设计于 1992 年,解决了不同国家不同标准的问题,unicode 用一个统一编码方案,最常见的 unicode 是 16 位的,有超过一百万个位置,对所有语言的每个字符都够了,100 多种字母表加起来占了 12 万个位置,还有位置放数学符号,甚至 emoji 。\n就像 ascii 用二进制来表示字母一样,其他格式 - 比如 mp3 或 gif,用二进制编码声音/颜色,表示照片,电影,音乐。重要的是,这些标准归根到底是一长串位 。短信,这个 youtube 视频,互联网上的每个网页,甚至操作系统,只不过是一长串 1 和 0 。\n下周,我们会聊计算机怎么操作二进制,初尝\u0026quot;计算\u0026quot;的滋味。\n感谢观看,下周见。\n----------------\n你可以在我的另一篇博文中了解更多关于字符编码的知识 \u0026ndash; 《字符集和字符编码》 。\n","date":"2022-08-25","permalink":"https://loveminimal.github.io/posts/cscc/04/","summary":"\u003cp\u003ei.e. Representing Numbers and Letters with Binary\u003c/p\u003e\n\u003cp\u003e今天,我们讲计算机如何存储和表示数字,所以会有一些数学,不过别担心,你们的数学水平绝对够用了。\u003c/p\u003e\n\u003cp\u003e上集我们讲了,怎么用晶体管(transistors)做逻辑门(logic gates),逻辑门可以判断布尔语句,布尔代数只有两个值:\u003ccode\u003eTrue\u003c/code\u003e 和 \u003ccode\u003eFalse\u003c/code\u003e 。\u003c/p\u003e\n\u003cp\u003e但如果只有两个值,我们怎么表达更多东西 ❓ 这就需要数学了!\u003c/p\u003e","title":"cscc04 - 二进制"},{"content":"i.e. boolean-logic-and-logic-gates\n今天我们开始\u0026quot;抽象\u0026quot;(abstraction)的旅程!不用管底层细节,把精力用来构建更复杂的系统。\n上集,我们谈了计算机最早是机电设备(electromechanical),一般用十进制(decimal)计数,比如用齿轮数来代表十进制, 再到晶体管计算机。\n幸运的是,只用 ‘开/关’ 两种状态也可以代表信息,这叫二进制,意思是\u0026quot;用两种状态表示\u0026quot;,就像自行车有两个轮,双足动物有两条腿。你可能觉得两种状态不多,你是对的!但如果只需要表示 true 和 false,两个值就够了。电路闭合,电流流过,代表 \u0026ldquo;真\u0026rdquo;;电路断开,无电流流过,代表\u0026quot;假\u0026quot;。二进制也可以写成 1 和 0 而不是 true 和 false,只是不同的表达方式罢了。\n我们下集(episode)会讲更多细节。\n晶体管的确可以不只是开/关,还可以让不同大小的电流通过。一些早期电子计算机是三进制的,有 3 种状态,甚至五进制,5 种状态。问题是,状态越多,越难区分信号。如果手机快没电了或者附近有电噪音,因为有人在用微波炉(microwave),信号可能会混在一起。.. 而每秒百万次变化的晶体管会让这个问题变得更糟!所以我们把两种信号尽可能分开,只用\u0026quot;开\u0026quot;和\u0026quot;关\u0026quot;两种状态,可以尽可能减少这类问题。\n\u0026gt; on \u0026amp; off\n计算机用二进制的另一个原因是,有一整个数学分支存在,专门处理\u0026quot;真\u0026quot;和\u0026quot;假\u0026quot;。它已经解决了所有法则和运算 - 叫\u0026quot;布尔代数\u0026quot;(boolean algebra)!\n乔治·布尔(george boole)是布尔二字的由来,是一位 19 世纪自学成才的英国数学家,他有兴趣用数学式子扩展亚里士多德基于哲学的逻辑方法,布尔用逻辑方程系统而正式的证明真理 (truth)。他在 1847 年的第一本书\u0026quot;逻辑的数学分析\u0026quot;中介绍过,在\u0026quot;常规\u0026quot;代数里 - 你在高中学的那种 - 变量的值是数字,可以进行加法或乘法之类的操作,但在布尔代数中,变量的值是 true 和 false,能进行逻辑操作。\n布尔逻辑 布尔代数中有三个基本操作:not, and 和 or 。这些操作非常有用,我们一个个来看:\n= 布尔逻辑的本质是什么呢?很简单,一个或若干个输入(真或假),经过若干次逻辑运算,最终得到一个确定的输出(真或假)的过程!\nnot not 操作把布尔值反转,把 true 进行 not 就会变成 false,反之亦然。我们可以根据 not 操作的输入和输出,做出这个表。\ninput output ture false false true \u0026gt; not 真值表\n酷的地方是 - 用晶体管可以轻松实现这个逻辑。\n= 晶体管的用武之地~~\n上集说过,晶体管只是电控制的开关 - 有 3 根线:2 根电极和 1 根控制线。\n控制线通电时,电流就可以从一个电极流到另一个电极,就像水龙头一样 - 打开水龙头,就有水流出来,关掉水龙头,就没水了。可以把控制线当做输入 ( input ),底部的电极当做输出(output),所以 1 个晶体管,有一个输入和一个输出。如果我们打开输入(input on) ,输出也会打开(output on),因为电流可以流过。如果关闭输入(input off),输出也会关闭(output off),因为电流无法通过。或者用布尔术语来说,输入为真,输出为真,输入为假,输出为假,我们也可以把这个做成\u0026quot;真值表\u0026quot;。\n这个电路没什么意思,因为它没做什么事 - 输入和输出是一样的。\n但我们可以稍加修改,实现 not 。\n与其把下面那根线当做 输出,我们可以把 输出 放到上面。如果打开 输入,电流可以流过然后 \u0026ldquo;接地\u0026rdquo;,输出就没有电流,所以输出是 off 。如果用水来举例,就像家里的水都从一个大管子流走了,打开淋浴头一点水也没有。\n如果输入是 on,输出是 off 。当输入是 off,电流没法接地,就流过了输出,所以输出是 on ;当输入是 off,电流没法接地,就流过了输出,所以输出是 on 。\n如果输入是 off,输出是 on 。\n和 not 操作表一样!太棒了!我们做了个有点用的电路!🎉\n我们叫它 \u0026ldquo;not 门\u0026rdquo;,之所以叫 \u0026ldquo;门\u0026rdquo;,是因为它能控制电流的路径 。\nand \u0026ldquo;and\u0026quot;操作有 2 个输入,1 个输出。如果 2 个输入都是 true,输出才是 true 。和上次一样,可以给\u0026quot;and\u0026quot;做个表 。\ninput a input b output true true true true false false false true false false false false \u0026gt; and 真值表\n为了实现 \u0026ldquo;and 门\u0026rdquo;,我们需要 2 个晶体管连在一起,这样有 2 个输入和 1 个输出。如果只打开 a,不打开 b ,电流无法流到 output,所以输出是 false ;如果只打开 b,不打开 a ,也一样,电流无法流到 output ;只有 a 和 b 都打开了,output 才有电流 。\nor 最后一个是 or (前面讲了 not 和 and)。\ninput a input b output true true true true false true false true true false false false \u0026gt; or 真值表\n只要 2 个输入里,其中 1 个是 true,输出就是 true ,只有 2 个输入都是 false,or 的结果才是 false 。\n实现 \u0026ldquo;or 门\u0026rdquo; 除了晶体管还要额外的线,不是串联起来,而是并联。然后左边这条线有电流输入,我们用\u0026quot;小拱门\u0026quot;代表 2 条线没连在一起,只是跨过而已,虽然看起来像连在一起。\n如果 a 和 b 都是 off,电流无法流过,所以输出是 off ;如果只打开 a,电流可以流过,输出是 on ;如果只打开 b 也一样;只要 a or b 是 on, 输出就是 on 。\n好,现在 not 门,and 门,or 门 都搞定了。\n我们可以进行一次抽象!\nnot 门的画法是三角形前面一个圆点,and 门用 d 表示 ,or 门用太空船表示 。\u0026ldquo;d 形状和太空船\u0026quot;不是标准叫法,只是我喜欢这样叫而已。我们可以用这种方法表示它们,构建更大的组件,就不会变得很复杂。\nxor 晶体管和电线依然在那里,我们只是用符号来代表而已。除了前面说的三个、n 另一个有用的布尔操作叫 \u0026ldquo;异或\u0026rdquo;,简称 xor 。\ninput a input b output true true false true false true false true true false false false \u0026gt; xor 真值表\nxor 就像普通 or,但有一个区别:如果 2 个输入都是 true,xor 输出 false 。想要 xor 输出 true ,一个输入必须是 true,另一个必须是 false 。\n就像你出去吃晚饭,你点的饭要么配沙拉,要么配汤 - 你不能两个都要!\n用晶体管实现 xor 门有点烧脑子,但我可以展示一下,怎么用前面提到的 3 种门来做 xor 门。\n\u0026gt; xor 的组成\n= 组合的威力!\n我们有 2 个输入,a 和 b ,还有 1 个输出。我们先放一个 or 门,因为 or 和 xor 的逻辑表很像。只有 1 个问题 - 当 a 和 b 都是 true 时 , or 的输出和想要的 xor 输出不一样。我们想要 false,xor 超有用的,我们下次再说它。因为超有用,工程师给了它一个符号,一个 or 门 + 一个笑脸。\n重要的是,现在可以把 xor 放入\u0026quot;工具箱\u0026quot;了。不用担心 xor 具体用了几个门,这几个门又是怎么用晶体管拼的,或电子是怎么流过半导体的。\n再次向上抽象。\n工程师设计处理器(processors)时,很少在晶体管的层面上思考,而是用更大的组件,比如逻辑门,或者由逻辑门组成的更大组件。我们以后会讲,就算是专业程序员,也不用考虑逻辑是怎样在物理层面实现的。\n我们从电信号开始,到现在第一次表示数据 - 真和假 - 开始有点\u0026quot;计算\u0026quot;的感觉了。\n仅用这集讲的 逻辑门,我们可以判断复杂的语句 比如:[如果是 john green] and [下午 5 点后] or [周末] and [在比萨店附近],那么 \u0026quot;john 想要比萨\u0026quot; = 真 。\n我都说饿了,下周见。\n","date":"2022-08-17","permalink":"https://loveminimal.github.io/posts/cscc/03/","summary":"\u003cp\u003ei.e. boolean-logic-and-logic-gates\u003c/p\u003e\n\u003cp\u003e今天我们开始\u0026quot;抽象\u0026quot;(abstraction)的旅程!不用管底层细节,把精力用来构建更复杂的系统。\u003c/p\u003e\n\u003cp\u003e上集,我们谈了计算机最早是机电设备(electromechanical),一般用十进制(decimal)计数,比如用齿轮数来代表十进制, 再到晶体管计算机。\u003c/p\u003e\n\u003cp\u003e幸运的是,\u003cstrong\u003e只用 \u003ccode\u003e‘开/关’\u003c/code\u003e 两种状态也可以代表信息\u003c/strong\u003e,这叫\u003ccode\u003e二进制\u003c/code\u003e,意思是\u0026quot;用两种状态表示\u0026quot;,就像自行车有两个轮,双足动物有两条腿。你可能觉得两种状态不多,你是对的!但如果只需要表示 \u003ccode\u003etrue\u003c/code\u003e 和 \u003ccode\u003efalse\u003c/code\u003e,两个值就够了。电路闭合,电流流过,代表 \u0026ldquo;真\u0026rdquo;;电路断开,无电流流过,代表\u0026quot;假\u0026quot;。二进制也可以写成 \u003ccode\u003e1\u003c/code\u003e 和 \u003ccode\u003e0\u003c/code\u003e 而不是 \u003ccode\u003etrue\u003c/code\u003e 和 \u003ccode\u003efalse\u003c/code\u003e,只是不同的表达方式罢了。\u003c/p\u003e","title":"cscc03 - 布尔逻辑和逻辑门"},{"content":"i.e. electronic computing\n上集讲到 20 世纪初,当时的早期计算设备都针对特定用途,比如制表机(tabulating machines),大大推进了政府和企业。它们帮助,甚至代替了人工。然而人类社会的规模在以前所未有的速度增长,20 世纪上半叶,世界人口几乎翻倍。一战动员 7 千万人,二战 1 亿多人。全球贸易和运输更加紧密,工程和科学的复杂度也达到新高。我们甚至开始考虑造访其他行星,复杂度的增高导致数据量暴增,人们需要更多自动化,更强的计算能力。\n很快,柜子大小的计算机变成房间大小,维护费用高,而且容易出错,而正是这些机器为未来的创新打下基础。\n继电器 最大的机电计算机之一是哈佛马克一号,ibm 在 1944 完成建造,给二战同盟国建造的。它有 76 万 5 千个组件,300 万个连接点和 500 英里长的导线。为了保持内部机械装置同步,它有一个 50 英尺的传动轴,由一个 5 马力的电机驱动,这台机器最早的用途之一 是给\u0026quot;曼哈顿计划\u0026quot;跑模拟。\n这台机器的大脑是 \u0026ldquo;继电器\u0026rdquo;(relays),继电器是:用电控制的机械开关。继电器里,有根\u0026quot;控制线路\u0026quot;,控制电路是开还是关。\u0026ldquo;控制线路\u0026quot;连着一个线圈,当电流流过线圈,线圈产生电磁场,吸引金属臂,从而闭合电路。你可以把继电器想成水龙头,把控制线路想成水龙头把,打开水龙头,水会流出来,关闭水龙头,水就没有了。继电器是一样的,只不过控制的是电子而不是水。这个控制电路可以连到其他电路,比如马达,马达让计数齿轮 +1,就像上集中 hollerith 的制表机一样。\n= 电磁原理…… 另外,看,最初的开始就是为了 ‘控制连通’ ,区分开关,划分阴阳。\n不幸的是,继电器内的机械臂有质量,因此无法快速开关。\n1940 年代一个好的继电器 1 秒能翻转 50 次,看起来好像很快,但还不够快,不足以解决复杂的大问题。哈佛马克一号,1 秒能做 3 次加法或减法运算,一次乘法要花 6 秒,除法要花 15 秒。更复杂的操作,比如三角函数,可能要一分钟以上。\n除了速度慢,另一个限制是齿轮磨损。\n任何会动的机械都会随时间磨损,有些部件会完全损坏,有些则是变黏,变慢,变得不可靠,并且随着继电器数量增加,故障概率也会增加。哈佛马克一号有大约 3500 个继电器,哪怕假设继电器的使用寿命是 10 年,也意味着平均每天得换一个故障继电器!这个问题很严重,因为有些重要运算要运行好几天,而且还有更多其他问题要考虑。\n这些巨大,黑色,温暖的机器也会吸引昆虫 🦟。\n\u0026gt; ha, bug...\n1947 年 9 月,哈佛马克 2 型的操作员从故障继电器中,拔出一只死虫。grace hopper(这位我们以后还会提到)曾说,\u0026ldquo;从那时起,每当电脑出了问题,我们就说它出了 bug(虫子)\u0026quot;,这就是术语 \u0026ldquo;bug\u0026rdquo; 的来源。\n显然,如果想进一步提高计算能力,我们需要更快更可靠的东西,来替代继电器。幸运的是,替代品已经存在了!\n真空管 在 1904 年,英国物理学家 \u0026ldquo;约翰·安布罗斯·弗莱明\u0026rdquo;,开发了一种新的电子组件,叫 \u0026ldquo;热电子管\u0026rdquo;。把两个电极(electrodes)装在一个气密的玻璃灯泡里,这是世上第一个真空管(vacuum tube)。其中一个电极可以加热,从而发射电子(electrons),这叫 \u0026ldquo;热电子发射\u0026rdquo;。另一个电极会吸引电子,形成\u0026quot;电龙头\u0026quot;的电流,但只有带正电才行。如果带负电荷或中性电荷,电子就没办法被吸引,越过真空区域,因此没有电流。\n电流只能单向流动的电子部件叫 \u0026ldquo;二极管\u0026rdquo;(diode),但我们需要的是,一个能开关电流的东西。\n= ⚡️ 控制联通!控制联通!控制联通!\n幸运的是,不久之后在 1906 年,美国发明家 \u0026ldquo;李·德富雷斯特\u0026rdquo;,他在\u0026quot;弗莱明\u0026quot;设计的两个电极之间,加入了第三个 \u0026ldquo;控制\u0026rdquo; 电极,向\u0026quot;控制\u0026quot;电极施加正电荷,它会允许电子流动,但如果施加负电荷,它会阻止电子流动。因此通过控制线路,可以断开或闭合电路,和继电器的功能一样。\n\u0026gt; 真空管 但重要的是,真空管内没有会动的组件,这意味着更少的磨损。更重要的是,每秒可以开闭数千次。因此这些 \u0026ldquo;三极真空管\u0026rdquo;(triode vacuum tubes)成为了无线电,长途电话以及其他电子设备的基础,持续了接近半个世纪。我应该提到,真空管不是完美的,它们有点脆弱,并且像灯泡一样会烧坏,但比起机械继电器是一次巨大进步。\n起初,真空管非常昂贵,收音机一般只用一个,但计算机可能要上百甚至上千个电气开关。但到了 1940 年代,它的成本和可靠性得到改进,可以用在计算机里,至少有钱人负担得起,比如政府。\n这标志着计算机从机电转向电子。\n💭 我们来进入思想泡泡\n第一个大规模使用真空管的计算机是 \u0026ldquo;巨人 1 号\u0026rdquo;,由工程师 tommy flowers 设计,完工于 1943 年 12 月。巨人 1 号 在英国的\u0026quot;布莱切利园\u0026rdquo;, 用于破解纳粹通信,听起来可能有点熟,因为 2 年前 阿兰·图灵(他经常被称为\u0026quot;计算机科学之父\u0026rdquo;)也在\u0026quot;布莱切利园\u0026quot;做了台机电装置,叫 \u0026ldquo;bombe\u0026rdquo;,这台机器的设计目的是破解纳粹\u0026quot;英格码\u0026quot;通讯加密设备,但 bombe 严格来说不算计算机。我们之后会讨论\u0026quot;阿兰·图灵\u0026quot;的贡献。总之,巨人 1 号有 1600 个真空管,总共造了 10 台巨人计算机,来帮助破解密码。巨人被认为是第一个可编程的电子计算机,编程的方法是把几百根电线插入插板(plugboards),有点像老电话交换机,这是为了让计算机执行正确操作。虽然\u0026quot;可编程\u0026quot;(programmable) ,但还是要配置它。\n\u0026gt; eniac - 世上第一个真正的通用、可编程的电子计算机\n电子数值积分计算机 \u0026ldquo;eniac\u0026quot;几年后在 1946 年,在\u0026quot;宾夕法尼亚大学\u0026quot;完成建造,设计者是 john mauchly 和 j. presper eckert,这是世上第一个真正的通用,可编程,电子计算机。eniac 每秒可执行 5000 次十位数加减法,比前辈快了很多倍,它运作了十年。据估计,它完成的运算,比全人类加起来还多。因为真空管很多,所以故障很常见,eniac 运行半天左右就会出一次故障。\n谢了 思想泡泡\n到 1950 年代,真空管计算机都达到了极限。美国空军的 an/fsq-7 计算机于 1955 年完成,是 \u0026ldquo;sage\u0026rdquo; 防空计算机系统的一部分,之后的视频还会提到。\n为了降低成本和大小,同时提高可靠性和速度,我们需要一种新的电子开关。\n晶体管 1947 年,贝尔实验室科学家 john bardeen,walter brattain,william shockley,发明了 晶体管(transistor),一个全新的计算机时代诞生了!\n晶体管的物理学相当复杂,牵扯到量子力学(quantum mechanics),所以我们只讲基础。晶体管就像之前提过的\u0026quot;继电器\u0026quot;或\u0026quot;真空管\u0026rdquo;,它是一个开关,可以用控制线路来控制开或关。晶体管有两个电极,电极之间有一种材料隔开它们,这种材料有时候导电,有时候不导电,这叫\u0026quot;半导体\u0026quot;(semiconductor)。控制线连到一个 \u0026ldquo;门\u0026rdquo;(gate)电极,通过改变 \u0026ldquo;门\u0026rdquo; 的电荷。我们可以控制半导体材料的导电性,来允许或不允许电流流动,就像之前的水龙头比喻。\n\u0026gt; 第一个晶体管\n贝尔实验室(bell labs)的第一个晶体管就展示了巨大的潜力,每秒可以开关 10,000 次,而且,比起玻璃制成小心易碎的真空管,晶体管是固态的。晶体管可以远远小于继电器或真空管,导致更小更便宜的计算机,比如 1957 年发布的 ibm 608 - 第一个完全用晶体管,而且消费者也可以买到的计算机,它有 3000 个晶体管,每秒执行 4500 次加法,每秒能执行 80 次左右的乘除法。ibm 很快把所有产品都转向了晶体管,把晶体管计算机带入办公室,最终引入家庭,如今,计算机里的晶体管小于 50 纳米,而一张纸的厚度大概是 10 万纳米。晶体管不仅小,还超级快 - 每秒可以切换上百万次,并且能工作几十年。\n很多晶体管和半导体的开发在\u0026quot;圣克拉拉谷\u0026quot;,这个地方在加州,位于\u0026quot;旧金山\u0026quot;和\u0026quot;圣荷西\u0026quot;之间,而生产半导体最常见的材料是 \u0026ldquo;硅\u0026rdquo;,所以这个地区被称为 \u0026ldquo;硅谷\u0026rdquo;。甚至 william shockley 都搬了过去,创立了\u0026quot;肖克利半导体\u0026quot;,里面的员工后来成立了\u0026quot;仙童半导体\u0026quot;,这里面的员工后来创立了英特尔 - 当今世界上最大的计算机芯片制造商。\n好了,我们从\u0026quot;继电器\u0026quot;到\u0026quot;真空管\u0026quot;到\u0026quot;晶体管\u0026quot;,我们可以让电路开闭得非常非常快,但我们是如何用晶体管做计算的?我们没有马达和齿轮啊?\n= 开关的过程,就是阴阳转换的过程,信息变化的过程!\n我们接下来几集会讲,感谢观看,下周见。\n","date":"2022-08-15","permalink":"https://loveminimal.github.io/posts/cscc/02/","summary":"\u003cp\u003ei.e. Electronic Computing\u003c/p\u003e\n\u003cp\u003e上集讲到 20 世纪初,当时的早期计算设备都针对特定用途,比如制表机(tabulating machines),大大推进了政府和企业。它们帮助,甚至代替了人工。然而人类社会的规模在以前所未有的速度增长,20 世纪上半叶,世界人口几乎翻倍。一战动员 7 千万人,二战 1 亿多人。全球贸易和运输更加紧密,工程和科学的复杂度也达到新高。我们甚至开始考虑造访其他行星,复杂度的增高导致数据量暴增,人们需要更多自动化,更强的计算能力。\u003c/p\u003e\n\u003cimg alt=\"picture 1\" src=\"/posts/cscc/02/imgs/dde01eb023647fbef243f8ce436363ef4ee215027d014314260ee97fa00187fb.png\" width=\"400\" /\u003e \r\n\u003cp\u003e很快,柜子大小的计算机变成房间大小,维护费用高,而且容易出错,而正是这些机器为未来的创新打下基础。\u003c/p\u003e","title":"cscc02 - 电子计算机"},{"content":"i.e. early computing\nhello world!我是 carrie anne,欢迎收看计算机科学速成课(crash course computer science)!\n\u0026gt; carrie anne 小姐姐\n在这个系列中,我们会学习 bits(位),bytes(字节),晶体管(transistors),逻辑门(logic gates),一直到操作系统,虚拟现实和机器人!我们要学很多东西,但预先说明,我们 不会 教你怎么编程,我们会从高层次上纵览一系列计算机话题。\n计算机是当今世界的命脉,如果突然关掉所有的计算机,电网会关闭,车辆会相撞,飞机会坠毁,净水厂会关闭,证券市场会停止运作,装满食物的卡车不知运往何方,员工得不到薪水,甚至很多和计算机无关的东西,例如 dftba 的 t 恤和我现在坐的椅子也都是在计算机管理的工厂中制造的。\n计算机改变了我们生活中几乎所有方面。\n我们也不是第一次遇到推动全球发展的科技了。\n工业革命(industrial revolution)中生产能力的提高,大幅提升了农业,工业,畜牧业的规模。机械化导致更好的收成,更多的食物,商品可以大批量生产。旅行和通讯变得更便宜更快,生活质量变得更好。\n\u0026gt; 蒸气车 计算机和工业革命有一样的影响。\n从自动化农业和医疗设备到全球通信和教育机会,还有虚拟现实和无人驾驶汽车等新领域,现在这个时代很可能会被后人总结成 \u0026ldquo;信息时代\u0026rdquo;。\n你的智能手机中有数十亿个晶体管,看起来好像很复杂,但实际上它是很简单的机器,通过一层层的 抽象(abstraction) 来做出复杂操作。\n在这个系列中,我们会一层层讲解,从最底层的 1 和 0,到逻辑门,cpu,操作系统,整个互联网,以及更多~~\n不用担心,正如在网上买 t 恤的人不用知道网站代码是怎么写的,设计师不用知道数据包(packets)是怎么传输的,设计路由器的工程师不用理解晶体管的逻辑。\n本系列中每个视频会接着上集继续讲,但并不依赖前面的视频。等这个系列结束后,希望你能了解计算机在你的人生以及社会中扮演什么角色,以及这个人类史上最伟大的发明(可以这样说啦)是怎么开始的。\n它对未来还会有更大的影响。\n但深入之前,我们应该从计算的起源讲起,虽然电子计算机才出现不久,但人类对计算的需求早就有了。\n公认最早的计算设备是算盘(abacus),发明于\u0026quot;美索不达米亚\u0026quot;,大约公元前 2500 年。它是手动计算器,用来帮助加减数字,它存储着当前的计算状态,类似于如今的硬盘。人们制造算盘,是因为社会的规模已经超出个人心算的能力。一个村庄可能有上千个人和上万头牛。\n= “公认”?算盘起源,此处存疑,待求证。\n算盘有很多变种,但我们来看一个基础版,每行代表 10 的不同次方。最底下那行,一个珠子代表 10 的 0 次方,也就是 1,再上面一行是 10 的 1 次方(也就是 10),再上面一行是 10 的 2 次方 (以此类推)……\n假设最底部的 3 颗珠子,代表 3 头牛。假设再买 4 头牛,只需要向右移动 4 颗珠子,共 7 个珠子。但如果再买 5 头,珠子就不够用了,所以把所有珠子移回左边。在第二排把 1 颗珠子向右移动,代表 10,然后最底下那行,向右移动 2 颗珠子,代表 12。\n这种方法处理大数字很有效。假设要表示 1251,从下往上:第一行移 1 个,第二行移 5 个,第三行移 2 个,第四行移 1 个。\n\u0026gt; 计量‘牛牛’\n我们不用记在脑子里,算盘会记住。\n在接下来 4000 年,人类发明了各种巧妙的计算设备。比如星盘,让船只可以在海上计算纬度,或计算尺,帮助计算乘法和除法。人们还创造了上百种时钟,算日出,潮汐,天体的位置,或纯粹拿来计时。这些设备让原先很费力的事变得更快,更简单,更精确,降低了门槛,加强了我们的能力。\n记笔记!(敲黑板)这个系列会多次提到这一点。\n计算机先驱 charles babbage 说过:\u0026ldquo;随着知识的增长和新工具的诞生,人工劳力会越来越少\u0026rdquo;。\n然而,这些设备那时都不叫\u0026quot;计算机\u0026quot;。最早使用 \u0026ldquo;计算机\u0026rdquo; 一词的文献来自 1613 年的一本书,作者 richard braithwait。然而指的不是机器,而是一种职业。\n\u0026gt; hi, computers.\nbraithwait 说:\u0026ldquo;我听说过的计算者里最厉害的,能把好几天的工作量大大缩减。\u0026rdquo;\n那时,\u0026ldquo;computer\u0026rdquo; 指负责计算的人。\u0026ldquo;computer\u0026rdquo; 偶尔会用机器帮忙,但大部分时候靠自己。这个职位一直到 1800 年代还存在,之后 \u0026ldquo;computer\u0026rdquo; 逐渐开始代表机器。\n\u0026gt; 步进计算器\n其中\u0026quot;步进计算器\u0026quot;(step reckoner)最有名,由德国博学家戈特弗里德·莱布尼茨建造于 1694 年。\n= 对,就是和牛顿先后发明微积分的那个莱布尼茨……\n莱布尼茨说过 \u0026ldquo;\u0026hellip; 让优秀的人浪费时间算数简直侮辱尊严,农民用机器能算得一样准。\u0026rdquo;\n\u0026ldquo;步进计算器\u0026quot;有点像汽车里的里程表,不断累加里程数。它有一连串可以转动的齿轮(gears),每当一个齿轮转过 9,它会转回 0,同时让旁边的齿轮前进 1 个齿,就像算盘超过 10 一样。做减法时,机器会反向运作。利用一些巧妙的机械结构,步进计算器也能做乘法和除法。\n乘法和除法实际上只是多个加法和减法。举例,17 除以 5,我们只要减 5,减 5,再减 5,直到不能再减 5,就知道了 17=5x3+2 。步进计算器可以自动完成这种操作,它是第一台能做\u0026quot;加减乘除\u0026quot;全部四种运算的机器。它的设计非常成功,以至于沿用了 3 个世纪。\n不幸的是,即使有机械计算器,许多现实问题依然需要很多步,算一个结果可能要几小时甚至几天,而且这些手工制作的机器非常昂贵,大部分人买不起。所以在 20 世纪以前,大部分人会用预先算好的计算表,这些计算表由之前说的 \u0026ldquo;人力计算器\u0026rdquo; 编撰。如果你想知道 867,5309 的平方根,与其花一整天来手摇 \u0026ldquo;步进计算器\u0026rdquo;,你可以花一分钟在表里找答案。\n速度和准确性(speed and accuracy)在战场上尤为重要,因此军队很早就开始用计算解决复杂问题。如何精确瞄准炮弹是一个很难的问题。19 世纪,这些炮弹的射程可以达到 1 公里以上(比半英里多一点),因为风力,温度,大气压力会不断变化,想打中船一样大的物体也非常困难。于是出现了射程表(range tables),炮手可以查环境条件和射击距离,然后这张表会告诉他们,角度要设成多少。这些射程表很管用,二战中被广泛应用。问题是如果改了大炮或炮弹的设计,就要算一张新表,这样很耗时而且会出错。\ncharles babbage 在 1822 年写了一篇论文,向皇家天文学会指出了这个问题,标题叫: \u0026ldquo;机械在天文与计算表中的应用\u0026rdquo;(\u0026ldquo;note on the application of machinery to the computation of astronomical and mathematical tables\u0026rdquo;)。\n💭 让我们进入思想泡泡。\ncharles babbage 提出了一种新型机械装置叫 \u0026ldquo;差分机\u0026rdquo;(the difference engine),一个更复杂的机器,能近似多项式(polynomials)。多项式描述了几个变量之间的关系,比如射程和大气压力,或者 carrie anne 要吃多少披萨才开心。多项式也可以用于近似对数(logarithmic)和三角函数(trigonometric functions),这些函数手算相当麻烦。\n\u0026gt; ‘分析机’进行时...\ncharles babbage 在 1823 年开始建造差分机,并在接下来二十年,试图制造和组装 25,000 个零件,总重接近 15 吨。不幸的是,该项目最终放弃了,但在 1991 年,历史学家根据 charles babbage 的草稿做了一个差分机,而且它还管用!但更重要的是,在差分机的建造期间,charles babbage 构想了一个更复杂的机器 - 分析机。不像差分机,步进计算器 和以前的其他计算设备,分析机是 \u0026ldquo;通用计算机\u0026rdquo;(\u0026ldquo;general purpose computer\u0026rdquo;)。它可以做很多事情,不只是一种特定运算,甚至可以给它数据,然后按顺序执行一系列操作。它有内存,甚至一个很原始的打印机。就像差分机,这台机器太超前了,所以没有建成,然而,这种 \u0026ldquo;自动计算机\u0026rdquo;(\u0026ldquo;automatic computer\u0026rdquo;)的概念。\n计算机可以自动完成一系列操作,是个跨时代的概念,预示着计算机程序的诞生。\n英国数学家 ada lovelace 给分析机写了假想的程序,她说:\u0026ldquo;未来会诞生一门全新的,强大的,专为分析所用的语言。\u0026rdquo; 因此 ada 被认为是世上第一位程序员(programmer)。\n分析机激励了(可以这么讲)第一代计算机科学家,这些计算机科学家把很多 charles babbage 的点子融入到他们的机器。所以 charles babbage 经常被认为是 \u0026ldquo;计算之父\u0026rdquo;。\n谢啦!思想泡泡\n到了 19 世纪末,科学和工程领域中的特定任务会用上计算设备。但公司,政府,家庭中很少见到计算设备。然而,美国政府在 1890 年的人口普查中面临着严重的问题,只有计算机能提供所需的效率。美国宪法要求 10 年进行一次人口普查,目的是分配联邦资金,国会代表,等等。到 1880 年代,美国人口迅速增长,大部分因为移民,人口普查要七年时间来手工编制,等做完都过时了,而且 1890 年的人口普查,预计要 13 年完成,但人口普查可是 10 年一次啊!\n人口普查局找了 herman hollerith,他发明了打孔卡片制表机(tabulating machine),他的机器是 \u0026ldquo;电动机械的\u0026rdquo;,用传统机械来计数,结构类似莱布尼茨的乘法器,但用电动结构连接其他组件。hollerith 的机器用打孔卡(punch cards) - 一种纸卡,上面有网格,用打孔来表示数据。\n\u0026gt; herman hollerith 和 ‘打孔卡片制表机’\n举个例子,有一连串孔代表婚姻状况。如果你结婚了,就在 \u0026ldquo;结婚\u0026rdquo; 的位置打孔。当卡插入 hollerith 的机器时,小金属针会到卡片上。如果有个地方打孔了,针会穿过孔。泡入一小瓶汞,联通电路,电路会驱动电机,然后给 \u0026ldquo;已婚\u0026rdquo; 的齿轮 + 1 。\n= 一个简单程序的威力 - 控制联通!\n\u0026gt; 是不是最早的‘程序’卡?\nhollerith 的机器速度是手动的 10 倍左右,使人口普查在短短两年半内完成,给人口普查办公室省了上百万美元。企业开始意识到计算机的价值,可以提升劳动力以及数据密集型任务来提升利润。比如会计,保险评估和库存管理等行业。为了满足这一需求,hollerith 成立了制表机器公司,这家公司后来在 1924 年与其它机械制造商合并,成为了 \u0026ldquo;国际商业机器公司\u0026rdquo;,简称 ibm(the international business machines corporation)。\n= 利润!!!恐怕没有什么比它更吸引企业的了!\n你可能听过 ibm 😂\n这些电子机械的 \u0026ldquo;商业机器\u0026rdquo; 取得了巨大成功,改变了商业和政府。到了 1900 年代中叶,世界人口的爆炸和全球贸易的兴起。要求更快,更灵活的工具来处理数据,为电子计算机的发展奠定了基础。\n我们下周讨论。\n","date":"2022-08-14","permalink":"https://loveminimal.github.io/posts/cscc/01/","summary":"\u003cp\u003ei.e. Early Computing\u003c/p\u003e\n\u003cp\u003eHello world!我是 Carrie Anne,欢迎收看计算机科学速成课(Crash Course Computer Science)!\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/01/imgs/8a3e8687c14cce28b9f8368ee5e06354c6ce69625c4f8167244ec40f3079f3ea.png\" width=\"400\" /\u003e \r\n\u003cp\u003e\u003ccode\u003e\u0026gt; Carrie Anne 小姐姐\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e在这个系列中,我们会学习 Bits(位),Bytes(字节),晶体管(transistors),逻辑门(logic gates),一直到操作系统,虚拟现实和机器人!我们要学很多东西,但预先说明,我们 \u003cstrong\u003e不会\u003c/strong\u003e 教你怎么编程,我们会从高层次上纵览一系列计算机话题。\u003c/p\u003e","title":"cscc01 - 计算机早期历史"},{"content":" “圣人不治已病,治未病;不治已乱,治未乱。”\n-- 《黄帝内经·素问·四气调神大论》\n生老病死,成住坏空,古往皆如是。生命的前几十年,身体是在上升期,经得起小风小浪小消耗。然岁月不饶人,专治各种不服 …… 😅\n我们要爱护自己的身体,守护好自己的健康。养生之道,任重道远,却是千里之行,始于足下。生活中的每一点有益的改变,都会让我们身体受用。现代医学多是西方那一套,不是说不好,心中却总是有一种其“治标不治本”的“偏见”。西医的“头痛医头,脚痛医脚”,着实有其局限,它把人体的大系统割裂式的看待,感觉还是太局部了。之前也没有看过什么医学著作,主要是感觉看了也看不懂,不知为何心血来潮,突然就想了解一二(也就只能学一点皮毛常识)。就想着从我国传统医学著作着手,毕竟几千年的实践检验了,至少不会给自己带坑里去,而且感觉上中医侧重养生,比较符合目前的状态。再不济,全当强化一下古文学习了,当成一种休闲消遣也不错。\n百科一下,传统医学四大经典著作:《黄帝内经》、《难经》、《伤寒杂病论》、《神农本草经》,让我们择其一二先一窥究竟。\n一二经典 \u0026gt; 黄帝内经\n《黄帝内经》分《灵枢》、《素问》两部分,是一本综合性的医书,在黄老道家理论上建立了中医学上的“阴阳五行学说”、“脉象学说”、“藏象学说”、“经络学说”、“病因学说”、“病机学说”、“病症”、“诊法”、“论治”及“养生学”、“运气学”等学说。 其基本素材来源于中国古人对生命现象的长期观察、大量的临床实践以及简单的解剖学知识。《黄帝内经》奠定了人体生理、病理、诊断以及治疗的认识基础,是中国影响极大的一部医学著作,被称为医之始祖。\n\u0026gt; 难经\n《难经》是战国时期成书,作者是扁鹊 ,原名《黄帝八十一难经》,古代中医学著作之一,传说为战国时期秦越人(扁鹊)所作。本书以问答解释疑难的形式编撰而成,共讨论了81个问题,故又称《八十一难》,全书所述以基础理论为主,还分析了一些病证。其中一至二十二难为脉学,二十三至二十九难为经络,三十至四十七难为脏腑,四十八至六十一难为疾病,六十二至六十八为腧穴,六十九至八十一难为针法。\n\u0026gt; 四圣心源\n《四圣心源》是清·黄元御撰写于1753年的医书,又名《医圣心源》。作者将黄帝、岐伯、秦越人、张仲景视为医中四圣。本书阐发《内经》、《难经》、《伤寒论》、《金匮要略》诸书蕴义,卷一天人解;卷二六气解;卷三脉法解;卷四劳伤解;卷五至卷七杂病解;卷八七窍解;卷九疮疡解;卷十妇人解。是一部包括中医基本理论和部分临床医学的综合性著作。\n\u0026gt; 奇经八脉考\n奇经八脉是指十二经脉之外的八条经脉,包括任脉、督脉、冲脉、带脉、阴跷脉、阳跷脉、阴维脉、阳维脉。奇者,异也。因其异于十二正经,故称“奇经”。它们既不直属脏腑,又无表里配合。其生理功能,主要是对十二经脉的气血运行起着溢蓄、调节作用。(看小说的都爱这个,长长见识 🧐)\n其他如《金匮要略》、《温病条辨》、《伤寒杂病论》和《神农本草经》太过专项了,概览了一下,实在是看不懂,暂且不去管它,后续有一定基础后再啃。\n结语 无论做什么事情,思而后行,行而方有所得。看,你其实有很多事情值得去做,总可以变得更好。\n","date":"2022-08-08","permalink":"https://loveminimal.github.io/posts/learn-some-medicines/","summary":"\u003cblockquote\u003e\n\u003cp\u003e“圣人不治已病,治未病;不治已乱,治未乱。”\u003cbr\u003e\n\u003ccode\u003e-- 《黄帝内经·素问·四气调神大论》\u003c/code\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e生老病死,成住坏空,古往皆如是。生命的前几十年,身体是在上升期,经得起小风小浪小消耗。然岁月不饶人,专治各种不服 …… 😅\u003c/p\u003e","title":"你可能需要学一些医学知识"},{"content":"i.e. cs (computer science) crash course\n这个系列来自于 crash course 的一个视频系列课程(共有 40 课程,每个课程 10 分钟左右),多谢热心的 crash course 字幕组配上了优质的字幕 - b 站地址 📺 。\n知乎上已经有小伙伴做了同样的事 - 将视频课程文档化,详见 计算机速成课笔记 。我们在这里,重新制作一次文档,一是为了加深印象、深化理解;二是方便后续做内容扩展。\n好吧,竟然还有神奇的字幕组 - 计算机科学速成课字幕组 repo,本系列内容为以该仓库字幕文件为基础的图文版。\n感谢所有主创、二创人员的付出!🎉\n","date":"2022-08-03","permalink":"https://loveminimal.github.io/posts/cscc/00/","summary":"\u003cp\u003ei.e. cs \u003ccode\u003e(computer science)\u003c/code\u003e crash course\u003c/p\u003e\n\u003cimg alt=\"picture 2\" src=\"/posts/cscc/00/imgs/e95986763251c49010ca90d1af26b02870c7f0070bfa57452dae28bc028f8588.png\" width=\"\" /\u003e \r\n\u003cp\u003e这个系列来自于 \u003ca href=\"https://thecrashcourse.com/\"\u003eCRASH COURSE\u003c/a\u003e 的一个视频系列课程(共有 40 课程,每个课程 10 分钟左右),多谢热心的 CRASH COURSE 字幕组配上了优质的字幕 - \u003ca href=\"https://www.bilibili.com/video/BV1EW411u7th?p=1\u0026amp;vd_source=a6f6452712ce1cd91d115827d0148715\"\u003e B 站地址 📺\u003c/a\u003e 。\u003c/p\u003e","title":"cscc00 - 计算机科学速成课(引言)"},{"content":" 转载自 https://www.jb51.net/hardware/cpu/610074.html\n我们知道,cpu 性能是主要由 cpu 构架、核心线程数量、主频、缓存等诸多因素共同决定,而“缓存”是很多网友容易忽视的一个地方。那么,cpu 缓存是什么?在电脑 cpu 中,一二三级缓存究竟谁对 cpu 性能影响最重要呢?\ncpu 缓存是什么 cpu 缓存(cache memory)是位于 cpu 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。cpu 高速缓存的出现主要是为了解决 cpu 运算速度与内存读写速度不匹配的矛盾,因为 cpu 运算速度要比内存读写速度快很多,这样会使 cpu 花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内 cpu 即将访问的,当 cpu 调用大量数据时,就可先缓存中调用,从而加快读取速度。\n\u0026gt; cpu 缓存\n缓存大小是 cpu 的重要指标之一,而且缓存的结构和大小对 cpu 速度的影响非常大,cpu 内缓存的运行频率极高,一般是和处理器同频运作,工作效率远远大于系统内存和硬盘。实际工作时,cpu 往往需要重复读取同样的数据块,而缓存容量的增大,可以大幅度提升 cpu 内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能。但是从 cpu 芯片面积和成本的因素来考虑,缓存都很小。\n\u0026gt; cpu 缓存设计示意图\n按照数据读取顺序和与 cpu 结合的紧密程度,cpu 缓存可以分为一级缓存,二级缓存,如今主流 cpu 还有三级缓存,甚至有些 cpu 还有四级缓存。每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。\n为什么 cpu 会有 l1、l2、l3 这样的缓存设计?主要是因为现在的处理器太快了,而从内存中读取数据实在太慢(一个是因为内存本身速度不够,另一个是因为它离 cpu 太远了,总的来说需要让 cpu 等待几十甚至几百个时钟周期),这个时候为了保证 cpu 的速度,就需要延迟更小速度更快的内存提供帮助,而这就是缓存,如下图所示。\n当 cpu 要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在 80%左右,也就是说全部数据量的 80%都可以在一级缓存中找到,只剩下 20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个 cpu 缓存架构中最为重要的部分。\ncpu 的一二三级缓存哪个最重要 一般来说,每级缓存的命中率大概都在 80%左右,也就是说全部数据量的 80%都可以在一级缓存中找到,只剩下 20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个 cpu 缓存架构中最为重要的部分。\n但是,现在 cpu 的一级缓存几乎都一样,容量都比较小,多为 64k,因此如今的 cpu 基本很少提一级缓存,主要是大家都一样,虽然最重要,但却不值得一提。\n二级缓存,对 cpu 是很重要的,不过很多朋友会发现,如今很多 intel 的 cpu 也都不怎么提二级缓存,只标注三级缓存。而 amd 的不少新 cpu 也多为标注三级缓存为主,二级缓存只有部分型号会标注,比如 amd 锐龙 5 2600x 提供 3m 三级缓存和 16m 三级缓存,r7 2700x 则也只有 16m 三级缓存。而 intel 酷睿 i3 8100 则只有 6m 三级缓存,高端的 i7 8700k 则只标注 12mb。\n因此,在目前的新款 cpu 中,二级缓存的重要性在减弱,三级缓存则成为重点。\n现代 cpu 的高速缓存体系结构是非常复杂的,其中包括硬件预取和数据转发,以便能提供最佳的高速缓存命中机会,有些 cpu 甚至还加入了 l4 缓存。\n以上就是电脑 cpu 一二三级缓存的知识科普。对于普通电脑用户来说,只要知道 cpu 缓存是决定 cpu 性能的因素之一,普通用户对 cpu 缓存并不太敏感,游戏玩家更为在意的 cpu 核心数、主频等因素,而对于一些 3d 制图、视频渲染用户来说,比较考验 cpu 综合性能,这个时候大缓存会显得更优势。\n","date":"2022-08-02","permalink":"https://loveminimal.github.io/posts/cpu-cache/","summary":"\u003cblockquote\u003e\n\u003cp\u003e转载自 \u003ca href=\"https://www.jb51.net/hardware/cpu/610074.html\"\u003ehttps://www.jb51.net/hardware/cpu/610074.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我们知道,CPU 性能是主要由 CPU 构架、核心线程数量、主频、缓存等诸多因素共同决定,而“缓存”是很多网友容易忽视的一个地方。那么,CPU 缓存是什么?在电脑 CPU 中,一二三级缓存究竟谁对 CPU 性能影响最重要呢?\u003c/p\u003e","title":"cpu 缓存是什么"},{"content":" 转载自 https://www.jb51.net/hardware/cpu/610350.html\n2018 年 4 月 3 日,intel 在北京举办了第八代酷睿移动处理器全球发布会,正式发布了全新的第八代酷睿移动处理器。很多小伙伴其实对 intel 的芯片很感兴趣,但是一但深入了解,很快就会被扑面而来的晶体管,微架构,纳米,cpi,睿频,超频,缓存等概念搞疯………\n为何 cpu 工作如此迅速,为何主频变化多端,纳米级别的工艺到底是外星文明的帮助还是人类智慧的结晶,一片薄薄硅圆晶上到底隐藏着怎样的秘密,就让我们大家一起走一走“芯”吧……\n先说说,芯片到底是个什么东西。\n芯片,就是我们说的 cpu,又名中央处理器,常说芯片是一个机器的大脑,重要性可见一斑,它负责计算机多项工作的完成,快速处理数据,并把工作分类并传输指令给计算机的其它部件,这有点像一个物流中心,要把源源不断又种类繁多的数据“包裹”分析,分类,处理,传送……\n所以,要不我们就把它比作一个物流中心好了,为方便理解,我们就从甄老板要开一个物流厂说起吧……\n从沙子到芯片 甄老板要开一个物流厂,首先第一件事,当然是要选一块地,芯片物流厂的用地要求比较奇怪,它是需要建在沙子上的,因为工厂要顺利运行,需要一种沙子里面含有的特殊元素,si(硅),这也就是我们熟知的 cpu 是由沙子造的。\n但是我们知道沙子上是不能直接建厂的,所以沙子需要特殊处理,这个处理就是通过一系列的提纯及切割,获得芯片基座-圆晶的过程。\n圆晶(wafer)是芯片的基础,是由硅纯度 99.9999% 的硅锭切割成的薄片。在这个基础上,芯片制造商们将展开体现人类制造智慧的顶级工艺。当然,为了好理解,我们还是回到工厂的建造上来吧。\n纳米技术及微架构 好了,现在工厂有了地,但是怎么建造流水线设施,让工人好好干活呢?\n当然得找个牛一点的设计师先设计一下厂房的构造, 越牛的设计师,设计得越精巧,就是我们说的“工艺”,随着时代的进步,设计师们的精细度也从 45nm 级的设计,逐步精细化到 32,22,甚至现在的第八代 14nm,工艺越好,能容纳的工作单位(晶体管)就越多,一个针尖就有高达 5k 晶体管,当然也就意味着可以在工厂干活的人也越来越多,干活的速度也就能越来快啦……\n设计师们还得去设计厂房的具体布局,比如这条流水线应该从左往右,还是从上到下呢?这种布局就叫微架构,当然布局过程非常复杂,介于我们都不是学建筑的,具体的工序也没必要特别了解,通俗点讲可以看作是一个在晶圆上做微雕,雕出众多晶体管的过程……这个东东的技术性和复杂性都已经登峰造极了,下方的部分工艺大家随意感受下就行……\n架构决定了芯片的性能和功能,设计师们为了工人们能更快,更好的干活,并且能处理更多新出现的任务,在架构上简直绞尽脑汁,尽善尽美,所以在工艺进步的同时,也经常在架构上推陈出新,比如第八代 intel 就是“coffee lake-h”设计代替了前一代的“kabylake-h”的设计。\n不过微微吐槽一句,intel 的架构命名还真是好喜欢用地名啊,不是这个湖就是那个桥,可能是想表明他们就是个跟土建沙石有关的公司吧(……大误)。\n晶体管做完之后,后续还有一系列的测试,切割,封装等工序,圆晶就变成了一块块芯片,我们的工厂也就建好啦。\n虽然讲的很简单,但其实背后有很多的尖端工艺,intel 作为金字塔的顶端,也只有 70% 的成功率,所以芯片行业真是汇集了精英资本和精英智慧的一个产业啊,不是高帅富的大佬还真的玩不起,当然这也是为什么,世界上的芯片制造商屈指可数的原因。\n工厂总算建好啦,工人们各就各位,开始干活吧……\n时钟频率与核心 人和厂都准备好了,但是人实在太多了,如果不好好管理的话,也会乱套,所以甄老板找了个大喇叭统一喊“1,2,1,2”,让工人们有节奏地干活,这个“1,2,1,2”就是时钟频率。\n通常意义上我们把它认为是 cpu 的主频,主频越高,说明工人们干活越快,cpu 的速度也就越快,一般我们能看到就是 xxghz 这样的显示,数值越高,说明速度越快。\n当然甄老板为了效率能更高点,可能会同时建 2 个或者 4 个厂房,就我们熟知的双核 cpu,4 核 cpu,这样通过协同,工人们的效率就更高,单位时间能处理的活也就越多。\n而这样根据不同的等级,cpu 也就有了 i3,i5,i7 之分(现在还出现了 i9 级 cpu),等级越高,主频表现越好,工厂越有效率,而在第八代的 intel 芯片中,又有了划时代的产品:首次出现了 6 核的移动平台 cpu。\n这颗芯将主流移动计算提身高了 6 核的时代(需要说明的是服务器及商用领域其实早就已经出现 6 核,但是移动平台是首次),当然,我们在这一张图中除了看到熟悉的 14 纳米制程工艺,核心数之外,还看到了另外的参数,一是功耗,一是线程,功耗我们放后面说,先说说线程。\n线程、睿频、超频 还是举工厂的例子,甄老板在厂房布局(微架构)及工人数量(晶体管)的布置上已经到了极限,还想提高效率怎么办呢?就要说到我们下面的概念了线程和超线程。\n线程其实是个虚拟概念,类似于工厂外面排好等待处理的包裹队列,一般每个核心对应着一个线程,包裹按顺序进入工厂,这样 cpu 就可以有条不稳地工作了\n而超线程相当于一个核心对应了两条队列,两条队列上的包裹按工厂的处理能力依次进入厂房,道路宽了,也让整个进程更有效率。\n然后工厂这边,任务突然多了起来,处理速度也要相应的提高,甄老板想到了一个办法,号召想多干活的工人们去拿加班奖励,一部分工人当然积极响应,多出来的任务很快完成了,这个自主接收激励的动作,就叫睿频。\n睿频:是指当启动一个运行程序后,cpu 会自动加速到合适的频率,而原来的运行速度会提升 10%~20% 以保证程序流畅运行的一种技术。睿频是 intel 的一个重要技术,也是智能 cpu 的基础,这项技术被运用到了 intel 的全系列,包括第八代 cpu。\n下图就是最新的睿频技术,可以提升至 4.8ghz,完成峰值任务 so easy!\n当然,有时候工厂的活特别多,自愿去加班的工人都完成不了了。这时候甄老板不得不放出了“全体人必须加班“的大招,将工厂的速度强制提升到某一个值上,这个动作叫做超频。\n超频:用户强制将处理器的所有内核运行在规格限定频率范围之外,以求更好的速度。这个动作通过给 cpu 加压实现的,虽然可以提升效率,但有时候也会损伤元件,各位少侠,请慎重使用哦。\n缓存与傲腾 经过一系列的处理,工厂的生意越来越好,但是甄老板渐渐发现,这个厂,有!问!题!\n因为他发现,每次包裹传送带都要从很远的“内存仓库“甚至更远的“硬盘仓库”传送,路途遥远,耗费时常不说,还经常丢失包裹。\n痛定思痛,甄老板决定在厂房的周围自建仓库以临时存储待处理的包裹,离厂房最近且最快的叫一级仓库,但因为地价比较贵,所以一级仓库的面积最小,以此类推,又建了二级仓库,三级仓库,三个仓库配合无间,让包裹能不间断地运送到工厂。\n这就是 cpu 一,二,三级缓存的概念(下图中其实只能看到三级,因为一二级太小了)。\n后来 intel 认为,3 级缓存已经跟不上现在的时代,所以在最近的第八代 cpu 中,直接在芯片上建了了内存仓库大小的超级缓存,就是“傲腾”。\n这个可选配的“傲腾”内存,这大大提高了用户的峰值速度,也让电脑在大任务面前,终于可以轻松起飞。\n能耗 刚刚借着 intel 第八代 cpu 普及了很多芯片上的参数,最后说说能耗一般效率越高的工厂,当然能耗就越多,放在 cpu 上,意味着更废电,以及发热更大,所以保证运行的稳定,cpu 主频往往到一定阶段就不再上升了,能耗性能如何平衡?也是考验 cpu 厂商的一个关键。\n不过 intel 在这方面处理得还是相当不错的,可以说是极大的兼顾了速度与稳定在第八代的 intel i9 cpu 中还首次实现了移动智能不锁频的技术,可以说是很艺高人胆大了。\n结语 熟悉 intel 的人对这个发布会还是有所意料,因为毕竟我们都已经知道,intel 遵循 tick-tock 开发模式,两年更新一个工艺,隔年更新微架构,不过这两年 intel 在工艺上已经遥遥领先于对手,所以新 cpu 在工艺上的更新并没有太大,但转而在架构上进行调整,以适应现阶段如 4k、vr、娱乐、游戏、无线技术等更应用化也更新的多任务的处理。\n不管怎么说,一代确实比一代好了,而随着时代的发展,intel 也逐渐由原来单纯的芯片厂商,逐渐开始在自家的处理器上集成显卡、内存(四级缓存),不满足做个快递大厂,开始做店面,还包了飞机火车轮船,直接开始打开了地图打野模式……\n当然乐见的是,技术的革新为新硬件的诞生也提供了坚实的基础。\n未来已来,我们拭目以待!\n","date":"2022-08-02","permalink":"https://loveminimal.github.io/posts/expensive-cpu/","summary":"\u003cblockquote\u003e\n\u003cp\u003e转载自 \u003ca href=\"https://www.jb51.net/hardware/cpu/610350.html\"\u003ehttps://www.jb51.net/hardware/cpu/610350.html\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2018 年 4 月 3 日,Intel 在北京举办了第八代酷睿移动处理器全球发布会,正式发布了全新的第八代酷睿移动处理器。很多小伙伴其实对 intel 的芯片很感兴趣,但是一但深入了解,很快就会被扑面而来的晶体管,微架构,纳米,CPI,睿频,超频,缓存等概念搞疯………\u003c/p\u003e","title":"用沙子造的 cpu 凭什么卖的那么贵"},{"content":"好吧,试了,但真的没找到 “佚名” 是谁 😂 ,希望原作者能感受到我的原力感谢吧…… 不少网站都有转载该篇文章,以下是图片较清晰的几个页面链接。\nhttps://www.sohu.com/a/255397866_468626 https://www.jb51.net/hardware/cpu/611229.html https://www.jb51.net/hardware/cpu/610350.html 1 寸 = 33.3333 mm 集成电路 ic (integrated circuit )\n引言 cpu 是现代计算机的核心部件,又称为“微处理器”。对于 pc 而言,cpu 的规格与频率常常被用来作为衡量一台电脑性能强弱重要指标。\n许多对电脑知识略知一二的朋友大多会知道 cpu 里面最重要的东西就是晶体管了,提高 cpu 的速度,最重要的一点说白了就是如何在相同的 cpu 面积里面放进去更加多的晶体管,由于 cpu 实在太小,太精密,里面组成了数目相当多的晶体管,所以人手是绝对不可能完成的,只能够通过光刻工艺来进行加工的。\n这就是为什么一块 cpu 里面为什么可以数量如此之多的晶体管。晶体管其实就是一个双位的开关:即开和关。如果您回忆起基本计算的时代,那就是一台计算机需要进行工作的全部。两种选择,开和关,对于机器来说即 0 和 1。\n那么如何制作一个 cpu 呢?\n大家可能只知道制作 ic 芯片的硅来源于沙子,但是为什么沙子做的 cpu 却卖那么贵?下面本文以 intel 电脑 cpu 作为例子,讲述沙子到 cpu 简要的生产工序流程,希望大家对 cpu 制作的过程有一个大体认识,其它 cpu 或者手机 cpu 制造原理也大抵相同。\n一、硅圆片的制作 1. 硅的重要来源:沙子 作为半导体材料,使用得最多的就是硅元素,其在地球表面的元素中储量仅次于氧,含硅量在 27.72%,其主要表现形式就是沙子(主要成分为二氧化硅),沙子里面就含有相当量的硅。因此硅作为 ic 制作的原材料最合适不过,想想看地球上有几个浩瀚无垠的沙漠,来源既便宜又方便。\n2. 硅熔炼、提纯 不过实际在 ic 产业中使用的硅纯度要求必须高达 99.999999999%。目前主要通过将二氧化硅与焦煤在 1600-1800℃中,将二氧化硅还原成纯度为 98% 的冶金级单质硅,紧接着使用氯化氢提纯出 99.99% 的多晶硅。虽然此时的硅纯度已经很高,但是其内部混乱的晶体结构并不适合半导体的制作,还需要经过进一步提纯、形成固定一致形态的单晶硅。\n3. 制备单晶硅锭 单晶的意思是指原子在三维空间中呈现规则有序的排列结构,而单晶硅拥有“金刚石结构”,每个晶胞含有 8 个原子,其晶体结构十分稳定。\n通常单晶硅锭都是采用直拉法制备,在仍是液体状态的硅中加入一个籽晶,提供晶体生长的中心,通过适当的温度控制,就开始慢慢将晶体向上提升并且逐渐增大拉速,上升同时以一定速度绕提升轴旋转,以便将硅锭控制在所需直径内。结束时,只要提升单晶硅炉温度,硅锭就会自动形成一个锥形尾部,制备就完成了,一次性产出的 ic 芯片更多。\n\u0026gt; 籽晶是具有和所需晶体相同晶向的小晶体,是生长单晶的种子,也叫晶种。\n制备好的单晶硅锭直径约在 300mm 左右,重约 100kg。而目前全球范围内都在生产直径 12 寸的硅圆片,硅圆片尺寸越大,效益越高。\n4. 硅锭切片 将制备好的单晶硅锭一头一尾切削掉,并且对其直径修整至目标直径,同时使用金刚石锯把硅锭切割成一片片厚薄均匀的晶圆(1mm)。有时候为了定出硅圆片的晶体学取向,并适应 ic 制作过程中的装卸需要,会在硅锭边缘切割出“取向平面”或“缺口”标记。\n5. 研磨硅圆片 切割后的晶圆其表面依然是不光滑的,需要经过仔细的研磨,减少切割时造成的表面凹凸不平,期间会用到特殊的化学液体清洗晶圆表面,最后进行抛光研磨处理,还可以在进行热处理,在硅圆片表面成为“无缺陷层”。一块块亮晶晶的硅圆片就这样被制作出来,装入特制固定盒中密封包装。\n\u0026gt; 制作完成的硅圆片\n通常半导体 ic 厂商是不会自行生产这种晶圆,通常都是直接从硅圆片厂中直接采购回来进行后续生产。\n二、前工程 \u0026ndash; 制作带有电路的芯片 6. 涂抹光刻胶 买回来的硅圆片经过检查无破损后即可投入生产线上,前期可能还有各种成膜工艺,然后就进入到涂抹光刻胶环节。微影光刻工艺是一种图形影印技术,也是集成电路制造工艺中一项关键工艺。首先将光刻胶(感光性树脂)滴在硅晶圆片上,通过高速旋转均匀涂抹成光刻胶薄膜,并施加以适当的温度固化光刻胶薄膜。\n\u0026gt; 光刻胶是一种对光线、温度、湿度十分敏感的材料,可以在光照后发生化学性质的改变,这是整个工艺的基础。\n7. 紫外线曝光 就单项技术工艺来说,光刻工艺环节是最为复杂的,成本最为高昂的。因为光刻模板、透镜、光源共同决定了“印”在光刻胶上晶体管的尺寸大小。\n将涂好光刻胶的晶圆放入步进重复曝光机的曝光装置中进行掩模图形的“复制”。掩模中有预先设计好的电路图案,紫外线透过掩模经过特制透镜折射后,在光刻胶层上形成掩模中的电路图案。一般来说在晶圆上得到的电路图案是掩模上的图案 1/10、1/5、1/4,因此步进重复曝光机也称为“缩小投影曝光装置”。\n一般来说,决定步进重复曝光机性能有两大要素:一个是光的波长,另一个是透镜的数值孔径。如果想要缩小晶圆上的晶体管尺寸,就需要寻找能合理使用的波长更短的光(euv,极紫外线)和数值孔径更大的透镜(受透镜材质影响,有极限值)。\n\u0026gt; asml 公司 twinscan nxe:3300b\n8. 溶解部分光刻胶 对曝光后的晶圆进行显影处理。以正光刻胶为例,喷射强碱性显影液后,经紫外光照射的光刻胶会发生化学反应,在碱溶液作用下发生化学反应,溶解于显影液中,而未被照射到的光刻胶图形则会完整保留。显影完毕后,要对晶圆表面的进行冲洗,送入烘箱进行热处理,蒸发水分以及固化光刻胶。\n\u0026gt; 将光刻胶分为两类,一类是负性光刻胶,一类是正性光刻胶。在显影后,正性光刻胶未曝光的部分将被保留下来,而负性光刻胶则在显影后保留下来。也就是说,在曝光结束后,用显影液显影的是正性光刻胶的感光区、负性光刻胶的非感光区,将溶解在显影液中,此步骤完成后,光刻胶涂层上的图象便可显示出来。为提高分辨率,几乎每种光刻胶都配备了专用显影液,以保证显影效果。\n\u0026gt; 负性光刻胶一般采用负性显影液。在无界面活性剂的情况下,正性光刻胶采用正性显影液显影。正性和负性显影液都是碱性溶剂,其主要区别是负性显影液中含有表面活性剂,而正性显影液中没有这种成分。\n9. 蚀刻 将晶圆浸入内含蚀刻药剂的特制刻蚀槽内,可以溶解掉暴露出来的晶圆部分,而剩下的光刻胶保护着不需要蚀刻的部分。期间施加超声振动,加速去除晶圆表面附着的杂质,防止刻蚀产物在晶圆表面停留造成刻蚀不均匀。\n10. 清除光刻胶 通过氧等离子体对光刻胶进行灰化处理,去除所有光刻胶。此时就可以完成第一层设计好的电路图案。\n11. 重复第 6-8 步 由于现在的晶体管已经 3d finfet finfet 的原理与工艺 设计,不可能一次性就能制作出所需的图形,需要重复第 6-8 步进行处理,中间还会有各种成膜工艺(绝缘膜、金属膜)参与到其中,以获得最终的 3d 晶体管。\n12. 离子注入 在特定的区域,有意识地导入特定杂质的过程称为“杂质扩散”。通过杂质扩散可以控制导电类型(p 结、n 结)pn 结原理简述之外,还可以用来控制杂质浓度以及分布。\n现在一般采用离子注入法进行杂质扩散,在离子注入机中,将需要掺杂的导电性杂质导入电弧室,通过放电使其离子化,经过电场加速后,将数十到数千 kev 能量的离子束由晶圆表面注入。离子注入完毕后的晶圆还需要经过热处理,一方面利用热扩散原理进一步将杂质“压入”硅中,另一方面恢复晶格完整性,活化杂质电气特性。\n离子注入法具有加工温度低,可均匀、大面积注入杂质,易于控制等优点,因此成为超大规模集成电路中不可缺少的工艺。\n10. 再次清除光刻胶 完成离子注入后,可以清除掉选择性掺杂残留下来的光刻胶掩模。此时,单晶硅内部一小部分硅原子已经被替换成“杂质”元素,从而产生可自由电子或空穴。\n\u0026gt; 左:硅原子结构; 中:掺杂砷,多出自由电子; 右:掺杂硼,形成电子空穴\n真的是复杂……\r11. 绝缘层处理 此时晶体管雏形已经基本完成,利用气相沉积法,在硅晶圆表面全面地沉积一层氧化硅膜,形成绝缘层。同样利用光刻掩模技术在层间绝缘膜上开孔,以便引出导体电极。\n12. 沉淀铜层 利用溅射沉积法,在晶圆整个表面上沉积布线用的铜层,继续使用光刻掩模技术对铜层进行雕刻,形成场效应管的源极、漏极、栅极。最后在整个晶圆表面沉积一层绝缘层以保护晶体管。\nokay ,现在我们有晶体管了……\r13. 构建晶体管之间连接电路 经过漫长的工艺,数以十亿计的晶体管已经制作完成。剩下的就是如何将这些晶体管连接起来的问题了。同样是先形成一层铜层,然后光刻掩模、蚀刻开孔等精细操作,再沉积下一层铜层。\u0026hellip;..,这样的工序反复进行多次,这要视乎芯片的晶体管规模、复制程度而定。最终形成极其复杂的多层连接电路网络。\n由于现在 ic 包含各种精细化的元件以及庞大的互联电路,结构非常复杂,实际电路层数已经高达 30 层,表面各种凹凸不平越来越多,高低差异很大,因此开发出 cmp 化学机械抛光技术。每完成一层电路就进行 cmp 磨平。\n另外为了顺利完成多层 cu 立体化布线,开发出大马士革法新的布线方式,镀上阻挡金属层后,整体溅镀 cu 膜,再利用 cmp 将布线之外的 cu 和阻挡金属层去除干净,形成所需布线。\n\u0026gt; 大马士革法多层布线\n芯片电路到此已经基本完成,其中经历几百道不同工艺加工,而且全部都是基于精细化操作,任何一个地方出错都会导致整片晶圆报废,在 100 多平方毫米的晶圆上制造出数十亿个晶体管,是人类有文明以来的所有智慧的结晶。\n三、后工程 \u0026ndash; 从划片到成品销售 14. 晶圆级测试 前工程与后工程之间,夹着一个 good-chip/wafer 检测工程,简称 g/w 检测。目的在于检测每一块晶圆上制造的一个个芯片是否合格。通常会使用探针与 ic 的电极焊盘接触进行检测,传输预先编订的输入信号,检测 ic 输出端的信号是否正常,以此确认芯片是否合格。\n由于目前 ic 制造广泛采用冗余度设计,即便是“不合格”芯片,也可以采用冗余单元置换成合格品,只需要使用激光切断预先设计好的熔断器即可。当然,芯片有着无法挽回的严重问题,将会被标记上丢弃标签。\n15. 晶圆切片、外观检查 ic 内核在晶圆上制作完成并通过检测后后,就进入了划片阶段。划片使用的划刀是粘附有金刚石颗粒的极薄的圆片刀,其厚度仅为人类头发的 1/3。将晶圆上的每一个 ic 芯片切划下来,形成一个内核 die。\n裂片完成后还会对芯片进行外观检查,一旦有破损和伤痕就会抛弃,前期 g/w 检查时发现的瑕疵品也将一并去除。\n\u0026gt; 未裂片的一个个 cpu 内核\n16. 装片 芯片进行检测完成后只能算是一个半成品,因为不能被消费者直接使用。还需要经过装片作业,将内核装配固定到基片电路上。装片作业全程由于计算机控制的自动固晶机进行精细化操作。\n17. 封装 装片作业仅仅是完成了芯片的固定,还未实现电气的连接,因此还需要与封装基板上的触点结合。现在通常使用倒装片形式,即有触点的正面朝下,并预先用焊料形成凸点,使得凸点与相应的焊盘对准,通过热回流焊或超声压焊进行连接。\n封装也可以说是指安装半导体集成电路芯片用的外壳,它不仅起着安放、固定、密封、保护芯片,还可以增强导热性能的作用。目前像 intel 近些年都采用 lga 封装,在核心与封装基板上的触点连接后,在核心涂抹散热硅脂或者填充钎焊材料,最后封装上金属外壳,增大核心散热面积,保护芯片免受散热器直接挤压。\n至此,一颗完整的 cpu 处理器就诞生了。\n18. 等级测试 cpu 制造完成后,还会进行一次全面的测试。测试出每一颗芯片的稳定频率、功耗、发热,如果发现芯片内部有硬件性缺陷,将会做硬件屏蔽措施,因此划分出不同等级类型 cpu,例如 core i7、i5、i3。\n19. 装箱零售 cpu 完成最终的等级划测试后,就会分箱进行包装,进入 oem、零售等渠道。\n结语 现在进入了科技时代,极度依赖计算机科学与技术,其中的 cpu 又是各种计算机必不可少重要部件。暂且不论架构上的设计,仅仅在 cpu 的制作上就凝聚了全人类的智慧,基本上当今世界上最先进的工艺、生产技术、尖端机械全部都投入到了该产业中。因此半导体产业集知识密集型、资本密集型于一身的高端工业。\n\u0026gt; cpu 制造全过程图解\n一条完整而最先进 cpu 生产线投资起码要数十亿人民币,而且其中占大头的是前工程里面的光刻机、掩膜板、成膜机器、扩散设备,占到总投资的 70%,这些都是世界上最精密的仪器,每一台都价值不菲。作为参照,cpu 工厂建设、辅助设备、超净间建设费用才占到 20%。\n不知道大家看到这里,觉得最低几百块就可以买到一颗汇聚人类智慧结晶的 cpu,还值不值呢?\n","date":"2022-08-01","permalink":"https://loveminimal.github.io/posts/how-to-make-a-cpu/","summary":"\u003cp\u003e好吧,试了,但真的没找到 “佚名” 是谁 😂 ,希望原作者能感受到我的原力感谢吧…… 不少网站都有转载该篇文章,以下是图片较清晰的几个页面链接。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.sohu.com/a/255397866_468626\"\u003ehttps://www.sohu.com/a/255397866_468626\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.jb51.net/hardware/cpu/611229.html\"\u003ehttps://www.jb51.net/hardware/cpu/611229.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.jb51.net/hardware/cpu/610350.html\"\u003ehttps://www.jb51.net/hardware/cpu/610350.html\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003e1 寸 = 33.3333 mm\u003c/code\u003e\n\u003ccode\u003e集成电路 ic (integrated circuit )\u003c/code\u003e\u003c/p\u003e","title":"cpu 是如何制造出来的"},{"content":"约两个月前,午休后的一个小意外不小心拉伤了右手姆指,当时也没有太注意,想着过几天就好了。俗话说,“伤筋动骨一百天嘛”。一周又一周,情况并没有得到好转。正好周末安排了体检,就顺带就诊一下。\n为什么拖了那么久?一是不在意,二是天太热、人太懒。\n不得不说“二档的医保不如狗”,限制太多 😿。医院的医生呢?现在离开各种检查就跟个“白痴”一样,当然,医生也要吃饭,但水平确实有待提一提。经历过一系列 blabla\u0026hellip; 的检查,医生瞄了半眼片子,真的就半眼(无力吐槽\u0026hellip;),来了句不用吃药,做三周(3 次)的冲击波治疗\u0026hellip; 好的,不是情绪上的对立,不过现在的医患关系总给我一种下图的感觉,反正 1000 大洋就这样交待了。\n做护理治疗的小姐姐还可以,挺温柔,说可能有些疼。过程中痛感也还好,感觉可能是轻微拉伤的原因。好吧,5 分钟 200 大洋 💰 。目前效果还不显著,估计是在恢复期,希望可以完美恢复。\n总之呢,身体已经在敲警钟了。\n想想,不知道有多少人长期处于亚健康的状态,一天又一天,像是一辆“衣衫褴褛”的自行车(除了铃铛不响)歪歪扭扭地向前行驶。当身体给我们发信号的时候,千万不要忽视它,意味着你的健康已欠费了,请充值。“人生不过数十载,无病无灾过一生”,真好!\n言归正传,之前没检查还好,一检查,感觉整个手都不太好了,搞得现在基本上是“一指禅”输入。作为一个程序猿,这真的是被缴了枪了。听我说,一定要爱护好你的手,脱离输入效率神教,“远离 emacs” 😂,好吧,少用点。业余时间,不要长时间玩手机或敲击键盘,可以做一些手工 diy 、弹吉他、吹笛子、下象棋、读纸质书等,总之就是不要让你的手过渡地重复性劳损。\n治疗后续\n第二次是真的有感觉痛了,医生小姐姐说是因为内部组织重建、血液流通了,痛觉更强烈…… 好吧,真的痛,后悔没有接受小姐姐的建议,在那里冷敷一下。好吧,下次一定…… 不过效果是真的感觉到了这次,估计再有一次就可以完全恢复了,这倒是一件让人欣慰的事情了。\n如何养护 如何养护你的手,先停止输入请敲击键盘 🤣 …… 好吧,一指禅太弱了,先这样,后续再补充。\n","date":"2022-08-01","permalink":"https://loveminimal.github.io/posts/protect-your-hands/","summary":"\u003cp\u003e约两个月前,午休后的一个小意外不小心拉伤了右手姆指,当时也没有太注意,想着过几天就好了。俗话说,“伤筋动骨一百天嘛”。一周又一周,情况并没有得到好转。正好周末安排了体检,就顺带就诊一下。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e为什么拖了那么久?一是不在意,二是天太热、人太懒。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"保护好你的手"},{"content":"🔔 摘录自 冴羽写博客之深入系列 ,写的很不错。\n深入系列 从原型到到原型链 先来一张“震山总图”,后续我们会慢慢讲解它是什么得到的。\nprototype 每个函数都有一个 prototype 属性。那么这个函数到底指向的是什么?是这个函数的原型吗?\n先来看个简单的示例:\nfunction person() { } // 虽然写在注释里,但是你要注意: // prototype 是函数才会有的属性 person.prototype.name = \u0026#39;kevin\u0026#39;; var person1 = new person(); var person2 = new person(); console.log(person1.name)\t// kevin console.log(person2.name)\t// kevin 其实,函数的 prototype 属性指向了一个对象,这个对象正是调用构造函数面创建的实例的原型,也就是上述示例中 person1 和 person2 的原型。\n说了这么多, 什么是原型呢? 你可以这样理解:每一个 javascript 对象( null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性。\n“继承” ❓ 继承意味着复制操作,然而 javascript 默认并不会复制对象的属性,相反,它只是在两个对象之间创建一个关联,如此一个对象就可能通过委托访问另一个对象的属性和函数,所以,与其叫继承,委托的说法反而更准确些。\n上图表示构造函数和实例原型(此处我们用 object.prototype 表示实例原型)之间的关系。那么,我们该怎么表示实例与实例原型,即 person 和 person.prototype 之间的关系呢?__proto__ ❗\n__proto__ 每一个 javascript 对象(除了 null)都具有一个属性 \u0026ndash; __proto__ ,这个属性会指向该对象的原型。\n关于 __proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 person.prototype 中,实际上,它是来自于 object.prototype ,与其说是一个属性,不如说是一个 getter/setter ,当使用 obj.__proto__ 可以理解成返回了 object.getprototypeof(obj) 。\n为了证明这一点,我们可以在 firefox 或 chrome 浏览器中输入:\nfunction person() { } var person = new person(); console.log(person.__proto__ === person.prototype);\t// true chrome 的开发工具,真是一个神器 🎉\rok,现在让我们更新一下关系图,如下:\n既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数呢?\n原型:是啊,凭什么只能你指我,不服 👺\rconstructor 指向实例的倒是没有,因为一个构造函数可以生成多个实例。\n物以稀为贵,多了就是“原罪”……\r但是原型指构造函数倒是有的 \u0026ndash; constructor ,每个原型都有一个 constructor 属性指向关联的构造函数。如图:\n综上我们已经得出:\nfunction person() { } var person = new person(); console.log(person.__proto__ == person.prototype) // true console.log(person.prototype.constructor == person) // true // 顺便学习一个 es5 的方法,可以获得对象的原型 console.log(object.getprototypeof(person) === person.prototype) // true 实例与原型 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。\nfunction person() { } person.prototype.name = \u0026#39;kevin\u0026#39;; var person = new person(); person.name = \u0026#39;daisy\u0026#39;; console.log(person.name) // daisy delete person.name; console.log(person.name) // kevin 原型的原型是什么呢 ❓ 最顶层也没查到怎么办?\n在前面,我们已经讲了原型也是一个对象,即然是对象,我们就可以用最原始的方式创建它。如下:\nvar obj = new object(); obj.name = \u0026#39;kevin\u0026#39; console.log(obj.name) // kevin 上述示例中的 obj 就是 person.prototype 。其实原型对象就是通过 object 构造函数生成的。\n原型链 这样,就又产生了一个问题, ojbect.prototype 的原型是什么呢? null ❗ 不妨打印一下:\nconsole.log(object.prototype.__proto__ === null)\t// true 然而 null 到底是个什么东东?\nnull 表示“没有对象”,即该处不应该有值。所以 object.prototype.__proto__ 的值为 null 跟 object.prototype 没有原型,其实表达了一个意思。\n所以,查找属性的时候查到的 object.prototype 就可以停止查找了。\n然后,我们就得到了最初的这张图:\n其实,这些都是规定好了的,以上各项都是产生这种规定的可能的过程。\r词法作用域和动态作用域 什么是作用域?作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。\njavascript 采用词法作用域(lexical scoping),即静态作用域。\n其实,这个概念并没有那么多弯弯绕,它是 javascript 这门语言的运行机制和实现所自然而然会出现的结果。\r所谓“词法作用域”,就是函数的作用域在函数定义的时候就决定了;而与之相对对应的“动态作用域”,函数的作用域则是在函数调用的时候才决定的。\n我们先来看一个示例:\nvar value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar(); // 结果是 ??? 结果是 1 !你答对了吗? 让我们分析一下具体的执行过程。\n执行 bar 函数,其在内部调用了 foo 函数,foo 函数执行输出 value 。输出哪个 value ?(上面说过,所谓“词法作用域”,就是函数的作用域在函数定义的时候就决定了)在 foo 定义的时候,其内部并没有局部变量 value ,就根据书写的位置查找上一层的代码,(在这里也就是 var value = 1)即 value 等于 1 ,所以结果会打印 1 。\n试想,如果 javascript 采用的是动态作用域,会输出什么?\n明白了吗?😏\n好的,我们再来看一下示例,如下:\nvar value = 10; foo(); var value = 20; function foo() { console.log(value); } 思考一下,上面的代码会输出什么?为什么?\n❗️ 10\r为什么会是 10 呢?不应该是 20 吗?\r是这样的!在 javascript 中,函数声明和变量声明都会被提升,且函数声明会被先提升。\r提升之后,上述代码的实际执行顺序就如下面这样了(伪代码):\r---------\rvar value;\rfunction foo;\rvalue = 10;\rfoo = function() {\rconsole.log(value);\r}\rfoo();\rvalue = 20;\r---------\r看,现在很容易就可能看出输出 10 了,对吧。\r💡 注意:提升的只是声明,并不是初始定义哦 有不清楚的地方,继续阅读后续章节。😸\n执行上下文栈 思考一下:javascript 中的程序是怎么执行的呢?顺序执行吗?\n实际上,javascript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,如变量提升、函数提升等。\n那么,这个“段”究竟是如何划分的呢?javascript 引擎遇到一段怎样的代码时才会做“准备工作” ?\n可执行代码 这就要说到 javascript 的可执行代码(executable code)的类型有哪些了?\n其实很简单,就有三种:全局代码、函数代码、eval 代码。\n举个 🌰 ,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”专业一点的说法就是“执行上下文(execution context)” 。\n执行上下文栈 接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢???\njavascript 引擎🎙:“我”创建了执行上下文栈(execution context stack, ecs)来管理执行上下文。\n为了模拟执行上下文,我们不妨定义执行上下文栈为一个数组,如下 :\necstack = []; 试想,当 javascript 开始要执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalcontext 表示它,并且只有当整个应用程序结束的时候,ecstack 才会被清空。也就是说,在程序结束之前,ecstack 最底部永远有个 globalcontext :\necstack = [ globalcontext ]; 可见,环境是很重要的,是程序设计语言所必须实现的。\r我们来看一下示例,现在 javascript 引擎遇到下面的这段代码了:\nfunction fun3() { console.log(\u0026#39;fun3\u0026#39;) } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); 当执行一个函数的时候,就会创建一个执行上下文,并且压入 ecs(执行上下文栈),当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了工作原理,让我们来看看如何处理上面这段代码:\n// 伪代码\r// fun1()\recstack.push(\u0026lt;fun1\u0026gt; functioncontext);\r// fun1 中竟然调用了 fun2 ,还要创建 fun2 的执行上下文\recstack.push(\u0026lt;fun2\u0026gt; functioncontext);\r// f**k, fun2 还调用了 fun3 ...\recstack.push(\u0026lt;fun3\u0026gt; functioncontext);\r// fun3 执行完毕\recstack.pop();\r// fun2 执行完毕\recstack.pop();\r// fun1 执行完毕\recstack.pop();\r// javascript 接着执行后续的代码,但是 ecstack 底层永远有个 globalcontext ,直到程序结束 okay ,趁热打铁,让我们来看一段《javascript 高级程序设计》中的示例:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f(); } checkscope(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 再来看另一段示例:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } checkscope()(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.pop();\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 上述两个示例的输出是一样的,都是 'local scope' (若不明白,回顾一下上个章节),但是他们的执行的过程却是有区别的。请好好体会一下其中的不同。\n是不是意犹未尽呢?为了更详细了解两个函数执行上的区别,我们在后续章节继续探究一下执行上下文到底包含了哪些内容。\n变量对象 在上个章节中讲到,当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n变量对象(variable object,vo); 作用域链(scope chain); this 这里,我们重点讲讲创建变量对象的过程。\n变量对象,是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。\n全局上下文 我们先了解一个概念 \u0026ndash; 全局对象。以下是 w3school 中的介绍:\n全局对象是预定义的对象,作为 javascript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。\n在顶层 javascript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定的变量和函数名都会作为该对象的属性来查询。\n例如,当 javascript 代码引用 parseint() 函数时,它引用的是全局对象的 parseint 属性。全局对象是作用域链的头,还意味着在顶层 javascript 代码中声明的所有变量都将成为全局对象的属性。\n下面让我们来看一些关于全局对象的具体示例吧。\n1、可能通过 this 引用,在客户端 javascript 中,全局对象就是 window 对象。\nconsole.log(this); 2、全局对象是由 object 构造函数实体化的一个对象。\nconsole.log(this instanceof object); 3、预定义了一堆,嗯,一大堆函数和属性。\n// 都能生效\rconsole.log(math.random());\rconsole.log(this.math.random()); 4、作为全局变量的宿主。\nvar a = 1;\rconsole.log(this.a); 5、客户端 javascript 中,全局对象有 window 对象指向自身。\nvar a = 1;\rconsole.log(window.a);\rthis.window.b = 2;\rconsole.log(this.b); 看,全局上下文中的变量对象就是全局对象呐!\n可以认为全局对象就是一级掌控者,凡是游离在全局上下文的变量也好、函数也好,都归它管。🔱\r函数上下文 在函数上下文中,我们用活动对象(activation object,ao)来表示变量对象。\n❓ 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎上实现的,不可在 javascript 环境中访问,只有当进行一个执行上下文中,这个执行上下文的变量对象才会被激活,而只有被激活的变量对象(即活动对象)上的各种属性才能被访问。\n❗ 未进入执行阶段之前,变量对象(vo)中的属性都不能访问 !但是进入执行阶段之后,变量对象(vo)转变为了活动对象(ao),里面的属性都能被访问了,然后开始进行执行阶段的操作。它们其实都是同一个对象,只是处于执行上下文的不同生命周期。\n静则 v ,动则 a 活动对象是在进入函数上下文时被创建的,它通过函数的 arguments 属性初始化。 arguments 属性值是 arguments 对象。\n执行过程 执行上下文的代码会分成两个阶段进行处理 \u0026ndash; 分析和执行:\n进入执行上下文; 代码执行。 进入执行上下文\n当进入执行上下文时,这时候还没有执行代码。\n变量对象会包括:\n函数的所有形参(如果是函数上下文) 由名称和对应值组成的一个变量对象的属性被创建 没有实参,属性值设为 undefined 函数声明 由名称和对应值(函数对象 function-object)组成一个变量对象属性被创建 如果变量对象已经存在相同名称的属性,则完全替换这个属性 变量声明 由名称和对应值(undefined)组成一个变量对象的属性被创建 如果变量对象跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 好吧,还是来看例子 🍩。\nfunction foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1); 在进入执行上下文后,这时候的 ao 如下:\nao = { // 进入上下文,活动对象创建\rarguments: {\r0: 1,\rlength: 1\r},\ra: 1, // 形参函数的 `arguments` 属性初始化,\rb: undefined, // 没有对应实参的属性值设为 `undefined`\rc: reference to function c() {}, // 函数声明\rd: undefined // 变量声明。..\r} 代码执行\n在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。\n依然使用上面的例子,当代码执行完后,这时候的 ao 是:\nao = {\rauguments: {\r0: 1,\rlength: 1\r},\ra: 1,\rb: 3,\rc: reference to function c(){},\rd: reference to functionsexpression \u0026#34;d\u0026#34;\r} 到这里,变量对象的创建过程就介绍完了,来个小结吧:\n全局上下文的变量对象初始化是全局对象; 函数上下文的变量对象初始化只包括 arguments 对象; 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始化的属性值; 在代码执行阶段,会再次修改变量对象的属性值。 作用域链 当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 是的,我们将在这个章节来看一下什么是作用域链。\n在上个章节中,我们知道,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。\n下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。\n函数创建 我们知道,在 javascript 中,函数的作用域在函数定义的时候就决定了。\n这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意, [[scope]] 并不代表完整的作用域链。\n举个例子:\nfunction foo() { function bar() { // ... } } 函数创建时,各自的 [[scope]] 为:\nfoo.[[scope]] = [\rglobalcontext.vo\r];\rbar.[[scope]] = [\rfoocontext.ao,\rglobalcontext.vo\r] 函数激活 当函数激活时,进入函数上下文 ,创建 vo/ao 后,就会将活动对象添加到作用域链的前端 。\n这时候执行正文的作用域链,我们命名为 scope :\nscope = [ao].concat([[scope]]);\r// 引自的 ao 为当前执行函数的变量对象,\r// [[scope]] 为其所有父变量对象的层级链\r// 如:上例中 bar 的完整作用域链为 [ao, foocontext.ao, globalcontext.vo] 至此,作用域链创建完毕。\n🌰 我们来看一个示例,结合之前讲的变量对象和执行上下文栈,来总结一下函数执行上下文中作用域链和变量对象的创建过程:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope() { var scope2 = \u0026#39;local scope\u0026#39;; return scope2; } checkscope(); 执行过程如下:\n1、 checkscope 函数被创建,保存作用域链到内部属性 [[scope]]\ncheckscope.[[scope]] = [\rglobalcontext.vo\r]; 2、执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 3、checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数 [[scope]] 属性创建作用域链\ncheckscopecontext = {\rscope: checkscope.[[scope]],\r} 4、第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope2: undefined\r},\rscope: checkscope.[[scope]]\r} 5、第三步:将活动对象压入 checkscope 作用域链顶端\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope2: undefined\r},\rscope: [ao, [[scope]]]\r} 6、准备工作做完,开始执行函数,随着函数的执行,修改 ao 的属性值\ncheckscopecontext = {\rao = {\rarguments: {\rlength: 0\r},\rscope2: \u0026#39;local scope\u0026#39;\r},\rscope: [ao, [[scope]]]\r} 7、查找到 scope2 的值 ,返回函数执行完毕,函数上下文从执行上下文栈中弹出\necstack = [\rglobalcontext\r]; 从 ecmascript 规范解读 this 好吧,这个 cd 的 this 😂\r当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 是的,我们将在这个章节来看一下什么是 this 。让我们从 ecmascript5 规范开始讲起,附上 ecmascript 5.1 规范地址:英文版、中文版 。okay,让我们开始了解规范吧。\nes(ecmascript)的类型分为语言类型和规范类型。\nes 语言类型是开发者直接使用 es 可以操作的,其实就是我们常说的 undefined, null, boolean, string, number 和 object 。\nes 规范类型相当于 meta-values (元类型),是用来用算法描述 es 语言结构和 es 语言类型的。规范类型包括:reference, list, completion, property descriptor, property identifier, lexical environment 和 environment record 。\n这都是什么东东 ❓\r没懂?没关系,我们只要知道在 es 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。今天我们要讲的重点便是其中的 reference 类型,它与 this 的指向有着密切的关联。\nreference 什么是 reference 呢?\nreference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。它是一个 specification type ,也就是“只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。\nreference 由三个部分组成:\nbase value ,属性所在的对象或者就是 environmentrecord ,其值只可能是 undefined, object, boolean, string, number 或 environmentrecord 中的一种; referenced name ,属性的名称; strict reference ,…… 来看个例子:\nvar foo = 1; // 对应的 reference 是: var fooreference = { base: environmentrecord, name: \u0026#39;foo\u0026#39;, strict: false } 又如:\nvar foo = { bar: function() { return this; } }; foo.bar(); // foo // bar 对应的 reference 是: var barreference = { base: foo, propertyname: \u0026#39;bar\u0026#39;, strict: false }; 而且规范中还提供了获取 reference 组成部分的方法,比如:\ngetbase ,返回 reference 的 base value ; ispropertyreference ,如果 base value 是一个对象,就返回 true ; getvalue ,用于从 reference 类型获取对应值。 我们来简单模拟 getvalue 的使用:\nvar foo = 1; var fooreference = { base: environmentrecord, name: \u0026#39;foo\u0026#39;, strict: false }; getvalue(foorefence); // 1 可以看到, getvalue 返回对象属性真正的值,而不再是一个 reference ,切记切记。\n如何确定 this 的值 我们说了那么多关于 reference 的事情,它跟本文的主题 this 有哪些关联呢?\n看规范 11.2.3 function calls:这里讲了当函数调用的时候,如何确定 this 的取值。让我们简单描述一下:\n计算 memberexpression 的结果赋值给 ref ; 判断 ref 是不是一个 reference 类型: 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref) ; 2.2 如果 ref 是 reference ,并且 base value 的值是 environmentrecord,那么 this 的值为 implicitthisvalue(ref) ; 2.3 如果 ref 不是 reference ,那么 this 的值为 undefined 。 下面,让我们一步一步来具体分析一下。\n1、计算 memberexpression 的结果赋值给 ref\n什么是 memberexpression ?规范 11.2 left-hand-side expressions 说明如下:\nmemberexpression :\nprimaryexpression ,原始表达式 functionexpression ,函数定义表达式 memberexpression[expression] ,属性访问表达式 memberexpression.indentifiername ,属性访问表达式 new memberexpression arguments ,对象创建表达式 来看一个例子:\nfunction foo() { console.log(this) } foo(); // memberexpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // memberexpression 是 foo() var foo = { bar: function() { return this; } } foo.bar(); // memberexpression 是 foo.bar 所以简单理解 memberexpression 其实就是 () 左边的部分。\n2、判断 ref 是不是一个 reference 类型\n关键就在于看规范是如何处理各种 memberexpression ,返回的结果是不是一个 reference 类型。来看一个示例:\nvar value = 1; var foo = { value: 2, bar: function() { return this.value; } } // e.g.1 console.log(foo.bar()); // 2 // e.g.2 console.log((foo.bar)()); // 2 // e.g.3 console.log((foo.bar = foo.bar)()); // 1 // e.g.4 console.log((false || foo.bar)()); // 1 // e.g.5 console.log((foo.bar, foo.bar)()); // 1 foo.bar()\n在 e.g.1 中,memberexpression 计算的结果是 foo.bar ,那么 foo.bar 是不是一个 reference 呢?\n查看规范 11.2.1 property accessors 得知该表达式返回了一个 reference 类型,其值为:\nvar reference = {\rbase: foo,\rname: \u0026#39;bar\u0026#39;,\rstrict: false\r}; 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref)\n该值是 reference 类型,那么 ispropertyreference(ref) 的结果是多少呢? base value 为 foo ,是一个对象,所以 ispropertyreference(ref) 返回 true 。\n此时,我们可以确定 this 的值了:\nthis = getbase(ref) // getbase 返回 reference 的 `base value` ,即 foo 所以,在这个例子中, this 的值就是 foo ,e.g.1 的结果就是 2 !\n好吧,依然云里雾里,看来真的有必要系统去阅读一遍规范……\r(foo.bar)()\n实际上, () 并没有对 memberexpression 进行计算,所以其实跟 e.g.1 的结果是一样的。\n(foo.bar = foo.bar)()\n看 e.g.3 ,有赋值操作符,返回的值不是 reference 类型。\n2.3 如果 ref 不是 reference ,那么 this 的值为 undefined\nthis 为 undefined ,非严格模式下, this 的值为 undefined 的时候,其值会被隐式转换为全局对象。\n(false || foo.bar)()\n看 e.g.4 ,逻辑与算法,返回的值不是 reference 类型, this 为 undefined 。\n(foo.bar, foo.bar)()\n看 e.g.5 ,逗号操作符,返回的值不是 reference 类型, this 为 undefined 。\n注意,以上 是在非严格模式下的结果,严格模式下因为 this 返回 undefined , e.g.3 会报错。\n………………\r最后,我们来看一种最普通的情况:\nfunction foo() { console.log(this); } foo(); memberexpression 是 foo ,解析标识符,查看规范 10.3.1 identifier resolution,会返回一个 reference 类型的值:\nvar fooreference = {\rbase: environmentrecord,\rname: \u0026#39;foo\u0026#39;,\rstrict: false\r} 2.1 如果 ref 是 reference ,并且 ispropertyreference(ref) 是 true ,那么 this 的值为 getbase(ref)\n因为 base value 是 environmentrecord ,并不是一个 object 类型, ispropertyreference(ref) 的结果为 false ,继续判断\n2.2 如果 ref 是 reference ,并且 base value 的值是 environmentrecord,那么 this 的值为 implicitthisvalue(ref)\nbase value 正是 environmentrecord ,调用 implicitthisvalue(ref) ,查看规范 10.2.1.1.6,implicitthisvalue 方法的介绍:该函数始终返回 undefined 。\n所以最后 this 的值就是 undefined 。\n🍨 最后 尽管我们可以简单地理解 this 为调用函数的对象,如果是这样的话,如何解释下面这个例子呢?\nvar value = 1; var foo = { value: 2, bar: function () { return this.value; } } console.log((false || foo.bar)()); // 1 此外,又如何确定调用函数的对象是谁呢?\n在写这个章节之初,我(冴羽)就面临着这些问题,最后还是放弃从多个情形下给大家讲解 this 指向的思路,而是追根溯源地从 es 规范讲解 this 的指向。尽管从这个角度写起来和读起来都比较吃力,但是一旦多读几遍,明白原理,绝对会给你一个全新的视角看待 this 。而你也就能明白,尽管 foo() 和 (foo.bar = foo.bar)() 最后结果都指向了 undefined ,但是两者从规范的角度上却有着本质的区别。\n此篇讲解执行上下文的 this ,即便不是很理解此篇的内容,依然不影响大家了解执行上下文这个主题下其他的内容。所以,依然可以安心的看下一篇文章。\n好吧,其实在实际应用中,你大概率不会遇到太多关于 `this` 的刁难,常见的应用场景就那几个。一切的一切都要为了解决实际问题而服务。\r我们必须明白,规范或参考书之类的东西,晦涩繁琐几乎是它们的特定属性,也就是说,时间多就通读一下学习,后续逐点参考即可。\n执行上下文 当 javascript 代码执行一段可执行代码(excutable code)时,会创建对应的执行上下文(execution context)。\n对于每个执行上下文,都有三个重要属性:\n- 变量对象(variable object,vo);\r- 作用域链(scope chain);\r- this 让我们来看一段《javascript 高级程序设计》中的示例:\n// e.g.1 var scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f(); } checkscope(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 再来看另一段示例:\n// e.g.2 var scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } checkscope()(); ecstack.push(\u0026lt;checkscope\u0026gt; functioncontext);\recstack.pop();\recstack.push(\u0026lt;f\u0026gt; functioncontext);\recstack.pop(); 上述两段代码都会打印 'local scope' ,虽然执行结果一样,但执行过程却有区别(其执行上下文栈的变化不一样),我们在这个章节就详细的解析执行上下文栈和执行上下文的具体变化过程。\n以 e.g.1 的示例来说,其执行过程如下 :\n1、执行全局代码,创建全局执行上下文,全局上下文被存款利率执行上下文栈\necstack = [\rglobalcontext\r]; 2、全局上下文初始化\nglobalcontext = {\rvo: [global],\rscope: [globalcontext.vo],\rthis: globalcontext.vo\r} 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性 [[scope]]\ncheckscope.[[scope]] = [\rglobalcontext.vo\r]; 3、执行 checkscope 函数,创建 checkscope 函数执行上下文, checkscope 函数执行上下文被存款利率执行上下文栈\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 4、 checkscope 函数执行上下文初始化\n复制函数 [[scope]] 属性创建作用域链; 用 arguments 创建活动对象; 初始化活动对象,即加入形参、函数声明、变量声明; 将活动对象压入 checkscope 作用域链顶端。 同时 f 函数被创建,保存作用域到 f 函数的内部属性[[scope]]\ncheckscopecontext = {\rao: {\rarguments: {\rlength: 0\r},\rscope: undefined,\rf: reference to function f(){}\r},\rscope: [ao, globalcontext.vo],\rthis: undefined\r} 5、执行 f 函数,创建 f 函数执行上下文, f 函数执行上下文被压入上下文栈\necstack = [\rfcontext,\rcheckscopecontext,\rglobalcontext\r]; 6、 f 函数执行上下文初始化,以下跟第 4 步相同\n复制函数 [[scope]] 属性创建作用域链; 用 arguments 创建活动对象; 初始化活动对象,即加入形参、函数声明、变量声明; 将活动对象压入 f 作用域链顶端。 fcontext = {\rao: {\rarguments: {\rlength: 0\r}\r},\rscope: [ao, checkscopecontext.ao, globalcontext.vo],\rthis: undefined\r} 7、 f 函数执行,沿着作用域链查找 scope 值,返回 scope 值\n8、 f 函数执行完毕, f 函数上下文从执行上下文栈中弹出\necstack = [\rcheckscopecontext,\rglobalcontext\r]; 9、 checkscope 函数执行完毕, checkscope 执行上下文从执行上下文栈中弹出\necstack = [\rglobalcontext\r]; okay,就这样。\n对于 e.g.2 ,大家自己尝试模拟它的执行过程吧。\n好的吧,要返工了 😂\r深入闭包 什么是闭包?\nthis combination of a function object and a scope (a set of variable bindings) in which the function\u0026rsquo;s variables are resolved is called a closure in the computer science literature.\nes 中,闭包指的是:\n从理论角度:所有的函数。因为它们都在创建的时候就将上下文的数据保存起来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于在访问自由变量,这个时候使用最外层的作用域。 从实践角度:以下函数才算是闭包: 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回), 在代码中引用了自由变量 \u0026gt; 闭包是指那些能够访问自由变量的函数;自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。\n\u0026gt; 闭包 = 函数 + 函数能够访问的自由变量\n我们在实际应用中,基本上只关注实践角度的闭包 🤗\n来看看我们的老朋友:\nvar scope = \u0026#34;global scope\u0026#34;; function checkscope(){ var scope = \u0026#34;local scope\u0026#34;; function f(){ return scope; } return f; } var foo = checkscope(); foo(); 让我们再次分析一下这段代码中执行上下文栈和执行上下文的变化情况,其简要的执行过程如下:\n进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈; 全局执行上下文初始化; 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈; checkscope 执行上下文初始化,创建变量对象、作用域链、this 等; checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出; 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈; f 执行上下文初始化,创建变量对象、作用域链、this 等; f 函数执行完毕,f 函数上下文从执行上下文栈中弹出。 你可能会有一个疑问 - 当 f 函数执行的时候, checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢 ❓\n当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:\nfcontext = {\rscope: [ao, checkscopecontext.ao, globalcontext.vo],\r} 对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopecontext.ao 的值,说明当 f 函数引用了 checkscopecontext.ao 中的值的时候,即使 checkscopecontext 被销毁了,但是 javascript 依然会让 checkscopecontext.ao 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 javascript 做到了这一点,从而实现了闭包这个概念。\n所以,让我们再看一遍实践角度上闭包的定义:\n即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回), 在代码中引用了自由变量。 看,没什么太多玄妙的东西,只是 js 底层做了某些实现。同样的代码,在 php 中,就会报错,因为在 php 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。\n接下来,看这道刷题必刷,面试必考的闭包题 💯\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2](); 答案是都是 3,让我们分析一下原因。\n当执行到 data[0] 函数之前,此时全局上下文的 vo 为:\nglobalcontext = {\rvo: {\rdata: [...],\ri: 3\r}\r} 当执行 data[0] 函数的时候,data[0] 函数的作用域链为:\ndata[0]context = {\rscope: [ao, globalcontext.vo]\r} data[0]context 的 ao 并没有 i 值,所以会从 globalcontext.vo 中查找,i 为 3,所以打印的结果就是 3。\ndata[1] 和 data[2] 是一样的道理。\n所以让我们改成闭包看看:😈\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2](); 当执行到 data[0] 函数之前,此时全局上下文的 vo 为:\nglobalcontext = {\rvo: {\rdata: [...],\ri: 3\r}\r} 跟没改之前一模一样 ❓ 不然 ❗️\n当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:\ndata[0]context = {\rscope: [ao, 匿名函数 context.ao, globalcontext.vo]\r} 匿名函数执行上下文的 ao 为:\n匿名函数 context = {\rao: {\rarguments: {\r0: 0,\rlength: 1\r},\ri: 0\r}\r} data[0]context 的 ao 并没有 i 值,所以会沿着作用域链从匿名函数 context.ao 中查找,这时候就会找 i 为 0,找到了就不会往 globalcontext.vo 中查找了,即使 globalcontext.vo 也有 i 的值(值为 3),所以打印的结果就是 0。\ndata[1] 和 data[2] 是一样的道理。\n参数按值传递 ecmascript 中所有函数的参数都是按值传递的。\n\u0026gt; 按值传递,就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。\n什么是引用传递 ❓\n当值是一个复杂的数据结构的时候,就按引用传递,即传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。\n其实,就是一个是变量值,一个是地址值(引用内存地址),都是值啦 👻\rcall、apply、bind 的应用 \u0026gt; 该章节为附加章节,旨在说明其使用场景 ,可以参考 js 中的 call、apply、bind 方法详解 和 javascript 中 call、apply、bind 的区别 。\nbind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。\napply \u0026amp; call 在 javascript 中, call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。\njavascript 的一大特点是,函数存在 =定义时上下文 和 =运行时上下文 以及 =上下文是可以改变的 这样的概念。\nfunction fruits() {} fruits.prototype = { color: \u0026#39;red\u0026#39;, say: function() { console.log(\u0026#39;my color is \u0026#39; + this.color); } } var apple = new fruits; apple.say(); // my color is red 如果我们有一个对象 banana = { color: 'yellow' } ,但我们不想对它重新定义 say 方法,那么我们就可以通过 call 或 apply 来调用 apple 的 say 方法,如下:\nbanana = { color: \u0026#39;yellow\u0026#39; } apple.say.call(banana); // my color is yellow apple.say.apply(banana); // my color is yellow = 这里 apply 的语义就更符合人们的直觉,表示把一个对象的方法应用在另一个对象上;相对来说, call 就比较反直觉(用 called 都比 call 强)!好的吧,js 就是个坑货 😅*\n那么,apply 和 call 有什么区别呢?\n它们的作用完全一样,只是接受参数的方式不太一样。老规矩,上例子 🌰\nvar func = function(arg1, arg2) { // ... }; 就可以通过如下方式来调用:\nfunc.call(this, arg1, arg2); func.apply(this, [arg1, arg2]); 其中, this 就是想指定的上下文对象(可以是任何一个 javascript 对象), call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里面。\n= 其实这个很好记忆,只需要知道 apply 和 array 都是 5 个字母,所以其参数要放在数组中,就可以了。*\n一个相对常遇到的场景 - 类(伪)数组使用数组的方法。\n我们来几个面试中可能遇到的问题:\neg.1 定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:\nfunction log(msg) { console.log(msg); } log(1); // 1 log(1, 2); // 1 2 接下来的要求是给每一个 log 消息添加一个 '(app)' 的前缀,比如:\nlog(\u0026#34;hello world\u0026#34;); // (app)hello world 如何做比较优雅呢?我们知道 arguments 参数是个伪数组,通过 array.prototype.slice.call 转化为标准数组,即可使用数组的方法 unshift ,如下:\nfunction log() { var args = array.protype.slice.call(arguments); args.unshift(\u0026#39;(app)\u0026#39;); console.log.apply(console, args); } = 哎,怎么说呢,原理的理解是非常重要的,但学以致用,了解怎么用原理的具体应用才能真正的落地!*\nbind 在讨论 bind() 方法之前,我们先来看一道题目,如下:\nvar altwrite = document.write; altwrite(\u0026#39;hello\u0026#39;); 结果: uncaught typeerror: illegal invocation 怎么样,你答对了吗?为什么会是这样的结果呢?\n原来 altwrite() 函数改变 this 的指向 global 或 window 对象,导致执行时提示非法调用异常,正确的方案就是使用 bind() 方法,使其重新指向 document ,如下:\naltwrite.bind(document)(\u0026#39;hello\u0026#39;); // 或者 altwrite.call(document, \u0026#39;hello\u0026#39;) = 在 react 中,你会经常碰到 bind \u0026hellip;*\nbind() 都有什么用呢???\n_1. 绑定函数\nbind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。\n常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望 this 指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用 bind() 方法能够很漂亮的解决这个问题。\nthis.num = 9; var mymodule = { num: 81, getnum: function() { console.log(this.num); } }; mymodule.getnum(); // 81 var getnum = mymodule.getnum; getnum(); // 9, 因为在这个例子中,\u0026#34;this\u0026#34;指向全局对象 var boundgetnum = getnum.bind(mymodule); boundgetnum(); // 81 mdn 的解释是: bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind() 方法的第一个参数作为 this ,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的能数来调用原函数。\n= 是不是也是一种代理 proxy 呢 ❓*\n具体是个什么东东呢???\n在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:\nvar foo = { bar : 1, eventbind: function(){ var _this = this; $(\u0026#39;.someclass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* act on the event */ console.log(_this.bar); //1 }); } } 由于 javascript 特有的机制,上下文环境在 eventbind:function(){ } 过渡到 $('.someclass').on('click',function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:\nvar foo = { bar : 1, eventbind: function(){ $(\u0026#39;.someclass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* act on the event */ console.log(this.bar); //1 }.bind(this)); } } 在上述代码里,bind() 创建了一个函数,当这个 click 事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用 bind() 时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:\nvar bar = function(){ console.log(this.x); } var foo = { x:3 } bar(); // undefined var func = bar.bind(foo); func(); // 3 这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。\n_2. 偏函数(partial functions)\nbind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。\nfunction list() { return array.prototype.slice.call(arguments); } var list1 = list(1, 2, 3); // [1, 2, 3] // 预定义参数 37 var leadingthirtysevenlist = list.bind(undefined, 37); var list2 = leadingthirtysevenlist(); // [37] var list3 = leadingthirtysevenlist(1, 2, 3); // [37, 1, 2, 3] = 知道这些就行了,其他的不常用……*\napply call bind 的比较 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:\nvar obj = { x: 81, }; var foo = { getx: function() { return this.x; } } console.log(foo.getx.bind(obj)()); //81 console.log(foo.getx.call(obj)); //81 console.log(foo.getx.apply(obj)); //81 三个输出的都是 81,但是注意看使用 bind() 方法的,他后面多了对括号。\n也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。\n最后,来个小结吧:\napply 、 call 、bind 三者都是用来改变函数的this对象的指向的; apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文; apply 、 call 、bind 三者都可以利用后续参数传参; bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 call 和 apply 的模拟实现 call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。\n举个例子:\nvar foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1 注意两点:\ncall 改变了 this 的指向,指向到 foo bar 函数执行了 如何实现模拟一下 call 呢???思考一下 😎\n……\nbind 的模拟实现 bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 mdn )\n由此我们可以首先得出 bind 函数的两个特点:\n返回一个函数 可以传入参数 如何实现模拟一下 bind 呢???思考一下 😎\n……\nnew 的模拟实现 ……\n类数组对象与 arguments 类数组对象 什么是类数组对象呢?\u0026ndash; 拥有一个 length 属性和若干索引属性的对象。像这样:\nvar array = [\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;sex\u0026#39;]; var arraylike = { 0: \u0026#39;name\u0026#39;, 1: \u0026#39;age\u0026#39;, 2: \u0026#39;sex\u0026#39;, length: 3 } 类数组对象在读写、获取长度、遍历三个方基本是相同的,但类数组对象可以使用数组的一些方法会报错,如:\narraylike.push(\u0026#39;4\u0026#39;); // ❌ arraylike.push is not a function 所以终归还是类数组呐…… 错付了呀 😭\n调用数组方法 如果类数组就是任性的想用数组的方法怎么办呢?可以用 function.call 间接调用,如下:\nvar arraylike = {0: \u0026#39;name\u0026#39;, 1: \u0026#39;age\u0026#39;, 2: \u0026#39;sex\u0026#39;, length: 3 } array.prototype.join.call(arraylike, \u0026#39;\u0026amp;\u0026#39;); // name\u0026amp;age\u0026amp;sex // 类数组转数组 // 1. slice array.prototype.slice.call(arraylike); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 2. splice array.prototype.splice.call(arraylike, 0); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 3. es6 array.from array.from(arraylike); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] // 4. apply array.prototype.concat.apply([], arraylike) array.prototype.map.call(arraylike, function(item){ return item.touppercase(); }); // [\u0026#34;name\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;sex\u0026#34;] 看, call 改变了当前函数调用者的 this 指向到了 arraylike 。\narguments 对象 arguments 对象就是一个类数组对象!\narguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 arguments 对象。\n翠花,上 🌰\nfunction foo(name, age, sex) { console.log(arguments); } foo(\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;sex\u0026#39;) 打印结果如下:\nlength 属性\n其中,arguments 对象的 length 属性,表示 实参的长度\nfunction foo(b, c, d){ console.log(\u0026#34;实参的长度为:\u0026#34; + arguments.length) } console.log(\u0026#34;形参的长度为:\u0026#34; + foo.length) foo(1) // 形参的长度为:3 // 实参的长度为:1 callee 属性\narguments 对象的 callee 属性,通过它可以调用函数自身。\n讲个闭包经典面试题使用 callee 的解决方法:\nvar data = []; for (var i = 0; i \u0026lt; 3; i++) { (data[i] = function () { console.log(arguments.callee.i) }).i = i; } data[0](); // 0 data[1](); // 1 data[2](); // 2 arguments 和对应参数的绑定\n传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享。\n* 如果是在严格模式下,实参和 arguments 是不会共享的。\nfunction foo(name, age, sex, hobbit) { console.log(name, arguments[0]); // name name // 改变形参 name = \u0026#39;new name\u0026#39;; console.log(name, arguments[0]); // new name new name // 改变 arguments arguments[1] = \u0026#39;new age\u0026#39;; console.log(age, arguments[1]); // new age new age // 测试未传入的是否会绑定 console.log(sex); // undefined sex = \u0026#39;new sex\u0026#39;; console.log(sex, arguments[2]); // new sex undefined arguments[3] = \u0026#39;new hobbit\u0026#39;; console.log(hobbit, arguments[3]); // undefined new hobbit } foo(\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;) 传递参数\n将参数从一个函数传递到另一个函数,如下:\n// 使用 apply 将 foo 的参数传递给 bar function foo() { bar.apply(this, arguments); } function bar(a, b, c) { console.log(a, b, c); } foo(1, 2, 3) // 1 2 3 使用 es6 的 ... 运算符,我们可以把 arguments 轻松转成数组。\n结语 好的吧,好像学到了一些东西,又好像什么都没学到 🤪 ,后续还有一些篇章,但对我来说已经没有多大吸引力,有兴趣的小伙伴可以参阅 冴羽写博客 javascript 深入系列 。\n","date":"2022-07-25","permalink":"https://loveminimal.github.io/posts/deep-js/","summary":"\u003cp\u003e🔔 摘录自 \u003ca href=\"https://github.com/mqyqingfeng/Blog\"\u003e冴羽写博客之深入系列\u003c/a\u003e ,写的很不错。\u003c/p\u003e","title":"冴羽写博客之深入系列"},{"content":"健身训练的项目有很多,而有一些运动是大多数人所热爱的。跑步 🏃 就是一项大众所熟悉的,门槛比较低的运动项目,无论男女老少都可以跑起来。\n那么,每天坚持跑步,身材可以瘦下来吗?可以 ❗ 跑步的可以实现的目标很多,想要达到减肥的目的,需要注意以下一些方面。\n跑步速度 跑步可以分为快跑跟慢跑,快跑属于无氧运动,是无法持续坚持的运动,容易力竭并且出现小粗腿,这并不是你跑步的初衷。\n而慢跑属于可持续进行的训练,可以达到瘦腿塑形的效果。因此,跑步的时候你的速度控制为 6-9km/h 的速度即可。\n如果你的体重基数太大,很难坚持下来,建议你可以慢跑结合快走,坚持一段时间后心肺功能会加强,运动能力会提升,这个时候尝试慢跑训练,你会发现持久力有所提升。\n跑步时长 慢跑一小时大概可以消耗 500 大卡 的热量,而一斤脂肪的热量是 3900 大卡,如果你想要减掉 1 斤脂肪,那么要跑步将近 8 小时,如果你想要减掉 5 斤脂肪,那么你需要跑 40 小时左右。\n任重而道远!!!\r如果你每天跑步 1 小时,那么要坚持 40 天左右才能减脂 5 斤。所以,跑步减肥是一项需要坚持才能有所成效的事情。\n拉伸训练 跑步后肌肉会出现充血问题,很多人误以为是肌肉腿,其实,跑步后进行充分的拉伸放松腿部肌群,这样双腿也会越来越细长好看,跑步后肌肉酸疼问题也会减轻。\n所以,跑步后不要马上坐着,一定要针对腿部肌群进行拉伸,这样还能提升双腿的灵活性跟柔软度。\n饮食管理 跑步的时候,如果你能结合饮食管理,那么减肥效果也会翻倍。如果你胡吃海喝没有管理好饮食,那么你每天多摄入的热量可能比你跑步消耗的热量更高,比如:一份薯条的热量有 320 大卡,一杯可乐的热量是 180 大卡,一个炸鸡汉堡的热量达到了 400 大卡以上。\n只有管理好饮食,远离各种高热量、过度加工的食物,多吃一些低热量、天然的食物,才能控制卡路里摄入,提升减肥效率。\n参考链接 https://new.qq.com/omn/20220718/20220718a09w9900.html ","date":"2022-07-22","permalink":"https://loveminimal.github.io/posts/running/","summary":"\u003cp\u003e健身训练的项目有很多,而有一些运动是大多数人所热爱的。跑步 🏃 就是一项大众所熟悉的,门槛比较低的运动项目,无论男女老少都可以跑起来。\u003c/p\u003e\n\u003cp\u003e那么,每天坚持跑步,身材可以瘦下来吗?可以 ❗ 跑步的可以实现的目标很多,想要达到减肥的目的,需要注意以下一些方面。\u003c/p\u003e","title":"跑步"},{"content":"为什么需要防抖和节流?它们是什么?有什么区别?适用场景是什么?\n简介 防抖与节流,主要用来控制事件处理函数的调用频率 ❗\n在进行窗口的缩放(resize)、滚动(scroll),输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器负担,导致用户体验糟糕。\n试想,你的事件处理函数是异步的,每次调用都会进行大量的数据检索……\r如何控制调用频率呢?防抖和节流就是做这个的。\n防抖 什么是防抖(debounce)?当持续触发事件时,在设定时间段🕐内没有再触发事件,事件处理函数才会执行一次;若在设定的时间段🕜内,又一次触发事件,就重新开始延时。\n如下图,持续触发 scroll 事件时,并不执行 handle 函数,当 1000 毫秒内没有触发 scroll 事件时,才会延时触发 scroll 事件。\n……\n节流 什么是节流(throttle)?当持续触发事件时,保证一定时间段内只调用一次事件处理函数。如下图,持续触发 scroll 事件时,并不立即执行 handle 函数,每隔 1000 毫秒才会执行一次 handle 函数。\n……\n区别 不能看出,防抖的控频原理在于持续触发事件时,在设定的时间段内不再触发事件,才会调用一次执行函数;而节流的控频原理在于持续触发事件时,每隔设定的时间段才会调用一次执行函数。\n当然,实际操作中,我们会对二者做一些相应的优化处理,但是从本质上来说,它们就是如此。\n参考链接 https://www.cnblogs.com/momo798/p/9177767.html https://www.cnblogs.com/youma/p/10559331.html https://segmentfault.com/a/1190000012066399 https://segmentfault.com/a/1190000018428170 https://zhuanlan.zhihu.com/p/38313717 https://juejin.cn/post/6844903669389885453 https://redd.one/blog/debounce-vs-throttle https://webdesign.tutsplus.com/tutorials/javascript-debounce-and-throttle--cms-36783 ","date":"2022-07-21","permalink":"https://loveminimal.github.io/posts/debounce-and-throttle/","summary":"\u003cp\u003e为什么需要防抖和节流?它们是什么?有什么区别?适用场景是什么?\u003c/p\u003e","title":"防抖与节流"},{"content":"我理解中的极简主义,核心“断、舍、离”,理念“少即是多”,重点“有序”。\n极简主义是一种生活和思考模式,实现它的方式有很多, gtd 是其中一种,也是目前我感觉比较好的一种。它很有趣,但并不容易坚持,毕竟形成一种全新的生活习惯是一个长期的过程。开始很容易,难在持续。\n真正的“少” 为什么强调“少”?是因为,日常生活中往往充斥了过多不必要的东西,它们毫无意义,却占用了过多的时间,消耗掉过多的精力。所以,我们需要强调“少”,一般情况下,这样做总是没有错的。但是也不要走入另一个极端,我们应该始终明白,之所以强调“少”,是因为“过多了”,而不是因为要“足够少”。切忌为了“少”而少,适度、平衡才是我们真正的追求。\n现实生活中没有遇到过,网上到是见了不少“极简主义”的拥趸者,他们往往大肆鼓吹宣扬过度的“少”,有甚者还以清空所有家具为荣。也许,他们是真正的享受吧,但我并认为这是可持续的。如同,我认为吃面要用筷子而不是勺子,喝汤要用勺子而不是筷子一样。再次强调,真正的“少”,归于平衡、有序,而不是“空”。\n极简主义的目标在于形成一个“最少需要”,你可拥有很多,但我们说的“最少”是一个临界,比“最少”再少一些,就会带来不便,比“最少”再多一点,虽然不会带来不便,但可能产生毫无意义的消耗。“最少需要”的观念是也只能是一个指导,因人而异,要根据自己当前的现状去调整,持续性的调整,以达到一个相对舒适和谐的生活节奏。\n“最少需要”是动态的,其内容在不断地更新,但原则是一致的 \u0026ndash; 但(仅)取所需。精简是一个过程,这过程本身就是一种享受。结果是重要的,但结果也只不过是阶段性的一个时刻,是水到渠成,是自然而然。\n有序 gtd 之所以是一种优秀的极简主义实现方式,在于其“应收尽收,拒绝悬而未决”的品质。它使得你生活中的事务更加系统化、层次化,有主次、分轻重缓急,非常有助于“减少”不必要的消耗。你知道自己当下最适合做的事情是什么,焦虑自然而然地远离你。因为我们焦虑的原因往往不在于要做的事很多,而在于不知道要做什么。\n安全感和掌控来自“有序”,“有序”是系统性的收集、组织和管理的结果。\n未完待续……\n✔️ 《再谈“个人管理”》\n","date":"2022-07-20","permalink":"https://loveminimal.github.io/posts/minimalist-lifestyle/","summary":"\u003cp\u003e我理解中的极简主义,核心“断、舍、离”,理念“少即是多”,重点“有序”。\u003c/p\u003e\n\u003cp\u003e极简主义是一种生活和思考模式,实现它的方式有很多, \u003ca href=\"../get-things-done/\"\u003eGTD\u003c/a\u003e 是其中一种,也是目前我感觉比较好的一种。它很有趣,但并不容易坚持,毕竟形成一种全新的生活习惯是一个长期的过程。开始很容易,难在持续。\u003c/p\u003e","title":"极简主义生活"},{"content":"博客系统迁移好多次了,多少次?详见 一场“疲惫”的主题制作之旅 。域名啊、博客文章的层级结构啊,都难免存在差异。这就导致了一个问题,之前在别的网站发布的一些页面链接就成了“死链”。虽然,可以简单地重写向到 404 ,体验却不好,最好是可以重定向到变化后的页面。\n周末无聊,简单实现了一下,做个记录。\n如何重定向 之前的链接都是 https://ovirgo.com/xxx-yyy-zzz.html 格式的链接,现在要使之重定向到 https://ovirgo.com/posts/xxx-yyy-zzz 到这样的链接地址。\n修改服务器端 nginx 的相关配置,比如 /etc/nginx/conf.d/xxx.conf (当前主机为 ubuntu server ,其他发型版可能有区别)。\nlocation / {\r# ...\rif (!-e $request_filename) {\rrewrite ((\\w*=)*\\w*)\\.html /posts/$1 last;\rbreak;\r}\r# ...\r} $1 是什么?就是前面正则表达式的匹配的第一个捕获分组。\n当请求资源不存在时,重定向到新的路径。对,就是 rewrite 了。\n如果,你还不了解,或是很长时间没有使用正则表达式了,可以阅读一下另一篇博文 \u0026ndash; 正则表达式,附一个很不错的 正则表达式测试网站 。\nnginx rewrite 访问重写 rewrite 是 nginx http 请求处理过程中的一个重要功能,它是以模块的形式存在于代码中的,其功能是对用户请求的 uri 进行 pcre 正则重写,然后返回 30x 重定向跳转或按条件执行相关配置。\nrewrite 模块内置了类似脚本语言的 set、if、break、return 配置指令,通过这些指令,用户可以在 http 请求处理过程中对 uri 进行更灵活的操作控制。rewrite 模块提供的指令可以分为两类:\n标准配置指令(只是对指定的操作进行相应的操作控制); 脚本指令(可以在 http 指令域内以类似脚本编程的形式进行编写)。 标准配置指令 这里我们只介绍与本次修改相关的配置指令。\n名称 rewrite 指令 指令 rewrite 作用域 server, location 指令说明 对用户的 uri 用正则表达式的方式进行重写,并跳转到新的 uri 其语法格式如下:\nrewrite regex replacement [flag];\r# - regex 是 pcre 语法格式的正则表达式\r# - replacement 是重写 uri 的改写规则\r# - flag 是执行该条重写指令后的操作控制符 其中,对于 replacement 来说,当改写规则以 http://、https:// 或 $scheme 开头时,nginx 重写该语句后将停止执行后续任务,并将改写后的 uri 跳转返回客户端。\n对于,flag ,作为操作符有如下 4 种:\nlast 执行完当前重写规则跳转到新的 uri 后继续执行后续操作; break 执行完当前重写规则跳转到新的 uri 后不再执行后续操作,不影响用户浏览器 uri 显示; redirect 返回响应码 302 的临时重定向,返回内容是重定向 uri 的内容,但浏览器网址仍为请求时的 uri ; permanent 返回响应状态码 301 的永久重定向,返回内容是重写向 uri 的内容,浏览器网址变为重定向的 uri 。 脚本指令 这一块目前没有涉及,用的到的时候再说吧。(但取所需 😂)\n……\nfaq 如果,你要实验重写规则,你可能会遇到和我一样的问题,比如说,当你把 flag 为 permanent 时,你可能重定向了某个 uri,当你实验一次之后,发现后续再修改也不起作用了,咦,困扰。\n设置了 permanent ,被永久重定向了,你需要先清理浏览器缓存,再进行后续的操作。\n参考链接 https://www.w3schools.cn/nginx/nginx_command_rewrite.asp ","date":"2022-07-16","permalink":"https://loveminimal.github.io/posts/nginx-rewrite/","summary":"\u003cp\u003e博客系统迁移好多次了,多少次?详见 \u003ca href=\"../a-theme-making-journey\"\u003e一场“疲惫”的主题制作之旅\u003c/a\u003e 。域名啊、博客文章的层级结构啊,都难免存在差异。这就导致了一个问题,之前在别的网站发布的一些页面链接就成了“死链”。虽然,可以简单地重写向到 404 ,体验却不好,最好是可以重定向到变化后的页面。\u003c/p\u003e\n\u003cp\u003e周末无聊,简单实现了一下,做个记录。\u003c/p\u003e","title":"nginx 重定向"},{"content":"👉 推荐一个不错的网站 - c 语言中文网\n关于进制 在计算机内部,数据都是以二进制的形式存储的。\n进制是什么?\n进制(system of numeration),是人为定义的带进位的计数方法。对于任何一种进制(如 x 进制),就表示每一位上的数运算时都是逢 x 进一位。比如,我们常用的十进制,基数为 10,数码由 0-9 组成,计数规律逢十进一。\n人类天然选择了十进制,思考一下这是为什么?\n同理,二进制的基数为 2,数码由 0-1 组成,计数规律逢二进一。八进制的基数为 8,数码由 0~7 组成,计数规律逢八进一。十六进制的基数为 16,数码由 0~9a~f 组成,计数规律逢十六进一。\n进制转换 n 进制转十进制 二进制、八进制和十六进制向十进制转换都非常简单,就是 “按权相加”。所谓“权”,即“位权”。\n假设当前数字是 n 进制,那么:\n对于整数部分,从右往左看,第 i 位的位权等于 ni-1 ; 对于小数部分,恰好相反,要从左往右看,第 j 位的位权为 n-j 然而,将十进制转换为其它进制时比较复杂,整数部分和小数部分的算法不一样。\n十进制转 n 进制 十进制整数转换为 n 进制整数采用 “除 n 取余,逆序排列” 法。如下图:\n十进制数字 36926 转换成八进制的结果为 110076 。\n十进制小数转换成 n 进制小数采用 “乘 n 取整,顺序排列” 法。如下图:\n十进制小数 0.6875 转换成二进制小数的结果为 0.1011 。\n二进制和八进制、十六进制的转换 从上文,我们知道,以十进制为中转,可以完成所有进制的转换。但由于某种必然,将二进制转换为八进制和十六进制时有更加简洁的方法。\n1. 二进制整数和八进制整数之间的转换\n二进制整数转换为八进制整数时,每三位二进制数字转换为一位八进制数字,运算的顺序从低位向高位依次进行,高位不足三位用零补齐。\n二进制整数 1110111100 转换为八进制的结果为 1674 。\n八进制整数转换为二进制整数时,思路是相反的,每一位八进制数字转换为三位二进制数字,运算的顺序也是从低位向高位依次进行。\n八进制整数 2743 转换为二进制的结果为 10111100011。\n2. 二进制整数和十六进制整数之间的转换\n二进制整数转换为十六进制整数时,每四位二进制数字转换为一位十六进制数字,运算的顺序是从低位向高位依次进行,高位不足四位用零补齐。\n二进制整数 10 1101 0101 1100 转换为十六进制的结果为 2d5c。\n十六进制整数转换为二进制整数时,思路是相反的,每一位十六进制数字转换为四位二进制数字,运算的顺序也是从低位向高位依次进行。\n十六进制整数 a5d6 转换为二进制的结果为 1010 0101 1101 0110。\n实际工作中,二进制、八进制、十六进制之间几乎不会涉及小数的转换,八进制和十六进制之间也极少直接转换。\n数据存储 计算机要处理的信息是多种多样的,如数字、文字、符号、图形、视音频等,这些信息在人们的眼里是不同的。但对于计算机来说,它们在内存中都是一样的,皆 以二进制的形式来表示 。\n在运行时,数据和指令都要载入到内存中才可以。内存依托于内存条,后者是一个非常精密的部件,包含了上亿个电子元器件。这些元器件,实际上就是电路,依据电路电压的不同(断电 0v、通电5v)状态,表示为 0 和 1 。\n看,物理世界与数字世界连接了。🧲 以此为基点,我们可以模拟整个宇宙了。\r一个元器件为 1 bit ,表现力太小了,一般情况下,我们将 8 个元器件看做一个单位 (8 bit = 1 byte),这样每个单位就有 28 = 256 种表示。\n答前所问,人类天然选择了十进制,计算机则天然选择了二进制,因为人类有十根手指,而计算机只有两种状态。(可以如此认为 😸)\n计算机以二进制的形式来存储数据,它只认识 0 和 1 两个数字,我们在屏幕上看到的文字,在存储之前都被转换成了二进制,在显示时也要根据二进制找到对应的字符。\n可想而知,特定的文字必然对应着固定的二进制,否则在转换时将发生混乱。那么, 怎样将文字与二进制对应起来呢?\n这就需要有一套规范,计算机公司和软件开发者都必须遵守,这样的一套规范就称为字符集(character set)或者字符编码(character encoding)。\n我们将在 字符集和字符编码 中认识它们。\n参考链接 http://c.biancheng.net/view/1724.html https://www.interviewcake.com/article/python/data-structures-coding-interview?course=fc1\u0026amp;section=algorithmic-thinking ","date":"2022-07-15","permalink":"https://loveminimal.github.io/posts/base-and-conversion/","summary":"\u003cp\u003e👉 推荐一个不错的网站 - \u003ca href=\"http://c.biancheng.net/\"\u003eC 语言中文网\u003c/a\u003e\u003c/p\u003e","title":"进制及转换"},{"content":"字符是什么?字母、汉字、标点符号、控制字符、假名……\n计算机中储存的信息都是二进制数表示的,我们在屏幕上看到的英文、汉字等字符都是二进制转换之后的结果。按照何种规则将字符存储在计算机中,如 a 用什么表示,称为“编码”;反之,将存储在计算机中的二进制数解析显示出来,称为“解码”。\n= 可以这样说,人类可读的即为“解”,计算机可读的即为“编”。\n严格来说,字符集和字符编码不是一个概念,字符集定义了字符和二进制的对应关系,为字符分配了唯一的编号,而字符编码规定了如何将字符的编号存储到计算机中。\n也就是说,字符编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。如下图所示:\n为什么要严格区分字符集与字符编码这两个概念呢?\n在早期,字符集与字符编码是一对一的,但随着时间的发展,出现了一对多的情形,即一种字符集可能有了多种编码实现。如上图所示,unicode 字符集就有 utf-8、utf-16、utf-32 多种编码方式。\n如果你想要了解更多关于字符集及字符编码相关的历史,可以阅读 该文档 。\n常用字符集 \u0026amp; 编码 知道了字符、字符集及字符编码的基本概念,哪到底都有什么字符集及其编码规则呢 ❓\nascii ascii(american standard code for information interchange,美国信息交换标准代码),是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语(ascii)和其他西欧语言 (eascii,基于 ascii 的扩展)。它由 ansi(american national standard insitute,美国国家标准学会)制定的,是一种标准的 单字节字符编码方案 。\n好吧,还是缩写好用 😅……\r单字节?1 个字节 包含 8 位,也就是最多编码 256 个字符喽。事实上,基础的 ascii 只使用了 7 位,共 128 个字符,后续的 eascii 扩展为了表示更多欧洲常用字符,才使用了第 8 位。\n下图为基础版的 ascii :\ngb* 系 256 种表示?在汉字面前微不足道!不够用啊!!!\n其实对于汉字的个数并没有个准确的数字,不完全统计汉字大约有十万个左右。根据 1988 年颁布的《现代汉语常用字表》,在我们日常生活中常用的汉字有 2500 个,次常用的汉字有 1000 个。\n一个字节不够用?那就再加一个,216 = 65536 种表示,勉强基本够用了……\n前后经历了,gb2312 → gbk → gb18030 ,具体细节请自行查阅哦。你可能还见过 big5 ,它是繁体中文常用的汉字字符标准。\ngbk 并非国标,是微软搞出来的在 gb13000(国标过渡版)基础上扩展的(编码方式不同,emm\u0026hellip;),最初实现于 windows95 简体中文版。\nunicode 全世界有上百种语言,各国有各国的标准,会冲突的!!!多语言混合的文本中,就成了“一锅粥”,乱码了……\n有没有一种字符集,收录了世界上所有的字符,统一编码呢 ❓ 有,unicode !\nunicode 编码系统为表达任意语言的任意字符而设计,它使用 4 字节数来表达每个字母、符号,或者表意文字。\n4 个字节,232 = 4,294,967,296 种表示,遇到外星人 📡 也够用了……\nutf-32 就是用 4 个字体,utf-16 用的是 2 个字节,那 utf-8 就是 1 个字节了? 不,utf-8 是变长的(可变长度字符编码)。\n就左边这们大佬(肯·汤普森)搞出来的,他还做了 b 语言,基于 b 语言的 unix ,c 语言,后又用 c 语言重新编写了 unix ,现在又搞了个 golang …… 右边这位好基友(丹尼斯·里奇)也是个神,unix 和 c 语言的共同创始人。\n谢祖师爷……歇歇吧,卷不动了…… 😱\r为什么我们需要 utf-8 呢?如果用 utf-16(最常用的 unicode 标准),如果你写的都全部是英文的话,使用它编码就需要多出一倍的存储空间,在存储和传输上就十分不划算。\n硬盘不贵,带宽贵啊!\r本着节约的精神,可变长编码的 utf-8 诞生了,它把一个 unicode 字符根据不同的字符大小编码成 1~6 个字节,常用的英文字母被编码成 1 个字节(ascii 的超集),汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4~6 个字节。\n实际上,现在计算机系统通用的字符编码工作方式:在计算机内存中,统一使用 unicode 编码(utf-16),当需要保存到硬盘或者需要传输的时候,就转换为 utf-8 编码。\n用记事本编辑的时候,从文件读取的 utf-8 字符被转换为 unicode 字符到内存里,编辑完成后,保存的时候再把 unicode 转换为 utf-8 保存到文件:\n浏览网页的时候,服务器会把动态生成的 unicode 内容转换为 utf-8 再传输到浏览器。\n互联网工程工作小组(ietf)要求所有互联网协议都必须支持 utf-8 编码。\nutf-8 的实现原理 上文,我们知道 utf-8 是可变长度编码,那么在解码时,如何知道当前字符占用几个字节呢?通过解析第一个字节获取信息。\n1 个字节\n如果第一个字节的最高位是 0 ,那么表示当前字符占一个字节,如下:\n这里也可以看出 utf-8 是完全兼容 ascii 码的,因为 ascii 码的最高位也是 0 。\n2 个字节\n如果第一个字节的最高位是 110 ,那么表示这个字符占 2 个字节,第二个字节的最高 2 位是 10 ,如下:\n蓝色部分的数字组合在一起,就是实际的码位值。假如,要表示的字符,其码位值为 413 (对应填制为 00110011101 ),其表示就如下:\n3 个字节\n如果第一个字节的最高位是 1110,那么第 2 和第 3 个字节的最高位是 10 ,如下:\n4 个字节\n原理同上,只是第一个字节的最高位是 11110 ,如下 :\n6 个字节\n不同字节对应的码位范围如下图,左侧 bits 栏表示用于表示码位的 bit 数,如 4 个字节,其中有 21 位用于表示码位,即上图中的蓝色部分 。\n不难看出,utf-8 的产生是循序渐进的, 其拥有很高的灵活性,而且可以进行扩展,能够表示的字符范围很大。\n结语 一切都是在发展的,一切都是在改善的,有时候,只要一点奇思妙想,就会让世界变得更加美好。\n参考链接 https://blog.csdn.net/weixin_44198965/article/details/93125017 https://zhuanlan.zhihu.com/p/260192496 https://www.runoob.com/w3cnote/charset-encoding.html https://www.liaoxuefeng.com/wiki/1016959663602400/1017075323632896 https://blog.csdn.net/whahu1989/article/details/118314154 ","date":"2022-07-14","permalink":"https://loveminimal.github.io/posts/charset-and-character-encoding/","summary":"\u003cp\u003e字符是什么?字母、汉字、标点符号、控制字符、假名……\u003c/p\u003e\n\u003cp\u003e计算机中储存的信息都是二进制数表示的,我们在屏幕上看到的英文、汉字等字符都是二进制转换之后的结果。按照何种规则将字符存储在计算机中,如 \u003ccode\u003ea\u003c/code\u003e 用什么表示,称为“编码”;反之,将存储在计算机中的二进制数解析显示出来,称为“解码”。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 可以这样说,人类可读的即为“解”,计算机可读的即为“编”。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"字符集和字符编码"},{"content":"https://jwt.io/\n相关参考:\njson web token introduction - jwt.io json web token 入门教程 - 阮一峰的网络日志 jwt 介绍 - step by step - 技术译民 - 博客园 ","date":"2022-07-14","permalink":"https://loveminimal.github.io/posts/json-web-tokens/","summary":"\u003cp\u003e\u003ca href=\"https://jwt.io/\"\u003ehttps://jwt.io/\u003c/a\u003e\u003c/p\u003e","title":"json web tokens"},{"content":" 当前博文以 org mode 格式编辑。\ni have a vim, i have a emacs.\neh~ vim-emacs!\ni have a code, i have a vim-emacs.\neh~ code-vim-emacs!\ncve!\n简介 在 键位映射那些事儿 中, blablabla…… 说了好多。之前,我一直在个人电脑上使用 vscode ,各种插件和键位设置都是可以自动同步的。然而,当切换到别人的电脑时,就又不得不面对原版的键位模式。还是那句话,不是不能接受,只是有点困扰,哎,处女座……\n于是,我开发了一款简单易用的 vscode 插件 – cve keymap ,本文做一些插件开发过程中遇到问题的记录。\n\u0026lt;img src=\u0026#34;imgs/1.jpg\u0026#34; width=\u0026#34;\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n基础 磨刀不误砍柴工,先补了一下基础知识和概念。\n一次偶然的机会,看到极客教程中的 vscode 模块 ,里面系统性的介绍了 vscode 的一些实现原理( 部分命令可能有些老旧,但无伤大雅)。大家如果感兴趣,可以点进去阅读一下。\n当然,你也可以直接阅读官方文档。\n安装 开发和编译 vscode 插件,需要做一些准备工作,确保当前环境已经安装了 node.js 和 git ,然后执行以下命令安装 yeoman 和 vscode extension generator 。\nnpm install -g yo generator-code # 使用脚手架创建插件模板, myextension 是你的插件名称 yo code myextension 注意, node.js 的版本不能太低,本机使用的是 16+ ,最低不要低于 14 ,不然有些依赖不兼容。如果,你的已有项目依赖低版本的 node.js (哎,历史项目啦),那么你需要 nvm 。\n\u0026lt;img src=\u0026#34;imgs/tn.jpg\u0026#34; width=\u0026#34;\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n我们选择 new keymap 模板,其他模板请自行了解 😅。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这个过程中,唯一可能遇到的问题,就是我们提到的 node.js 版本的问题。 \u0026lt;/div\u0026gt;\n一系列操作之后,就生成了我们的插件目录。如下:\n. ├── changelog.md ├── .git ├── .gitattributes ├── .gitignore ├── package.json ├── readme.md ├── vsc-extension-quickstart.md ├── .vscode └── .vscodeignore 2 directories, 7 files 使用 vscode 打开该目录,即可进行开发工作了。具体细节,请参考官方文档。\n开发 开发过程中,可以使用 f5 进入到调试预览模式,它会新打开一个 vscode 窗口(微软良心开发工具 🥰)。\n发布 在插件开发完成之后呢,就可以打包成 vsix 格式的插件包分享给其他小伙伴使用了。更多了解 publishing extensions。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 如果,你是第一次制作插件,并准备发布它,恭喜你,坑才刚刚开始,你还有一系列准备工作要完成。 \u0026lt;/div\u0026gt;\n打包、发布在配置完成之后,后续执行都是很简单的。\n首先,我们先准备一个 cli 工具 – vsce ,来管理我们的 vscode 插件。安装:\nnpm install -g vsce 而后执行 vsce package 即可完成打包。but 你也大概率可能会遇到以下问题,来看看吧。\n# 问题 1 error:make sure to edit the readme.md file before you publish your extension. 直接删除目录中的 readme.md ,新建一个写些说明保存提交即可。 不要用原文件进行修改,我这边试了好多次都通不过,一直报上述错误。\n\u0026gt; 注意:同于安全考虑, vsce 不允许插件中包含用户自己添加的 svg 格式图片,且远程图片链接要使用 https urls 。\n1. 获取 pat\n因为 vscode 使用 azure devops 为插件市场提供服务,所以去申请一个账号吧,如果你还没有的话。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 此处,建议使用你平时使用的微软账号。 \u0026lt;/div\u0026gt;\nvsce 可以通过 personal access tokens 发布扩展,所以,如果我们想要更加方便的发布插件,就需要先创建一个 pat(personal access tokens ,私人访问令牌) 。如何创建呢?点击这里查看 。\n注意:给 pat 复制下来记录一下,关了页面之后 ,你就再也找不到了。\n2.创建 publisher\n# 问题 2 error:missing publisher name. learn more:https://code.visualstudio.com... publisher 自然就是发布者了,它是插件市场的一种身份认证,随后你需要在 package.json 中插入 \u0026#34;publisher\u0026#34;: \u0026#34;xxxx\u0026#34; 类似行,更多详情查看 package.json 。\n我们可以通过插件市场的 management page 来创建一个 publisher 。\n注意,这里需要使用申请 pat 时的那个微软账号(要一致)。\n创建完成之后 ,我们可以使用通过下述命令进行验证:\nvsce login \u0026lt;publisher name\u0026gt; 3. 发布\n通过验证后,后续只需要执行如下命令,就可以方便地发布你的插件到插件市场了。\nvsce publish 当然,我们也可以使用 vsce package 打包的 vsix 文件,手动上传到插件市场,但是如果,你后续大频率更新插件的话,就会很困扰了。\n最后 怎么说呢?初看可能很繁琐,但其实按照官方教程一步步进行操作,遇到问题后再搜索查询一下,整个过程还是很容易完成的。just go ! 🌟\n","date":"2022-07-06","permalink":"https://loveminimal.github.io/posts/cve-keymap/","summary":"\u003cp\u003e\n\u003cstrong\u003e当前博文以 \u003ccode class=\"verbatim\"\u003eOrg Mode\u003c/code\u003e 格式编辑。\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eI have a vim, I have a emacs.\u003cbr\u003e\nEh~ vim-emacs!\u003cbr\u003e\nI have a code, I have a vim-emacs.\u003cbr\u003e\nEh~ code-vim-emacs!\u003cbr\u003e\nCVE!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"vscode 插件 cve keymap 的开发记录"},{"content":"vim 的键位不错,无论是移动光标还是进行编辑操作,但是如果是使用中文输入的话,就很尬 \u0026ndash; 中文状态下命令模式不能用,虽然通过一些方式可以使得在切换为非插入模式时自动切换为英文状态,但总是不那么尽如人意。\nemacs 的键位模式,就很适应中英文的混合输入,唯一的缺点就是有点费小手指。在 emacs 中,我一般会启用 evil 插件,如此,可以实现如果是只读文件和做一些删减工作的时候,就直接使用 vim 的键位模式,而在插入模式下,就使用 emacs 的键位模式,很不错,附上一份个人的 evil 插件的配置 init-evil.el 。唯一不足的是,这种方式只有在 emacs 中才是可用的,如果你使用其他编辑器时,就没有这份待遇了。不过,单纯的 emacs 键位也足够好用了。\n在 ide 中,基本上都可以配置键位映射,如 jetbrains 家的系列产品中的 emacs 键位映射都很好用,映射的也比较完整。\n但是在 visual studio 中,就不那么喜人了,vs 恪守着在 windows 系列下的那一套操作模式,不是说不好,而是你好歹让人定制一下。和 vs 比起来,vscode 就良心的多了,丰富的插件仓库,美观的主题,流畅度也很好。因为 vim 存在中英文混输时候的不便,我之前一直使用 emacs 的键位映射,通过下面这两个插件的一种。\n让人遗憾的是,这两个插件,无论哪一个都不能带来完美的体验。在通常的编辑区还可以,但是在诸如搜索、替换等操作时让人很不爽。只能说有实现方式,但实现的比较曲线。\n正如在主题之旅中描述的那样,window 下的 emacs 也或多或少的有一些问题,小问题,但让人很强迫。出于此种原因,我当下只在 linux 中使用它。\n目前的文字编辑工作主力是 vscode ,它有一个非常靠谱的功能,就是你可以自定义键位映射。你完全可以在原版键位的基础上进行个性化的定制,使之变为你想要的样子。\n由于原版按键的兼容性最好,所以这里尽量避免了与原有按键的冲突,充分发挥了 alt 键的使用。它像是一个 emacs 和 vim 的混合体,😄 如下:\n上图对应的 keybindings.json 如下:\n// place your key bindings in this file to override the defaults [ { \u0026#34;key\u0026#34;: \u0026#34;alt+p\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopen\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+n\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.terminal.toggleterminal\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;terminal.active\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+x\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.showcommands\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+x\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopennavigatepreviousinfilepicker\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;infilespicker \u0026amp;\u0026amp; inquickopen\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+i\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.insertsnippet\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+b\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.toggleactivitybarvisibility\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+h\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorleft\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+l\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorright\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorup\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursordown\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+g\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorhome\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_1\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cursorend\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_1\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.commentline\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editortextfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_2\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;undo\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+z\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.togglezenmode\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+e\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.view.explorer\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;viewcontainer.workbench.view.explorer.enabled\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+g\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.view.scm\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;workbench.scm.active\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+j ctrl+f\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.replaceinfiles\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+s\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.replaceinfiles\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.triggersuggest\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editorhascompletionitemprovider \u0026amp;\u0026amp; textinputfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;togglesuggestiondetails\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;editor.action.triggersuggest\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;editorhascompletionitemprovider \u0026amp;\u0026amp; textinputfocus \u0026amp;\u0026amp; !editorreadonly\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;ctrl+oem_7\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;togglesuggestiondetails\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopenselectnext\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;workbench.action.quickopenselectprevious\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+j\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;selectnextsuggestion\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetmultiplesuggestions \u0026amp;\u0026amp; suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;alt+k\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;selectprevsuggestion\u0026#34;, \u0026#34;when\u0026#34;: \u0026#34;suggestwidgetmultiplesuggestions \u0026amp;\u0026amp; suggestwidgetvisible \u0026amp;\u0026amp; textinputfocus\u0026#34; }, ] 体验非常之不错,如果,你也使用 vscode ,快些动手开始定制你自己的键位映射吧!🌟\n","date":"2022-07-05","permalink":"https://loveminimal.github.io/posts/editor-keybindings/","summary":"\u003cp\u003eVim 的键位不错,无论是移动光标还是进行编辑操作,但是如果是使用中文输入的话,就很尬 \u0026ndash; 中文状态下命令模式不能用,虽然通过一些方式可以使得在切换为非插入模式时自动切换为英文状态,但总是不那么尽如人意。\u003c/p\u003e\n\u003cp\u003eEmacs 的键位模式,就很适应中英文的混合输入,唯一的缺点就是有点费小手指。在 Emacs 中,我一般会启用 Evil 插件,如此,可以实现如果是只读文件和做一些删减工作的时候,就直接使用 Vim 的键位模式,而在插入模式下,就使用 Emacs 的键位模式,很不错,附上一份个人的 Evil 插件的配置 \u003ca href=\"https://github.com/loveminimal/emacs.d/blob/master/lisp/init-evil.el\"\u003einit-evil.el\u003c/a\u003e 。唯一不足的是,这种方式只有在 Emacs 中才是可用的,如果你使用其他编辑器时,就没有这份待遇了。不过,单纯的 Emacs 键位也足够好用了。\u003c/p\u003e","title":"键位映射那些事儿"},{"content":"🔔 以下内容主要摘录自 廖雪峰老师的博客,具体示例请跳到原文参考。\n= 好吧,廖老师配图一直可以的。\n注解是什么 注解是什么呢?它和注释有什么区别?\n注解定义后也是一种 class ,所有的注解都继承自 java.lang.annotaion.annotation 。 ok,我们先看看注解是做什么用的,再来了解如何定义和使用它。\n注解是放在 java 源码的类、方法、字段、参数前的一种特殊的“注释”(注意并不是注释),是一种用做标注的“元数据”。如,我们日常使用的 @override 就是一种注解。\n从 jvm 的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。\n其实这里的意思时,注解的作用取决于你是如何定义注解和使用注解的方法的。注解,就像是你在代码的某个位置放的一个钩子,至于如何使用它,则完全由你决定。\rjava 的注解可以分为三类:\n由编译器使用的注解; 由工具处理 .class 文件使用的注解; 在程序运行运行期能够读取的注解。 第一类,编译器使用的注解,如 @override 让编译器检查该方法是否正确地实现了覆写, @suppresswarnings 告诉编译器忽略此处代码产生的警告等。这类注解不会被编译进入 .class 文件,它们在编译后就被编译器扔掉了。(😿)\n第二类,由工具处理 .class 文件使用的注解,比如有些工具会在加载 class 的时候,对 class 做动态修改,以实现一些特殊的功能。这类注解会被编译进入 .class 文件,但在类加载结束后并不会存在于内存中(使命已经完成了,仅作用于 class)。它只被一些底层库使用,一般不必我们自己处理。\n第三类,在程序运行期能够读取的注解,这类注解在加载后一起存在于 jvm(内存中啦) 中(因为要在运行期读取啦 😏),这也是最常用的注解。\nok,了解了注解是什么,有什么用之后 ,让我们来看一下如何定义一个注解吧。\n定义注解 java 语言使用 @interface 语法来定义注解,它的格式如下:\npublic @interface report { int type() default 0;\t// default 后就是默认值 string level() default \u0026#34;info\u0026#34;; string value() default \u0026#34;\u0026#34;; } 注解定义后也是一种 class,所有的注解都继承自 java.lang.annotation.annotation 。\n不难看出,在定义一个注解时,还可以定义配置参数。需要注意的是,配置参数必须是常量,在定义注解时就已经确定了每个参数的值(可以有默认值)。\n*大部分注解会有一个名为 value 的配置参数,对此参数赋值,可以只写常量,相当于省略了 value 参数。\n有一些注解可以修饰其他注解 \u0026ndash; 元注解(meta annotation)。\n这里,我们只了解两个常用的元注解: @target 和 @retention 。\n@target\n最常用的元注解是 @target ,它用来定义 annotation 能够被应用于源码的哪些位置:\n类或接口: elementtype.type ; 字段: elementtype.field ; 方法: elementtype.method ; 构造方法: elementtype.constructor ; 方法参数: elementtype.parameter 。 实际上 @target 定义的 value 是 elementtype[] 数组,只有一个元素时,可以省略数组的写法。\n@retention\n@retention 定义了 annotation 的生命周期:\n仅编译器(译后即丢): retentionpolicy.source ; 仅 class (不入 jvm)文件: retentionpolicy.class ; 运行期(加载进 jvm,供程序读取): retentionpolicy.runtime 。 如果 @retention 不存在,则该 annotation 默认为 class,但其实通常我们自定义的 annotation 都是 runtime ,所以 务必要加上 @retention(retentionpolicy.runtime) 这个元注解。\nok,我们来总结一下定义 annotation 的步骤:\n用 @interface 定义注解; 添加参数、默认值(把最常用的参数定义为 value() ,方便使用时直接写常量); 用元注解配置注解。 一直走在偷懒的路上,永不停歇……\r如这样:\n@target(elementtype.type)\t// 3 @retention(retentionpolicy.runtime) public @interface report {\t// 1 int type() default 0;\t// 2 string level() default \u0026#34;info\u0026#34;; string value() default \u0026#34;\u0026#34;; } 处理注解 在日常生产环境中,我们基本上只需编写和使用 runtime 类型的注解,所以我们只讨论它。前面已经说过,该类型注解是加载进 jvm 供程序读取的,那么如何读取呢?反射 api!\n使用反射 api 读取 annotation:\nclass.getannotation(class) ; field.getannotation(class) ; method.getannotation(class) ; constructor.getannotation(class) ; 如:\n// 获取 person 定义的@report 注解: report report = person.class.getannotation(report.class); int type = report.type(); string level = report.level(); 如果读取时,annotation 不存在,则返回 null 。\n……\n注意,定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解,检查逻辑完全是我们自己编写的,jvm 不会自动给注解添加任何额外的逻辑。\n应用注解 这时我们概览一下注解在 web 开发中的常见应用形式。\n在 servlet 中的应用 在 javaee 平台上,处理 tcp 连接,解析 http 协议这些底层工作统统扔给现成的 web 服务器去做。我们使用 servlet api 编写自己的 servlet 来处理 http 请求,web 服务器实现 servlet api 接口,实现底层功能:\n┌───────────┐\r│my servlet │\r├───────────┤\r│servlet api│\r┌───────┐ http ├───────────┤\r│browser│\u0026lt;──────\u0026gt;│web server │\r└───────┘ └───────────┘ 注解在 servlet 中如何应用呢?它有什么作用呢?\n1. @webservlet\n我们知道,一个 servlet 总是继承自 httpservlet ,然后覆写 doget() 或 dopost() 方法。如何知道客户端的请求地址呢?早期的 servlet 使用 web.xml 文件来配置映射路径,现在我们使用注解 @webservlet 来实现。如下:\n// webservlet 注解表示这是一个 servlet ,并映射到地址 /hello: @webservlet(urlpatterns = \u0026#34;/hello\u0026#34;) public class helloservlet extends httpservlet { protected void doget(httpservletrequest req, httpservletresponse resp) throws servletexception, ioexception { // 设置响应类型: resp.setcontenttype(\u0026#34;text/html\u0026#34;); // 获取输出流: printwriter pw = resp.getwriter(); // 写入响应: pw.write(\u0026#34;\u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt;\u0026#34;); // 最后不要忘记 flush 强制输出: pw.flush(); } } 浏览器发出的 http 请求总是由 web server 先接收,然后,根据 servlet 配置的映射,不同的路径转发到不同的 servlet 。\n2. @webfilter\n在一个复杂的 web 应用程序中,通常有很多 url 映射,对应的,也会有多个 servlet 来处理 url 。为了把一些公用逻辑从各个 servlet 中抽离出来,javaee 的 servlet 规范还提供了一种 filter 组件,即过滤器。它的作用是,在 http 请求到达 servlet 之前,可以被一个或多个 filter 预处理,类似打印日志、登录检查等逻辑,完全可以放到 filter 中。\n使用也很简单,来看一段示例:\n// 用 @webfilter 注解标注该 filter 需要过滤的 url ,这里的 /* 表示所有路径 @webfilter(urlpatterns = \u0026#34;/*\u0026#34;) public class encodingfilter implements filter { public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { system.out.println(\u0026#34;encodingfilter:dofilter\u0026#34;); request.setcharacterencoding(\u0026#34;utf-8\u0026#34;); response.setcharacterencoding(\u0026#34;utf-8\u0026#34;); chain.dofilter(request, response); } } 3. @weblistener\n除了 servlet 和 filter 外,javeee 的 servlet 规范还提供了第三种组件 - listener (监听器) 。\n有好几种 listener ,其中最常用的是 servletcontextlistener ,我们来编写一个实现该接口的类,如下:\n@weblistener public class applistener implements servletcontextlistener { // 在此初始化 webapp, 例如打开数据库连接池等: public void contextinitialized(servletcontextevent sce) { system.out.println(\u0026#34;webapp initialized.\u0026#34;); } // 在此清理 webapp, 例如关闭数据库连接池等: public void contextdestroyed(servletcontextevent sce) { system.out.println(\u0026#34;webapp destroyed.\u0026#34;); } } 任何标注为 @weblistener ,且实现了特定接口的类会被 web 服务器自动初始化。\n看,我们有钩子了 🥰\r一个 web 服务器可以运行一个或多个 webapp,对于每个 webapp ,web 服务器都会为其创建一个全局唯一的 servletcontext 实例,我们在上例中编写的两个回调方法实际上对应的就是 servletcontext 实例的创建和销毁。\n在 spring 中的应用 我们知道 spring 的核心就是提供了一个 ioc 窗口,它可以管理所有轻量级的 javabean 组件。起初,spring 也使用类似 xml 这样的配置文件,来描述 bean 的依赖关系,然后让容器来创建并装配 bean 。然而,这种方式虽然直观,写起来却很繁琐。\n1. @component 和 @autowired\n现在,我们可以使用 annotation 来注解,让 spring 自动扫描 bean 并组装它们。如:\n@component class mailservice { ... } @component public class userservice { @autowired mailservice mailservice; // ... } 如上,这个 @component 注解就相当于定义了一个 bean ,它有一个可选的名称,默认是 mailservice 、userservice (小写开头的类名)。\n@autowired 则相当于把指定类型的 bean 注入到指定的字段中。它不但可以写在 set() 方法上,还可以直接写在字段上,甚至可以写在构造方法中。\n2. @configuration 和 @componentscan\n要启动一个 spring 应用,我们需要编写一个类启动容器,如下:\n@configuration @componentscan public class appconfig { public static void main(string[] args) { applicationcontext context = new annotationconfigapplicationcontext(appconfig.class); userservice userservice = context.getbean(userservice.class); user user = userservice.login(\u0026#34;bob@example.com\u0026#34;, \u0026#34;password\u0026#34;); system.out.println(user.getname()); } } 其中, @configuration 表示 appconfig.class 是一个配置类,在创建 applicationcontext 时,使用的实现类是 annotationconfigapplicationcontext ,必须传入一个标注了 @configuration 的类名。\n@componentscan 则告诉容器,自动搜索当前类所在的包以及子包,把所有标注为 @component 的 bean 自动创建出来,并根据 @autowired 进行装配。\n看,ioc 容器其实啥都不知道,你需要用 annotation 告诉它,做什么、怎么做、在哪做。\r使用 @componentscan 很方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置类位于自定义的顶层包,其他 bean 按类别放入子包。\n思考一下,如何创建并配置一个第三方 bean 呢?它并不在当前可搜索的包中!\n3. @bean\n如果一个 bean 不在我们自己的 package 管理之内,例如 zoneid ,如何创建它?我们只需要在 @configuration 配置类中编写一个 java 方法(该方法使用 @bean 注解)创建并返回它。\n@configuration @componentscan public class appconfig { // 创建一个 bean: @bean zoneid createzoneid() { return zoneid.of(\u0026#34;z\u0026#34;); } } 4. @propertysource\n在开发应用程序时,经常需要读取配置文件,最常用的配置方法是以 key=value 的形式写在 *.properties 文件中。 spring 提供了一个简单的 @propertysource 来自动读取配置文件,只需要在配置类上再添加一个注解。\n@configuration @componentscan @propertysource(\u0026#34;app.properties\u0026#34;) // 表示读取 classpath 的 app.properties public class appconfig { @value(\u0026#34;${app.zone:z}\u0026#34;) string zoneid; @bean zoneid createzoneid() { return zoneid.of(zoneid); } } 如上,spring 容器看到 @propertysource(\u0026quot;app.properties\u0026quot;) 注解后,就会自动读取这个配置文件,然后,我们使用 @value 正常注入。\n5. @profile 和 @conditional\n创建某个 bean 时,spring 容器可以根据注解 @profile 来决定是否创建,除此之外,也可以根据 @conditional 来决定。\n其实,还有其他的一些,blablabla…… 因为,spring boot 提供了更好的,所以,我们在实际工作中并不怎么一些“老旧”的注解了。\r6. @aspect 和 @enableaspectjautoproxy\n当 spring 的 ioc 容器看到 @enableaspectjautoproxy 这个注解,就会自动查找带有 @aspect 的 bean,然后根据每个方法的 @before、@around 等注解把 aop 注入到特定的 bean 中。\n7. @enabletransactionmanagement 和 @transactional\nspring 提供了一个 platformtransactionmanager 来表示事务管理器,所有的事务都由它负责管理。使用编程的方式使用 spring 事务仍然比较繁琐,更好的方式是通过声明式事务来实现。\n使用声明式事务非常简单,除了在配置类中追加一个定义的 platformtransactionmanager 外,再添加一个 @enabletransactionmanagement 就可启用声明式事务。\n@configuration @componentscan @enabletransactionmanagement // 启用声明式 @propertysource(\u0026#34;jdbc.properties\u0026#34;) public class appconfig { // ... } 然后,对需要事务支持的方法,加一个 @transactional 注解。也可以直接加在 bean 的 class 处,它表示其所有 public 方法都具有事务支持。\nspring 对一个声明式事务的方法开启事务支持的原理,仍然是 aop 代理,取了通过自动创建 bean 的 proxy 实现。\n在 spring mvc 中的应用 我们知道,spring 提供的是一个 ioc 容器,所有的 bean 都在该容器中被初始化。而 servlet 容器由 javaee 服务器人提供(如 tomcat), servlet 容器对 spring 一无所知,它们之间依靠什么进行联系?又是以何种顺序初始化的呢? 详细答案请参考 如何关联 servlet 和 spring 。\n只需要在配置类上加上 @enablewebmvc 注意,就激活了 spring mvc 。\n// controller 使用@controller 标记而不是@component: @controller public class usercontroller { // 正常使用 @autowired 注入: @autowired userservice userservice; // 处理一个 url 映射: @getmapping(\u0026#34;/\u0026#34;) public modelandview index() { ... } ... } 这里,我们需要注意, controller 使用 @controller 标记,而不是 @component 。(很明显,前者针对 coontroller 做了一些增强)。\n好的,现在我们来回答一下开始的问题 - 如何关联 servlet 和 spring ?\nspring mvc 提供了一个 dispatcherservlet 类,我们只需在 web.xml 中配置它。\n\u0026lt;!doctype web-app public \u0026#34;-//sun microsystems, inc.//dtd web application 2.3//en\u0026#34; \u0026#34;http://java.sun.com/dtd/web-app_2_3.dtd\u0026#34; \u0026gt; \u0026lt;web-app\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.dispatcherservlet\u0026lt;/servlet-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextclass\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;org.springframework.web.context.support.annotationconfigwebapplicationcontext\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextconfiglocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;com.itranswarp.learnjava.appconfig\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;load-on-startup\u0026gt;0\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; servlet 容器会首先初始化 dispatcherservlet ,在 dispatcherservlet 启动时,根据配置类 appconfig 创建一个类型是 webapplicationcontext 的 ioc 容器,完成所有 bean 的初始化,并将该容器绑到 servletcontext 上。\n如此, dispatcherservlet 持有 ioc 容器,自然就可以从 ioc 容器中获取所有的 @controller 的 bean ,在接收到 http 请求后,根据 controller 方法配置的路径转发到指定方法,并根据返回的 modelandview 决定如何渲染页面。\n最后,在配置类 appconfig 中通过 main() 方法启动嵌入式 tomcat 即可。\n1. @controller\n该注解用来标识当前 bean 是一个 controller 。spring mvc 对 controller 没有固定的要求,也不需要实现特定的接口,只需要在 controller 类中,编写对应的方法处理相应的请求路径就可以了。\n2. @getmapping、@postmapping、@requestparam\n@controller public class usercontroller { // ... @postmapping(\u0026#34;/signin\u0026#34;) public modelandview dosignin( @requestparam(\u0026#34;email\u0026#34;) string email, @requestparam(\u0026#34;password\u0026#34;) string password, httpsession session) { // ... } } } 一个方法对应一个 http 请求路径,用 @getmapping 或 @postmapping 表示 get 或 post 请求。\n需要接收的 http 参数以 @requestparam() 标注。\n2. @restcontroller\n直接用 spring 的 controller 配合一大堆注解写 rest 太麻烦了,因此,spring 额外提供了一个 @restcontroller 注解,使用它注解 controller,每个方法自动变成 api 接口方法。\n@restcontroller @requestmapping(\u0026#34;/api\u0026#34;) public class apicontroller { @autowired userservice userservice; @getmapping(\u0026#34;/users\u0026#34;)\t// 实际为 /api/users,下同 public list\u0026lt;user\u0026gt; users() { return userservice.getusers(); } @getmapping(\u0026#34;/users/{id}\u0026#34;) public user user(@pathvariable(\u0026#34;id\u0026#34;) long id) { return userservice.getuserbyid(id); } @postmapping(\u0026#34;/signin\u0026#34;) public map\u0026lt;string, object\u0026gt; signin(@requestbody signinrequest signinrequest) { try { user user = userservice.signin(signinrequest.email, signinrequest.password); return map.of(\u0026#34;user\u0026#34;, user); } catch (exception e) { return map.of(\u0026#34;error\u0026#34;, \u0026#34;signin_failed\u0026#34;, \u0026#34;message\u0026#34;, e.getmessage()); } } public static class signinrequest { public string email; public string password; } } 如此,编写 rest 接口只需要定义 `@restcontroller` ,然后每个方法都是一个 api 接口,输入和输出只要能被 jackson 序列化或反序列化为 json 就没有问题。\n3. @crossorigin\n……\n在 spring boot 中的应用 spring boot 是什么?了解 她 。\n1. @springbootapplication\nspring boot 要求 main() 方法所在的启动类必须放到 package 下,命名不作要求。启动 spring boot 应用程序只需要一行代码加上一个注解 @springbootapplication 即可。\n@springbootapplication public class application { public static void main(string[] args) throws exception { springapplication.run(application.class, args); } } 还要啥自行车,直接飞起了 🚀\r2. @conditionalonxxx\nspring 本身提供了条件装配 @conditional,但是要自己编写比较复杂的 condition 来做判断,比较麻烦。spring boot 则为我们准备好了几个非常有用的条件,如:\n@conditionalonproperty :如果有指定的配置,条件生效; @conditionalonbean :如果有指定的 bean,条件生效; @conditionalonmissingbean :如果没有指定的 bean,条件生效; @conditionalonmissingclass :如果没有指定的 class,条件生效; @conditionalonwebapplication :在 web 环境中条件生效; @conditionalonexpression :根据表达式判断条件是否生效。 ……\n","date":"2022-07-05","permalink":"https://loveminimal.github.io/posts/annotation/","summary":"\u003cp\u003e🔔 以下内容主要摘录自 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255945389098144\"\u003e廖雪峰老师的博客\u003c/a\u003e,具体示例请跳到原文参考。\u003c/p\u003e","title":"注解"},{"content":"🔔 相关内容请参阅 https://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\nspring boot 是什么?它是一个基于 spring 的套件,它帮助我们预组装了一系列组件,以便以尽可能少的代码和配置来开发基于 spring 的 java 应用程序。\nspring boot makes it easy to create stand-alone, production-grade spring based applications that you can \u0026ldquo;just run\u0026rdquo;.\nwe take an opinionated view of the spring platform and third-party libraries so you can get started with minimum fuss. most spring boot applications need minimal spring configuration.\n\u0026ndash; spring boot\n在 spring 中,我们使用 xml 文件或是注解来告诉 spring 如果处理我们的组件。但是你必需给它明确的指令,它才知道如何正确的执行。随着组件数量的增多,配置项变得越来越长,以至于难以维护。所以,有必要让框架为我们做更多的事儿,比如解决依赖的依赖等问题,这就是为什么我们需要 spring boot。\nspring boot 是如何做到这些的呢?下面让我们来慢慢揭开这位“俏姑娘”的面纱 🥰。\n她长什么样? 我们不妨新建一个 springboot-hello 项目,创建标准的 maven 目录结构如下:\nspringboot-hello\r├── pom.xml\r├── src\r│ └── main\r│ ├── java\r│ └── resources\r│ ├── application.yml\t# spring boot 默认的配置文件\r│ ├── logback-spring.xml\t# spring boot 的 logback 配置文件名称\r│ ├── static\t# 静态文件目录\r│ └── templates\t# 模板文件目录\r└── target 我们主要的工作目录是 src/main/java/ ,我们来看一看源码目录结构:\nsrc/main/java\r└── com\r└── itranswarp\r└── learnjava\r├── application.java\t# !启动类\r├── entity\r│ └── user.java\r├── service\r│ └── userservice.java\r└── web\r└── usercontroller.java 注意:spring boot 要求 main() 方法所在的启动类必须放到根 package 下,命名不做要求(这里我们以 application.java 命名)。\n启动类,是一切的起点哦。\r她的魔法之源 启动类 我们来看一下上个章节中的 application.java 启动类,其内容如下:\n@springbootapplication public class application { public static void main(string[] args) throws exception { springapplication.run(application.class, args); } } 像是之前,我们使用的 spring 启动类,它包含了各种各样的注解,如 @configuration、 @componentscan 等。现在,我们却只需要 @springbootapplication,它是如何工作的?\n原来 @springbootapplication 这个注解实际上包含了:\n- @springbootconfiguration\r- @configuration\r- @enableautoconfiguration\r- @autoconfigurationpackage\r- @componentscan 所以,这一个注解相当于启动了自动配置和自动扫描。那么,她是 如何实现自动配置和自动扫描的呢?\n自动扫描和配置 我们再观察 pom.xml ,它的内容如下:\n\u0026lt;project ...\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-parent\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;2.3.0.release\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelversion\u0026gt;4.0.0\u0026lt;/modelversion\u0026gt; \u0026lt;groupid\u0026gt;com.itranswarp.learnjava\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;springboot-hello\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;1.0-snapshot\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;11\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;11\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;java.version\u0026gt;11\u0026lt;/java.version\u0026gt; \u0026lt;pebble.version\u0026gt;3.1.2\u0026lt;/pebble.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-web\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-starter-jdbc\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 集成 pebble view --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;io.pebbletemplates\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;pebble-spring-boot-starter\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;${pebble.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- jdbc 驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;org.hsqldb\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;hsqldb\u0026lt;/artifactid\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 使用 spring boot 时,强烈推荐从 spring-boot-starter-parent 继承,它会引入 spring boot 的预置配置。\n按大佬说的做,别给自己找 trouble 😂\r紧接着,我们引入了各种 starter 依赖,并且没有指定版本号,因为引入的 \u0026lt;parent\u0026gt; 内已经指定了,只有我们自己引入的某些第三方 jar 包需要指定版本号。\nstarter 是个啥?\nspring boot 将日常企业应用研发中的各种场景都抽取出来,做成一个个的 starter (启动器),starter 中整合了该场景下各种可能用到的依赖,用户只需要在 maven 中引入 starter 依赖,springboot 就能自动扫描到要加载的信息并启动相应的配置。\nstarter 提供了大量的自动配置,让用户摆脱了处理各种依赖和配置项的困扰。所有这些 starter 都遵循着约定俗成的默认配置,并允许用户调整这些配置。\n并不是所有的 starter 都是由 spring boot 官方提供的,也有部分 starter 是第三方技术厂商提供的,如 druid-spring-boot-starter 和 mybatis-spring-boot-starter 等等。\n在启动时, spring boot 自动启动了嵌入式 tomcat ,如数据源、声明式事务、jdbctemplate 等 bean 都是由 spring boot 自动创建 \u0026ndash; 通过 autoconfiguration。\n在 starter 引入后,在启动时会自动扫描所有的 xxxautoconfiguration ,如,当我们引入 spring-boot-starter-jdbc 时,它自动扫描了如下:\ndatasourceautoconfiguration :自动创建一个 datasource ,其中配置项从 application.yml 的 spring.datasource 中读取; datasourcetransactionmanagerautoconfiguration :自动创建了一个基于 jdbc 的事务管理器; jdbctemplateautoconfiguration:自动创建了一个 jdbctemplate 。 因此,我们自动得到了一个 datasource、一个 datasourcetransactionmanager 和一个 jdbctemplate 。\nspring boot 大量使用 xxxautoconfiguration 来使得许多组件被自动化配置并创建。\n看, xxxautoconfiguration 就是 spring boot 的魔力之源,许多事情这些自动配置类都帮我们做了,谢谢它们全家。\n点石成金 i.e. 打包 spring boot 应用\nspring boot 自带了一个简单强大的 spring-boot-maven-plugin 插件用来打包,我们只需要在 pom.xml 中引入它即可。像这样:\n\u0026lt;project ...\u0026gt; ... \u0026lt;build\u0026gt; \u0026lt;!-- 默认的项目名为 `项目名+版本号`,不喜欢可以通过 `finalname` 自定义 --\u0026gt; \u0026lt;!-- \u0026lt;finalname\u0026gt;awesome-app\u0026lt;/finalname\u0026gt; --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupid\u0026gt;org.springframework.boot\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactid\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 无需使用配置,spring boot 的这款插件会自动定位应用程序的入口 class ,我们只需要执行 mvn clean package 命令就可以打包了。\n之后 ,在打包后的 target 目录下,可以看到一个 springboot-exec-jar-1.0-snapshot.jar 包,它包含了项目运行所需的所有依赖,可以直接运行:\njava -jar springboot-exec-jar-1.0-snapshot.jar 这样,部署一个 spring boot 应用就非常简单,无需预安装任何服务器,只需要上传 jar 包即可。\n结语 看 spring boot 很简单,让我们的工程实现也变得简单。我们在这里只涉及了基本的初步介绍,想要了解更多,去设计一个项目吧,只有在具体的问题中才能更好地了解“她”。\n","date":"2022-07-04","permalink":"https://loveminimal.github.io/posts/spring-boot/","summary":"\u003cp\u003e🔔 相关内容请参阅 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\"\u003ehttps://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eSpring Boot 是什么?它是一个基于 \u003ca href=\"../spring/\"\u003eSpring\u003c/a\u003e 的套件,它帮助我们预组装了一系列组件,以便以尽可能少的代码和配置来开发基于 Spring 的 Java 应用程序。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSpring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can \u0026ldquo;just run\u0026rdquo;.\u003cbr\u003e\nWe take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.\u003cbr\u003e\n\u0026ndash; \u003ca href=\"https://spring.io/projects/spring-boot\"\u003eSpring Boot\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在 Spring 中,我们使用 \u003ccode\u003exml\u003c/code\u003e 文件或是注解来告诉 Spring 如果处理我们的组件。但是你必需给它明确的指令,它才知道如何正确的执行。随着组件数量的增多,配置项变得越来越长,以至于难以维护。所以,有必要让框架为我们做更多的事儿,比如解决依赖的依赖等问题,这就是为什么我们需要 Spring Boot。\u003c/p\u003e\n\u003cp\u003eSpring Boot 是如何做到这些的呢?下面让我们来慢慢揭开这位“俏姑娘”的面纱 🥰。\u003c/p\u003e","title":"spring boot"},{"content":"vim 是 vi 的升级版,一款功能强大、高度可定制的文本编辑器,它只有一个对手 \u0026ndash; emacs 。那么,它来自哪里?\nvim 123 说起 vi ,就不得不提起这们大佬 \u0026ndash; bill joy。除了 vi ,他还创建了 bsd 和 sun ,ok,大佬就是大佬。\n2003年9月9日,乔伊离开sun公司,sun发言人除了宣布joy辞职的消息外,不愿意发表其他评论。从一些迹象看来,他很关注机器人、纳米、基因工程等可能会改变全人类未来生存发展的技术;更加关注科技带来的道德问题: 如何不让科技成为一个国家、政府、集体、甚至个人做恶的工具?\n基础 vim 好学吗?好学,先来个速成 y 分钟学习 vim 。另外,附上关注的一挺有趣的视频 vim 使用技巧系列 。其实,一张图足矣,重要的是使用和练习。\n下图虽好,但可以先忽略,它是用来做查询用的,还是先了解下一些基础的概念。\r如果,以前没有接触过 vim/vi ,初次使用的时候大概率会很迷惑,这再正常不过了。\n安装 去官网下载对应版本安装即可,乌干达 forever …… ⁉️\n我们这里只针对 cli ,vim 谁用 gui ?那玩意没有灵魂\u0026hellip; 如果,你是在 windows 下(常用工作环境),建议使用 git-bash ,安装了 git 之后自带,内置已经安装好了 vim。\n模式 vim 编辑器基于 模式 这个概念。它有以下 4 种模式:\n命令模式:启动后处于这个模式,用于导航和操作命令; 插入模式:用于在文件中进行修改和编辑(同常规文本编辑器,如 notepad 😺); 可视模式:用于高亮文本并对它们进行操作(就是选择文字内容,进行复制粘贴之类的); ex 模式(底线命令模式):用于跳到底部的 : 提示行上输入命令。 在命令模式下,我们使用 hjkl 进行光标移动,为什么不直接使用 ←↓↑→ 方向键呢?原来,当前 bill joy 在开发 vi 编辑器时所使用的计算机是一个被称为 adm-3a 的终端,该终端附带的 hjkl 键本就和方向键同位一体,根本就没有独立的方向键。\n从历史中寻找答案,而不是主观臆断。\r配置 vim 是高度可定制的,而你只需要一个配置文件 .vimrc ,它位于家目录下面,gnu/linux 上就是 ~ ,windows 上为 /c/users/\u0026lt;用户名\u0026gt;。为了便于管理,我们一般会把 .vimrc 命名为 vimrc 放于文件夹 .vim 中,然后把该文件夹置于家目录。如下:\n.vim\r├── autoload\r│ └── plug.vim\r├── plugged\r│ ├── delimitmate\r│ ├── molokai\r│ ├── vim-airline\r│ ├── vim-markdown\r│ ├── vim-startify\r│ └── vim-surround\r├── readme.md\r└── vimrc 上边这个目录图是在 linux 下,由 tree 命令生成的,确切的说是 tree -l 2 ,其中 2 是指定的展开的目录层级。不是手画的,不是手画的,曾经手画过……\n我自己的 vim 配置是很简单的,因为平时使用 emacs 😅 。vim 有多种不同的插件管理工具,如 vundle、neobundle、vimplug 和 pathogen 等,我使用的是 vimplug ,小巧、稳定而强大。这里,我们推荐一个神奇的插件库 vimawesome ,速度快、视图怡人。\n最后,再放一张简单配置后的图片吧。\n预祝大家使用愉快,最后欢迎加入 vim 和 emacs 之间旷日持久的圣战,虽然没什么用 😏 。\n更多 着实是平时使用 vim 不是很多,就只说一些基本的使用理念和一些基础的配置情况。相对来说,这种理念性的东西,对所有编辑器都是通用的,只是实现方式有差异而已。\r所有类 unix 的系统中的工具,基本上都配备了一个不错的帮助系统,vim 自然也不例外。安装完成之后,可以在终端执行 vimtutor 打开一个内置的入门教程,默认是英文,当然你可以通过执行 vimtutor zh 打开对应的中文教程。\n*记住,你总是可以通过 vim --help 来获取更多的帮助。\n文件 编辑器是用来编辑文件的,自然最先接触的就是如何新建、打开、更新及保存文件。\n1.打开、新建\n不妨假设,我们在文件夹 ~/demo 中,想要打开该文件夹中的文件 a ,只需要执行 vim a 即可。也可以同时打开多个文件,如 vim a b c 同时打开文件 a、b、c 。(若有,则打开文件,若没有,则创建一个新的文件。)\n.\r├── a\r└── b\r0 directories, 2 files 同时打开多文件时,默认只会显示第一个文件的 buffer ,可以使用以下命令,多窗展示,如下:\nvim -o a b c\t# 窗口在一行\rvim -o a b c\t# 窗口在一列 2. 更新、保存\n打开文件后,进入插入模式,就可以编辑文本了,完成之后使用 :\n:w 保存当前文件; :wq 或是 \u0026lt;s-z s-z\u0026gt; 保存并退出。 我们使用 \u0026lt;c、s、m/a-\u0026gt;表示 ctrl、shift 和 meta/alt 键。\n3. 展示、切换\n当我们编辑多文件时,很多时候避免不了在文件之间进行切换,当然也免不了需要打印出当前所有打开文件的列表。该如何做呢?(其实,这些常规操作才是 vim 的魅力所在)\n在 vim 中,我们可以通过 :ls 来展示当前所有打开的文件列表,如下:\n其中 %a 表示当前激活的 buffer ,可以通过 :bn (buffer next)切换到下一个 buffer ,通过 :bp (buffer previous)切换到上一个 buffer 。\n文件过多了,也是繁琐,怎么办?\n可以通过 :e xxx 打开你想要的文件,但如果文件名 xxx 太长了,也很困扰。有一种更好的文件,如上图中所示,所有打开的文件都会被分配一个 id 号,我们可以通过 :buffer id 来进行直接切换到相应 buffer 。\n窗口 多窗口编辑是很常见的,窗口的新建、切换、关闭也是最常用到的操作之一。\n命令 描述 :sp 横向分屏(即窗口横向排列) :vsp 纵向分屏(即窗口纵向排列) :wincmd w 窗口切换 :close 关闭当前窗口 :only 关闭所有其他窗口 :q 退出当前窗口 当然,还有一些快捷键可以用,也很方便。这个请自行了解,因为我平时也不怎么用这些快捷键,也不大记的住。\n快捷键 关于快捷键,我们这里来单开一个章节来说明一下,之前也写过这方面的一些东西,如 vscode 插件 cve keymap 的开发记录 和 键位映射那些事儿 。这里,我们着重说明一下,在 vim 中如何方便的设计自己的键位映射。\n在 vim 中有个神奇的好东西,就是 \u0026lt;leader\u0026gt; ,强烈建议使用它来自定义你的键位。使用 emacs 的过程中,离不开 evil 插件的原因,很大程度上也是为了这个 \u0026lt;leader\u0026gt; 。\nvim 中在各种模式间进行切换的时候,会频繁使用到 \u0026lt;esc\u0026gt; 这个按键,它一般分布在键盘的左上角,很容易找到。也可以使用 \u0026lt;c-[\u0026gt; ,它与 \u0026lt;esc\u0026gt; 是等效的。很多朋友,也会把 ,, 映射成 \u0026lt;esc\u0026gt; ,看你爱好。\n我个人是使用 sharpkeys 软件(windows 系统),直接把键盘上的右 shift 键映射成了 esc ,如此全局通用。\n如果,你使用的是 gnu/linux ,可以方便地使用 xmodmap 进行全局的键位映射,更多可以阅读 如何使用 xmodmap 工具进行映射 。\nok,让我们转回 \u0026lt;leader\u0026gt; ,以下是我的一些键位映射配置:\n我把 \u0026lt;space\u0026gt; 空格键设置成了 \u0026lt;leader\u0026gt; 键,默认是 \\ 。如上图所示,我们用它实现了,上个章节中常用的文件及窗口操作。它有很多好处,方便记忆(毕竟是你自己设置的),还可以组合命令。如,我们使用 \u0026lt;space\u0026gt; jj 就可以先展示打开的文件列表,然后再供你输入想要跳转的 buffer 。\n更多配置,可以阅读我的 vim 配置 。\n结语 我自用的 vim ,配置的是很简单的,使用的功能也远不及 vim 所拥有功能的千分之一。如果感兴趣的话,不妨自己去亲自折腾一下。come on 🏃♂️ !\n","date":"2022-07-04","permalink":"https://loveminimal.github.io/posts/vim/","summary":"\u003cp\u003eVim 是 Vi 的升级版,一款功能强大、高度可定制的文本编辑器,它只有一个对手 \u0026ndash; Emacs 。那么,它来自哪里?\u003c/p\u003e","title":"vim"},{"content":" 哎,这让人费解的强迫症……\n博客没写几篇,博客系统倒是折腾了不少,哎,有点本末倒置了,希望这次是最后一次啦(不可能的),单开一篇碎碎念一下前前后后的那些事儿。\n“史前” 最初使用的“日志”系统就是 qq 空间了 😅(我是没有用过人人网的),那个时候一个与众不同的空间装扮是不少人心心念的。陆陆续续用了好多年,就是个情绪收集器,“为赋新词”强说的愁。随着 qq 的废弃,一时冲动就清空了所有内容,不时想起,难免有些遗憾,我的青春啊(冲动是魔鬼~~)。\n后来新浪博客可谓是红红火火,也不知道现在还有几个人用,反正我是连账号都不记得了。\n第三方博客平台 csdn 用的很少,界面太 low 了,广告一大堆,不过这个平台的 seo 做的真的不错,随便搜点什么,它的词条都排在很前面。感觉不少人直接把它当成备忘录用了,君不见,多少博客内容就只一个链接。\n博客园相对前者来说就清爽了很多,而且在一定程度上支持 diy ,可以添加不同的 css 模式,申请之后还可以加入 js 脚本。里面的内容也不错,至少不会是一两句话就自成一篇。\n简书最初的时候是个小可爱,简约美观,编辑内容的时候提供了两种模式,富文本和 markdown 模式,色调也柔和。只不过它的 markdown 解析引擎有点差劲,表现力不那么好。\n……\n第三方博客平台对博客内容的审核有时让人很困扰,ai 不够智能。像是 wordpress 之类的,生态是丰富,但也是折腾,而且编辑内容的体验让人不敢恭维。\n静态博客 markdown 是一种轻量级的标记语言,语法简单,书写清爽,表现力也还可以,用的范围挺广。许多静态博客生成器都只支持 markdown 的内容解析,如 jekyll 和 hexo。hugo 也支持 markdown,但它同时还支持 org mode 。\norg mode 是 emacs 编辑器中的一个插件,它拥有更加强大的表现力和直观性,配合 emacs 简直是纯文本编辑的无双利器。但是,成就它的同样也会束缚它,只有在 emacs 中才能发挥出它最大的威力。我知道有不少人都是为了 org mode 才开始接触并学习了 emacs 。\njekyll 在国内的网络环境下不是很友好,gem 不加镜像是真的慢(pass),虽然也没少折腾,但最初的时候真的是让人心累。hexo 就很好了,它基于 nodejs ,生态丰富完善,除了速度不如 hugo (但我们真的需要那么快的编译速度吗?),其他方面都让人很满意。使用了 hexo 挺长一段时间,甚至还写了一个主题 hexo-theme-zero。\n鉴于对 emacs 的一往情深,当时的博客内容都是用 org mode 写的,配合自制的一个 emacs 插件,把写好的 org mode 转成 markdown 输出到 hexo 进行解析生成。很繁琐是吧,听着就繁琐。\n后来,在阅读 org mode 文档的时候,发现了 ox-publish 这个功能,突然间感觉新世界的大门打开了。当时的念头是,如果可以借助这个功能直接从 org mode 生成页面,不就不需要繁琐的格式转换了嘛,当然后来我了解到已经有不少人已经对这个功能的增强和封装。在实现自定义封装的过程中,我参考和借鉴了不少别人的封装,具体实现 init-site.el,在这里主要实现了几个常用的功能,如文件保存的时候自动编译当前文件,配合一个 simple-httpd 实现当前文件实时预览,其他如删除当前文件对应的 html 文件,同时删除当前文件及其对应的 html 文件等等。配合第三方的 live-server ,封装并写一了一个简单的 bat 命令脚本,双击快速启动服务器,并开启浏览器本地开发。好吧,当时还做了一个视频 闲聊一种使用 org mode 生成静态博客的方式,尬 🤪 。\n如此,使用了有一段时间,也算是满足了个人对于博客的几点要求,如图片尺寸自定义大小等。它同样也有让人不爽的地方,就是慢,比 hexo 还慢,可怜的 elisp 。人性总是不安分的,有时候只是需要一个理由。在 hugo 经过了一段时间迭代之后 ,我又再次迁移到了它,先通览了一遍官方文档,看得我“飘飘然”。在经过了一周多的折腾之后,总算是有了个阶段性的稳定(主要自己给自己整得很疲惫,呵,处女座),hugo-theme-virgo 终于完工了,当然还制作了一个很随意的视频 virgo。\n这次应该能持续比较长的时间,大部分刚需功能在 hugo template 的帮助下都得以很快实现,最初的念头是好的,想着写一个朴素的主题就好。但你知道,这东西一旦开始,就停不来了,你会不断地想要重构它,增加一些功能。总之,就是事前“很会”,事后“很累”,自己折腾自己。目前终归是告一段落了,本地搜索,文章归档,同时具备朴素和炫酷、亮暗切换两种模式。hugo 原生支持 org mode,这非常好,我对它做了一些小小的增强,很爽。迫使我做出迁移的原因在于 windows 下的 emacs 问题多多,对中文字符的渲染尤其让人不爽。当下,对 vscode 进行了适当程度的“调教”,无论是书写 org mode 还是书写 markdown ,都到了差强人意的地步。\n= 呼,都不知道这些碎碎念有什么用,碎碎念能有什么用呢?\n好了,牢骚也发的差不多了,以后要减少无用的折腾,把时间和精力用在更有益的地方 ,比如深入学习、写博客等等。\n结语 事实上,这个章节是后来( 2023-04-17 16:00 )添加,使用 typora 进行编辑的。什么?又换编辑器了?是,也不是 😂。为什么呢?看了 一款 typora 主题 你就明白了。\n","date":"2022-07-02","permalink":"https://loveminimal.github.io/posts/a-theme-making-journey/","summary":"\u003cblockquote\u003e\n\u003cp\u003e哎,这让人费解的强迫症……\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e博客没写几篇,博客系统倒是折腾了不少,哎,有点本末倒置了,希望这次是最后一次啦(不可能的),单开一篇碎碎念一下前前后后的那些事儿。\u003c/p\u003e","title":"一场“疲惫”的主题制作之旅"},{"content":" 这里是 hugo 解析 org mode 内容的展示,如果你使用的是 markdown ,可以跳转 markdown 查看相关样式。\n*想要了解更多,可以去阅读 oxhugo 的文档 。\n以下 markdown 正文内容,摘自 markdown 测试文本 ,我们引用这部分内容,修改为 org mode 语法格式,并添加 org mode 独有的部分功能。\nthis post is originated from here and is used for testing markdown style. this post contains nearly every markdown usage. make sure all the markdown elements below show up correctly.\n\u0026lt;!– more –\u0026gt;\n—\nheaders ** h2 *** h3 **** h4 ***** h5 ****** h6 一篇文章中,通常一级标题只有一个,用来做为当前文章的标题,正文内容层级一般使用二级及其以下层级标题。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 是的,hugo 中内置的 org mode 解析引擎,默认会把正文中的最高层级置为 h2 ,事实上,这样做是正确的,markdown 没有这样做,反而有点奇怪。 \u0026lt;/div\u0026gt;\nenhance org mode 分析之后才发现 hugo 使用的 org mode 引擎是没有对 #+begin_html ... #+end_html 这种原样输出 html 内容的语法做处理的,它会把其内部元素整体做为一个字符串进行输出。好在这个问题很容易处理,我们对它做了一些增强,如下:\n$(\u0026#39;.html-block p\u0026#39;).each((idx, item) =\u0026gt; { item.innerhtml = `${item.innertext}` }) 思路很简单,我们操作转义后的文本,逆向解析为 html 。如此,我们便可以更加灵活的插入相应的 html 标签了,如我们插入图片。\n#+begin_html \u0026lt;img src=\u0026#34;/imgs/bg/cat.jpg\u0026#34; width=\u0026#34;400\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt; #+end_html \u0026lt;img src=\u0026#34;/imgs/bg/cat.jpg\u0026#34; width=\u0026#34;400\u0026#34; style=\u0026#34;float: ;\u0026#34; /\u0026gt;\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 原样输出 html 真的很重要,也更加灵活。 \u0026lt;/div\u0026gt;\nemphasis emphasis, aka underlines, with _asterisks_ or _underscores_. emphasis, aka italics, with /asterisks/ or /underscores/. strong emphasis, aka bold, with *asterisks* or *underscores*. combined emphasis with *asterisks and _underscores_*. strikethrough uses two tildes. +scratch this.+ emphasis, aka underlines, with asterisks or underscores.\nemphasis, aka italics, with asterisks or underscores.\nstrong emphasis, aka bold, with asterisks or underscores.\ncombined emphasis with asterisks and underscores.\nstrikethrough uses two tildes. scratch this.\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 很容易看出,org mode 的表现能力及语法直观形象性,都要比 markdown 好不少,后者连下划线都没有 😿 \u0026lt;/div\u0026gt;\nlists 1. first ordered list item 2. another item - unordered sub-list. 1. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number 1. ordered sub-list - unordered list can use asterisks + or minuses - or pluses + paragraph in unordered list common paragraph with some text. and more text. first ordered list item another item unordered sub-list. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number ordered sub-list unordered list can use asterisks or minuses or pluses paragraph in unordered list common paragraph with some text. and more text.\ninline html \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt;\n\u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt; \u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt; \u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt;\n\u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does not work very well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt;\nlinks [[https://www.google.com][]] [[https://www.google.com][i\u0026#39;m an inline-style link]] https://www.google.com\ni\u0026#39;m an inline-style link\nimages [[https://hexo.io/icon/favicon-196x196.png][click to see png]] ;; 设置图片链接 [[https://hexo.io/icon/favicon-196x196.png]] ;; 直接展示图片 [[./imgs/org-mode.png]] ;; 本地图片 hover to see the title text:\ninline-style:\nclick to see png\n引用本地图片\n或者,你可以直接输出 \u0026lt;img\u0026gt; 标签,更方便。\ncode and syntax highlighting inline code has equal sign around it. 你可以使用 =inline-code= 来包裹行内代码,也可以使用 ~ ,它们转义后的效果是相同的。 source code block 在 org mode 中,使用 \u0026lt;s ,按 tab 展开,很容易得到一组供插入代码块的标签,如下:\n#+begin_src js var s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); #+end_src var s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); 或者,\ns = \u0026#34;python syntax highlighting\u0026#34; print s 有时候,我们不指定语言侦测,也可以直接使用 \u0026lt;e 去展开,如下:\n#+begin_example no language indicated, so no syntax highlighting. but let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. #+end_example no language indicated, so no syntax highlighting. but let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. 如果,你在 emacs 中使用的话,可以直接用上述方式展开。如果,你使用 vscode 的话,这里有一个很不错的插件,如下:\ntables | name | phone | sub1 | sub2 | total | |-------------+--------+------+------+-------| | maple | 134... | 89 | 98 | | | wizard | 152... | 78 | 65 | | | hello world | 123... | 76 | 87 | | | hehe | 157... | 87 | 78 | | name phone sub1 sub2 total maple 134… 89 98 wizard 152… 78 65 hello world 123… 76 87 hehe 157… 87 78 blockquotes #+begin_quote blockquotes are very handy in email to emulate reply text. this line is part of the same quote. #+end_quote blockquotes are very handy in email to emulate reply text.\nthis line is part of the same quote.\nhorizontal rule three or more…\n----- 至少 5 个横折线。\noh essay 在编辑博文的时候,经常想插入一些突然闪现出来的内容,或是于行文无关的吐槽等。为了更好地与正文内容做区分,做了一个定制模式,以 html 格式插入。\n#+begin_html \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt; #+end_html \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt;\nbilibili videos \u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; youbube ? no no no ! bilibili ? yes ! \u0026lt;/div\u0026gt;\n\u0026lt;iframe src=\u0026#34;//player.bilibili.com/player.html?aid=338348299\u0026amp;bvid=bv1fr4y1u7gf\u0026amp;cid=489898794\u0026amp;page=1\u0026#34; scrolling=\u0026#34;no\u0026#34; border=\u0026#34;0\u0026#34; frameborder=\u0026#34;no\u0026#34; framespacing=\u0026#34;0\u0026#34; allowfullscreen=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;/iframe\u0026gt;\nbilibili 真的很不错,体验上比 youtube 要好,内容类型嘛,则没有后者丰富,这个没办法,生态大环境决定的。\n## youtube videos ```markdown \u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau \u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34; alt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt; pure markdown version: [![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) \u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau \u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34; alt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt; pure markdown version: [![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) ``` ","date":"2022-06-29","permalink":"https://loveminimal.github.io/posts/org-mode/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这里是 Hugo 解析 Org Mode 内容的展示,如果你使用的是 Markdown ,可以跳转 \u003ca href=\"/posts/markdown\"\u003eMarkdown\u003c/a\u003e 查看相关样式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\n*想要了解更多,可以去阅读 \u003ca href=\"https://ox-hugo.scripter.co/\"\u003eoxhugo\u003c/a\u003e 的文档 。\u003c/p\u003e","title":"org mode"},{"content":" 这里是 hugo 解析 markdown 内容的展示,如果你使用的是 org mode ,可以跳转 org mode 查看相关样式。\n以下 markdown 正文内容,摘自 markdown 测试文本 ,并添加、修改了一些章节。\nthis post is originated from here and is used for testing markdown style. this post contains nearly every markdown usage. make sure all the markdown elements below show up correctly.\nheaders # h1 ## h2 ### h3 #### h4 ##### h5 ###### h6 alternatively, for h1 and h2, an underline-ish style: # alt-h1 ## alt-h2 h1 h2 h3 h4 h5 h6 alternatively, for h1 and h2, an underline-ish style:\nalt-h1 alt-h2 emphasis emphasis, aka italics, with _asterisks_ or _underscores_. strong emphasis, aka bold, with **asterisks** or **underscores**. combined emphasis with **asterisks and _underscores_**. strikethrough uses two tildes. ~~scratch this.~~ emphasis, aka italics, with asterisks or underscores.\nstrong emphasis, aka bold, with asterisks or underscores.\ncombined emphasis with asterisks and underscores.\nstrikethrough uses two tildes. scratch this.\nlists 1. first ordered list item 2. another item - unordered sub-list. 1. actual numbers don\u0026#39;t matter, just that it\u0026#39;s a number 1. ordered sub-list 1. and another item. you can have properly indented paragraphs within list items. notice the blank line above, and the leading spaces (at least one, but we\u0026#39;ll use three here to also align the raw markdown). to have a line break without a paragraph, you will need to use two trailing spaces. note that this line is separate, but within the same paragraph. (this is contrary to the typical gfm line break behaviour, where trailing spaces are not required.) - unordered list can use asterisks * or minuses - or pluses * paragraph in unordered list for example like this. common paragraph with some text. and more text. first ordered list item another item unordered sub-list. actual numbers don\u0026rsquo;t matter, just that it\u0026rsquo;s a number\nordered sub-list\nand another item.\nyou can have properly indented paragraphs within list items. notice the blank line above, and the leading spaces (at least one, but we\u0026rsquo;ll use three here to also align the raw markdown).\nto have a line break without a paragraph, you will need to use two trailing spaces.\nnote that this line is separate, but within the same paragraph.\n(this is contrary to the typical gfm line break behaviour, where trailing spaces are not required.)\nunordered list can use asterisks or minuses or pluses paragraph in unordered list\nfor example like this.\ncommon paragraph with some text. and more text.\ninline html \u0026lt;p\u0026gt;to reboot your computer, press \u0026lt;kbd\u0026gt;ctrl\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;alt\u0026lt;/kbd\u0026gt;+\u0026lt;kbd\u0026gt;del\u0026lt;/kbd\u0026gt;.\u0026lt;/p\u0026gt; to reboot your computer, press ctrl+alt+del.\n\u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;definition list\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;is something people use sometimes.\u0026lt;/dd\u0026gt; \u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt; definition list\ris something people use sometimes.\r\u0026lt;dt\u0026gt;markdown in html\u0026lt;/dt\u0026gt;\r\u0026lt;dd\u0026gt;does *not* work **very** well. use html \u0026lt;em\u0026gt;tags\u0026lt;/em\u0026gt;.\u0026lt;/dd\u0026gt;\rlinks [i\u0026#39;m an inline-style link](https://www.google.com) [i\u0026#39;m an inline-style link with title](https://www.google.com \u0026#34;google\u0026#39;s homepage\u0026#34;) [i\u0026#39;m a reference-style link][arbitrary case-insensitive reference text] [i\u0026#39;m a relative reference to a repository file](../blob/master/license) [you can use numbers for reference-style link definitions][1] or leave it empty and use the [link text itself] some text to show that the reference links can follow later. [arbitrary case-insensitive reference text]: https://hexo.io [1]: https://hexo.io/docs/ [link text itself]: https://hexo.io/api/ i\u0026rsquo;m an inline-style link\ni\u0026rsquo;m an inline-style link with title\ni\u0026rsquo;m a reference-style link\ni\u0026rsquo;m a relative reference to a repository file\nyou can use numbers for reference-style link definitions\nor leave it empty and use the link text itself\nsome text to show that the reference links can follow later.\nimages hover to see the title text: inline-style: ![alt text](https://hexo.io/icon/favicon-196x196.png \u0026#39;logo title text 1\u0026#39;) reference-style: ![alt text][logo] [logo]: https://hexo.io/icon/favicon-196x196.png \u0026#39;logo title text 2\u0026#39; hover to see the title text:\ninline-style:\nreference-style: code and syntax highlighting inline code has back-ticks around it.\nvar s = \u0026#39;javascript syntax highlighting\u0026#39;; alert(s); s = \u0026#34;python syntax highlighting\u0026#34; print s no language indicated, so no syntax highlighting.\rbut let\u0026#39;s throw in a \u0026lt;b\u0026gt;tag\u0026lt;/b\u0026gt;. tables | | ascii | html | |------------------|---------------------------------|-------------------------------| | single backticks | `\u0026#39;isn\u0026#39;t this fun?\u0026#39;` | \u0026#39;isn\u0026#39;t this fun?\u0026#39; | | quotes | `\u0026#34;isn\u0026#39;t this fun?\u0026#34;` | \u0026#34;isn\u0026#39;t this fun?\u0026#34; | | dashes | `-- is en-dash, --- is em-dash` | -- is en-dash, --- is em-dash | ascii html single backticks 'isn't this fun?' \u0026lsquo;isn\u0026rsquo;t this fun?\u0026rsquo; quotes \u0026quot;isn't this fun?\u0026quot; \u0026ldquo;isn\u0026rsquo;t this fun?\u0026rdquo; dashes -- is en-dash, --- is em-dash \u0026ndash; is en-dash, \u0026mdash; is em-dash colons can be used to align columns.\n| tables | are | cool | |---------------|:-------------:|-----:| | col 3 is | right-aligned | | | col 2 is | centered | | | zebra stripes | are neat | | tables are cool col 3 is right-aligned col 2 is centered zebra stripes are neat the outer pipes (|) are optional, and you don\u0026rsquo;t need to make the raw markdown line up prettily. you can also use inline markdown.\n| markdown | less | pretty | |----------|-----------|------------| | _still_ | `renders` | **nicely** | | 1 | 2 | 3 | markdown less pretty still renders nicely 1 2 3 you can find more information about latex mathematical expressions here.\nblockquotes blockquotes are very handy in email to emulate reply text. this line is part of the same quote.\nquote break.\nthis is a very long line that will still be quoted properly when it wraps. oh boy let\u0026rsquo;s keep writing to make sure this is long enough to actually wrap for everyone. oh, you can put markdown into a blockquote.\nhorizontal rule three or more\u0026hellip;\n--- hyphens --- asterisks --- underscores hyphens\nasterisks\nunderscores\nline breaks here\u0026#39;s a line for us to start with. this line is separated from the one above by two newlines, so it will be a _separate paragraph_. this line is also a separate paragraph, but... this line is only separated by a single newline, so it\u0026#39;s a separate line in the _same paragraph_. here\u0026rsquo;s a line for us to start with.\nthis line is separated from the one above by two newlines, so it will be a separate paragraph.\nthis line is also a separate paragraph, but\u0026hellip; this line is only separated by a single newline, so it\u0026rsquo;s a separate line in the same paragraph.\nthis is a regular paragraph. \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;foo\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; this is another regular paragraph. this is a regular paragraph.\nfoo\rthis is another regular paragraph.\noh essay 在编辑博文的时候,经常想插入一些突然闪现出来的内容,或是于行文无关的吐槽等。为了更好地与正文内容做区分,做了一个定制模式,以 html 格式插入。\n\u0026lt;div class=\u0026#34;oh-essay\u0026#34;\u0026gt; 这就是我们插入的随笔喽…… blablablabla…… \u0026lt;/div\u0026gt; 这就是我们插入的随笔喽…… blablablabla……\rbilibili videos youbube ? no no no ! bilibili ? yes ! bilibili 真的很不错,体验上比 youtube 要好,内容类型嘛,则没有后者丰富,这个没办法,生态大环境决定的。\n## youtube videos\r```markdown\r\u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau\r\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34;\ralt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt;\rpure markdown version:\r[![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau)\r\u0026lt;a href=\u0026#34;https://www.youtube.com/watch?feature=player_embedded\u0026amp;v=arted4rniau\r\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;https://img.youtube.com/vi/arted4rniau/0.jpg\u0026#34;\ralt=\u0026#34;image alt text here\u0026#34; width=\u0026#34;240\u0026#34; height=\u0026#34;180\u0026#34; border=\u0026#34;10\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt;\rpure markdown version:\r[![image alt text here](https://img.youtube.com/vi/arted4rniau/0.jpg)](https://www.youtube.com/watch?v=arted4rniau) ","date":"2022-06-29","permalink":"https://loveminimal.github.io/posts/markdown/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这里是 Hugo 解析 Markdown 内容的展示,如果你使用的是 Org Mode ,可以跳转 \u003ca href=\"/posts/org-mode\"\u003eOrg Mode\u003c/a\u003e 查看相关样式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以下 markdown 正文内容,摘自 \u003ca href=\"https://github.com/hexojs/hexo-theme-unit-test/edit/master/source/_posts/markdown.md\"\u003eMarkdown 测试文本\u003c/a\u003e ,并添加、修改了一些章节。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis post is originated from \u003ca href=\"https://gist.github.com/apackeer/4159268\"\u003ehere\u003c/a\u003e and is used for testing markdown style. This post contains nearly every markdown usage. Make sure all the markdown elements below show up correctly.\u003c/p\u003e","title":"markdown"},{"content":" 🚨 后又陆陆续续加了些辅助功能,可以点击下载最新完整的search.js 源码(另存后使用编辑器打开就不中文乱码了)。\n更新日志 2022-07-13 11:02 修改了站点内容结构解析方式,以解决随着文章数量增长导致搜索性能下降的问题 简介 近来稍闲,实现了一个 hugo 本地搜索的小功能,分享一下 🍧 。\n这个功能写了两遍,第一次不知道怎么着就在一个临时分支(切换回主分支会自动消失的那种 😠 )中开发了,然后就没什么然后了…… 感觉是 ide 的锅,对,就是它的,不是也要是!\r让我们先来看一下,它可以做什么吧:\n内容实时搜索; 搜索内容摘要显示; 搜索词高亮。 都是一些搜索时常用的功能。稍后,我们来看一下上述功能的一些细节,以及开发过程中的一些糗事 😄 。你也可以,先在这里体验一下它的大概使用效果 search 。\n使用 如何实时获取站点的所有内容呢?这里有两个方面,一就是获取站点的所有页面内容,二是实时获取。\n搜索模板页 一个大概的思路,就是创建一个模板页,如 _search.html 文件,利用 hugo 模板本身的变量(如 .site)来获取站点所有的页面内容。\n\u0026lt;div class=\u0026#34;container-search\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;data\u0026#34; style=\u0026#34;display: none;\u0026#34;\u0026gt; \u0026lt;!-- 遍历所有的站点页面 --\u0026gt; {{ range where .site.pages \u0026#34;kind\u0026#34; \u0026#34;section\u0026#34; }} {{ if ne .title \u0026#34;secrets\u0026#34; }} [{{ range .pages }} {{- dict \u0026#34;title\u0026#34; (lower .title) \u0026#34;permalink\u0026#34; .permalink \u0026#34;date\u0026#34; (.date | time.format \u0026#34;2006-01-02\u0026#34;) \u0026#34;summary\u0026#34; .summary \u0026#34;content\u0026#34; (lower .plain) | jsonify -}},{{ end }}] {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;!-- 搜索框 --\u0026gt; \u0026lt;div id=\u0026#34;search\u0026#34;\u0026gt; \u0026lt;!-- 🔎 --\u0026gt; \u0026lt;span class=\u0026#34;sc-icon\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;/imgs/icons/search.svg\u0026#34; width=\u0026#34;48\u0026#34;\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;span id=\u0026#34;sc-clear\u0026#34; onclick=\u0026#34;clearinputval()\u0026#34;\u0026gt;✖\u0026lt;/span\u0026gt; \u0026lt;input id=\u0026#34;sc-input\u0026#34; oninput=\u0026#34;search()\u0026#34; type=\u0026#34;text\u0026#34; placeholder=\u0026#34;here search search...\u0026#34; /\u0026gt; \u0026lt;div id=\u0026#34;sc-res\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 加载所需搜索脚本 --\u0026gt; \u0026lt;script src=\u0026#34;/js/search.js\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 当然,你可以进行按需进行一些修改,过滤掉一些,你不想被搜索到的页面。\n下面,我们来看一下核心的 js 搜索脚本 search.js (这里我们放在了 static/js/search.js)。其中的注释,是开发过程中帮助记忆和理清思路和一些碎碎念,不要在意。😅\n解析站点页面内容 let data = document.queryselector(\u0026#39;#data\u0026#39;).innertext.trim(); data = data.slice(0, data.length - 2) + \u0026#39;]\u0026#39;; let map = json.parse(data); 我们使用 hugo 模板提供的相关功能,组织站点内容映射,以文本形式放在元素 #data 中,后反序列化以得到当前站点所有页面内容的一个集合 map 。\n如何搜索 我们的核心就是搜索函数 search() 。在 map 的生成过程中,我们对信息串的 content 做了一些处理,如将所有字符转化为小写。在 search() 中,我们也对搜索词 scval 进行同样的处理,以实现不区分大小写的内容搜索。\n在这之前,我们定义了另一个辅助函数,用来返回搜索词 scval 在对应页面中出现的所有索引位置 _arrindex 。\nfunction scanstr(content, str) { // content 页面内容信息串 let index = content.indexof(str); // str 出现的位置 let num = 0; // str 出现的次数 let arrindex = []; // str 出现的位置集合 while(index !== -1) { arrindex.push(index); num += 1; index = content.indexof(str, index + 1); // 从 str 出现的位置下一位置继续 } return arrindex; } 有它 scanstr 我们就可以方便的知道搜索词都出现在了哪里,以方便后续的内容摘要截取及高亮。\n内容摘要截取\n通过 scanstr ,我们得到了搜索词在页面内容出现的所有位置,我们默认截取每个位置前后 100 个字符长度(后续我称之后 截取半径)的内容进行罗列展示(可以自定义长度)。这里,我们做了一些小小的优化操作,当后续搜索词的索引位置与当前搜索词的索引位置之差仍小于截取半径的时候,将不再对该位置前后内容进行截取(因为它已经包含在了之前的截取内容中),以避免大量重复性内容的展示。\n具体逻辑,还是直接看代码吧,其实不需要了解,因为它并没有什么太大的通用性,都是对字符串的蹂躏和被蹂躏。😿\nlet scinput = document.queryselector(\u0026#39;#sc-input\u0026#39;); let scres = document.queryselector(\u0026#39;#sc-res\u0026#39;) let scval = \u0026#39;\u0026#39;; scinput.focus(); // 自动聚集搜索框 function search() { let post = \u0026#39;\u0026#39;; scval = scinput.value.trim().tolowercase(); map.foreach(item =\u0026gt; { if (!scval) return; if (item.content.indexof(scval) \u0026gt; -1) { let _arrindex = scanstr(item.content, scval); let strres = \u0026#39;\u0026#39;; let _radius = 100; // 搜索字符前后截取的长度 let _strstyle0 = \u0026#39;\u0026lt;span style=\u0026#34;background: yellow;\u0026#34;\u0026gt;\u0026#39; let _strstyle1 = \u0026#39;\u0026lt;/span\u0026gt;\u0026#39; let _strseparator = \u0026#39;\u0026lt;hr\u0026gt;\u0026#39; // 统计与首个与其前邻的索引(不妨称为基准索引)差值小于截取半径的索引位小于截取半径的索引的个数 // 如果差值小于半径,则表示当前索引内容已包括在概要范围内,则不重复截取,且 // 下次比较的索引应继续与基准索引比较,直到大于截取半径, _count重新置 为 0; let _count = 0; for (let i = 0, len = _arrindex.length; i \u0026lt; len; i++) { let _idxitem = _arrindex[i]; let _relidx = i; // 如果相邻搜索词出现的距离小于截取半径,那么忽略后一个出现位置的内容截取(因为已经包含在内了) if (_relidx \u0026gt; 0 \u0026amp;\u0026amp; (_arrindex[_relidx] - _arrindex[_relidx - 1 - _count] \u0026lt; _radius)) { _count += 1; continue; } _count = 0; // 概要显示 // _startidx, _endidx 会在超限时自动归限(默认,无需处理) strres += _strseparator; let _startidx = _idxitem - _radius + (_relidx + 1) * _strseparator.length; let _endidx = _idxitem + _radius + (_relidx + 1) * _strseparator.length; strres += item.content.substring(_startidx, _endidx); } // 进一步对搜索摘要进行处理,高亮搜索词 let _arrstrres = scanstr(strres, scval); // console.log(_arrstrres) for (let i = 0, len = _arrstrres.length; i \u0026lt; len; i++) { let _idxitem = _arrstrres[i]; let _realidx = i; strres = strres.slice(0, (_idxitem + _realidx * (_strstyle0.length + _strstyle1.length))) + // 当前索引位置之前的部分 _strstyle0 + scval + _strstyle1 + strres.slice(_idxitem + scval.length + _realidx * (_strstyle0.length + _strstyle1.length)); // 之后的部分 } post += ` \u0026lt;div class=\u0026#34;item\u0026#34; \u0026gt; \u0026lt;a href=\u0026#34;${item.permalink}\u0026#34;\u0026gt; \u0026lt;span\u0026gt;📄\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;date\u0026#34;\u0026gt;${item.date}\u0026lt;/span\u0026gt; \u0026lt;span\u0026gt;${item.title}\u0026lt;/span\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;div\u0026gt;${strres}\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ` } }) let res = `\u0026lt;div class=\u0026#34;list\u0026#34;\u0026gt;${post}\u0026lt;/div\u0026gt;`; scres.innerhtml = res; 高亮显示\n在遍历获取了所有的搜索摘要 strres 后,我们需要对其进行进一步的处理,以实现搜索词高亮显示,如下:\n同样,这也是和字符串长度之间的征战,没什么太大意思。\n最后 只需要把 search.js 放在 static/js/ 目录下,或者其他你喜欢的路径,但要保证 _search.html 可以正常引用。再使用 hugo 创建一个对应的 search.md 页面,用来启用 _search.html 模板即可。比如我把 _search.html 模板放在了 single.html 模板中,并且设置只有 /search 路径才加载这部分内容。\n{{ $issearch := eq .title \u0026#34;search\u0026#34;}}\r{{ if $issearch }}\r{{- partial \u0026#34;partials/_search.html\u0026#34; . -}}\r{{ end }} 上述代码块中的代码,是完整可用的,复制粘贴即可。\n","date":"2022-06-23","permalink":"https://loveminimal.github.io/posts/hugo-local-search/","summary":"\u003cblockquote\u003e\n\u003cp\u003e🚨 后又陆陆续续加了些辅助功能,可以点击下载最新完整的\u003ca href=\"https://ovirgo.com/js/search.js\"\u003esearch.js\u003c/a\u003e 源码(另存后使用编辑器打开就不中文乱码了)。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"一种简单的 hugo 本地搜索实现"},{"content":" go 之 1、2、3、4、5…… 不得不说,golang 的语法真的挺怪异的 😢\ntodo 快速简览","date":"2022-06-08","permalink":"https://loveminimal.github.io/posts/go/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGo 之 1、2、3、4、5…… 不得不说,Golang 的语法真的挺怪异的 😢\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cimg src=\"/posts/go/imgs/2.webp\" width=\"320\" /\u003e","title":"go"},{"content":" 很强大,也让人很头大…… 这只是一个精简的摘要,相信我,即使总结的再全面,一段时间之后,也需要借助参考手册,才能正确的使用它。\n简介 正则表达式是什么?一个表达式,它定义了一种规则,用来匹配(符合规则的)字符串。\n正则表达式本身也是用字符串表示的,如何表示呢?\n必须搭配例子一起食用,味道才好 😸 ……\r简单匹配 _1.精确匹配\n在正则表达式中,如果直接给出字符,就是精确匹配。其他,还有:\n元字符 说明 \\d 匹配一个数字 \\w 匹配一个字母或数字 \\s 匹配一个空白符 . 匹配任意字符 当然,还有其他的,用的时候再查好了,反正是记不住的。\r看一些示例:\n'00\\d' 可以匹配 '007' ,但无法匹配 +'00a'+ ; '\\d\\d\\d' 可以匹配 '010' ; '\\w\\w\\d' 可以匹配 'py3' ; 'py.' 可以匹配 'pyc'、 'pyo'、 'py!' 等等。 _2.匹配变长字符\n元字符 说明 * 匹配任意个字符 (0 或 0+) + 匹配至少一个字符 (1 或 1+) ? 匹配 0 或 1 个字符 {n} 匹配 n 个字符 {m,n} 匹配 (m 到 n) 个字符 看一些示例:\n\\d{3} 表示匹配 3 个数字,如 '010' ; \\s+ 表示至少有一个空格,可以匹配 ' '、 ' ' 等; \\d{3,8} 表示匹配 3-8 个数字,如 '123'、 '12345'、 '12345678' 等。 如果要匹配 010-12345 这样的号码呢?由于 - 是特殊字符 ,在正则表达式中,要用 \\ 转义,故上述的答案是 \\d{3}\\-\\d{3-8} 。\n如果,还不够,继续……\n_3. 更精确的匹配\n要做更精确的匹配,可以用 [] 表示范围,如 [0-9a-za-z\\_] 可匹配一个数字、字母或者下划线。 其他,有:\n元字符 说明 (a|b) 匹配 a 或 b ^ 表示行的开头 $ 表示行的结束 再来看一些示例:\n[a-za-z\\_][0-9a-za-z\\_]* 可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串(看,这不就是 python 的合法变量嘛); (p|p)ython 可以匹配 'python' 或者 'python' 。 进阶 _4. 分组\n除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用 () 表示的就是要提取的分组(group)。\n比如, ^(\\d{3})-(\\d{3-8})$ 分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码。\nm = re.match(r\u0026#39;^(\\d{3})-(\\d{3-8})$\u0026#39;, \u0026#39;010-12345\u0026#39;) m.group(0) # \u0026#39;010-12345\u0026#39; m.group(1) # \u0026#39;010\u0026#39; m.group(2) # \u0026#39;12345\u0026#39; # group(0) 永远是与整个正则表达式相匹配的字符串 # group(1)、group(2)... 表示第 1、2 个子串 _5.贪婪匹配\n需要注意的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符,加个 ? 可以切换成非贪婪匹配。\n比如,我们要匹配出数字后面的 0 :\nre.match(r\u0026#39;^(\\d+)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() # → (\u0026#39;102300\u0026#39;, \u0026#39;\u0026#39;) 由于 \\d+ 默认采用贪婪匹配,直接把后面的 0 全部匹配了,结果 0* 只能匹配空字符串了。\n必须让 \\d+ 采用非贪婪匹配(也就是尽可能少的匹配),才能把后面的 0 匹配出来,如下:\nre.match(r\u0026#39;^(\\d+?)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() # → (\u0026#39;1023\u0026#39;, \u0026#39;00\u0026#39;) 结语 以上内容主要来源于 廖雪峰老师的博客中关于正则表达式的章节 ,很短但很精致。如果,你想进一步练习的话,推荐:\nhttps://regexr.com/ https://regexr-cn.com/ (中文站) ","date":"2022-05-24","permalink":"https://loveminimal.github.io/posts/regexp/","summary":"\u003cblockquote\u003e\n\u003cp\u003e很强大,也让人很头大…… 这只是一个精简的摘要,相信我,即使总结的再全面,一段时间之后,也需要借助参考手册,才能正确的使用它。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"正则表达式"},{"content":"\rgtd 分为 =横向管理 和 =纵向管理 两个方面。\n= 横向管理,其实就是事物在时空层面上的大搜集;纵向管理,则是事物在某一时空节点上的纵深。 横向收集,纵向执行! 纵向管理偏重在横向管理的行动阶段!*\n横向管理 横向管理有五个阶段:收集、分析、组织管理、检查(反馈)、行动。\n1. 收集:填充工作篮 物理空间; 精神空间。 关键在于百分之百地捕获一切未尽事宜,以最快的速度收集下来,放入工作篮。\n保持极简主义生活方式, =断、舍、离 ,可以从根本上大大缩短收集时间。\n= 好的生活方式,可以让一个人更好地沉浸在某一种生活状态,以更利于个体的成长和发展。*\n= 在纯粹的精神世界中,一切起于 念! 取舍之间的平衡点在于有序、足够、简约。极简主义中的核心虽然是断、舍、离,但在执行过程中,“度”很重要,而在保持过程中,“有序”很关键!*\n2. 分析:清空工作篮 清空工作篮的本质在于确定每一项工作的内容和实质,判断其是否需要采取行动。\n_不需要采取行动:\n垃圾 → 抛进垃圾桶; 孵化器 → 未来某一天可能会做; 参考资料 → 归档保存。 _需要采取行动的:\n小于 2 分钟的立即处理; 大于 2 分钟的: 委托他人: 等待; 日程表(有时间要求的)。 自己执行(在管理阶段列清单): 日程表(有时间要求的); 下一步行动清单。 项目: 制定项目计划。 注意,这一步中,并不需要采取任何实际的行动,关键在于合理分类,分类的时候应遵循以下原则:\n自上而下; 逐条处理; 机会均等。 = 一切都是为了尽可能快地解除‘焦虑’状态,快速地为未尽事宜分类,归入清单。*\n3. 管理 对分类后的条目进行制定 _具体的行动方案 :\n垃圾 → 删除; 未来可期 → 孵化器; 参考资料 → 归档保存供查阅; 小于 2 分钟 → 立即处理; 下一步行动 → =必须是一目了然的具体行动 ; 日程表 → 只允许放置有具体时间要求的任务; 项目 → 分解为具体的行动、安排日程。 注意, _日程表 是十分神圣的,它和 _下一步行动 共同构成了每日管理的核心。\n四象限工作法是个不错的标准:\n= 在“分析”阶段,我们要做的是快速判断清空收集篮,在“管理”阶段,则需要认真分析,依照不同类型事物的标准进行切实的处理。*\n= 这一步无疑是 重中之重 ,只有科学的、具体化的、可执行的行动,才是解决问题的关键,才是改造世界的直接力量。*\n4. 检查 新的情况不断产生,每天的工作安排也必须时常回顾并做出相应调整,以保证 gtd 的尽收一切和全面管理。\n如何检查呢?\n\u0026hellip;\n每一天:\n以 _日程表 作为开始,处理好日程表中紧要的任务; 之后,查阅 _下一步行动 清单和 _孵化器 。 =注意,定期(如每周)更新清单是至关重要的。\n= 养成习惯,让享受美好的事情像呼吸一样简单和自然!早九晚九必检一次,中间实时更新即可。\n5. 行动 选择最佳方案,如何决策?\n_在某一时刻, 4 个模式:\n(地)环境; (时)有多少时间; (人)有多少精力; (事)重要性。 = 天时,地利,人和,重要性!*\n_评估每日工作, 3 种模式:\n(预定)处理事先安排好的工作; (突发)处理随时冒出来的事件; (成长)定义你自己的工作。 = 一切都在计划之中,留些弹性给突发事件。多管齐下,生活才会更有趣。还是要每天都小总一下才好!\n_回顾工作的 6 个标准:\n目前的行动; 当前的工作(亟待处理的事项,短期成效); 责任范围(工作、生活、个人成长); 1~2 年的目标(各个领域的成就); 3~5 年的展望(着眼全局,纵观趋势); 整个生活的全景(终极的人生意义)。 = 所有的评估和回顾,终极目标都是了解‘真我’!预期的有无和意义,便在于这个求‘真’的过程之中,它是自然而然的,是会自己冒出来,而你也注定要解决的问题。*\n= 这几个标准特别适合,周、月、季、年、大周期汇总,看,一切总结都是建立在实践之上的,空想大道是没有什么意义的。\n= 其中第一类决策模式是发生在具体行动中的,后两者则都归属于检查反馈(即融于日常的生活习惯之中),可见规律性、周期性的“反馈”的重要性!反馈、检查 ✔️\n高效能人士的七个习惯系统地讲解这个体系:\n= 组织管理和行动这两步是很关键的,好的方案为行动的实施提供了便利,事半功倍,这两个环节和纵向管理又是密切相关的,不可分离的。*\n纵向管理 需要更严格地控制某一项工作,确保行动方案切实可行时,便是纵向管理的用武之地。\n= 就个人而言,横向管理使用的比较多,基本已经融入日常生活当中。纵向管理,就用的少了些,可能还是有时候太浮躁了,要静下心来,多思、多想,思而后行,行而有得。\n纵向管理也有五个阶段:目的/原则、前景/结果、集思广益、组织协调、下一步行动。\n= 看!纵向管理,是正式的组织管理和行动,它是融入其中的!*\n1. 目的/原则 目的是什么?是否清晰而具体?\n界定成功(要实现什么); 集结资源(客观环境); 激发动机(主观能动); 阐明重点(主要矛盾); 拓宽选择(可能); 澄清原则(本心)。 = 过多的解释和条目,往往让人很烦!总结来说,就是结合自身内外条件,想要阶段性的取得一种什么结果,其他一切都是围绕这个中心的!为什么要‘界定’呢?要预演!脑海中的预演可以更好、更早地发现行动过程中可能出现的问题,防患于未然,不仅大大地节省了可能要花费的并无意义用处的时间和精力,对行动的激励所产生的正面效应也是极大的。“之所以打醋,不就是为了这点饺子!”*\n= 凡事计划,很少有能一蹴而就的,多是分阶段性的,对于成功的界定就很重要,正所谓,心向高远,目视脚下,路途长而艰、寂而孤,就更需要阶段性的鼓舞与激励!由内而外,反求诸己,利用客观环境,发挥主观能动,突破经验本本,分析主次轻重,扩展思维,明晰本心,澄清并坚守原则。*\n= 这个过程是繁琐的,却是非常重要的!认真思考要达成的目标,分析客观条件、主观现实,实事求是地分析矛盾及事物可能的走向,明晰本心真我。*\n2. 前景/结果 聚焦; 阐明结果。 = 能吃到的果子,才能真正解渴!\n量子吸引力法则:\n量子力学理论中重要的一点就是振动频率相同的东西,会互相吸引而且引起共鸣。我们的意念、思想、情绪具有可感知的能量,而我们的脑电波不断产生振动频率,只要有振动,就会影响其他同样在振动的事物。我们的大脑就是这个世界上最强的“磁铁”,我们的起心动念,无时不在向宇宙发出信号,和你的脑电波振动频率相同的东西,会统统被你吸引过来。你生活中的一切,都是你自己吸引来的。佛陀在 2500 多年前所说的“唯心所见,唯识所变”,便是这个哲理。\n= 意识的更新。*\n3. 集思广益 头脑风暴,启动思维,打破常规的思考模式,基本原则:\n不判断,不质疑,不评估,不批判; 追求数量,不求质量; 把分析组织工作置于次要的位置,放在下个阶段处理。 = 在头脑中搜集所有的可能性,不放过任何蛛丝马迹。不唯经验、教条,实事求是!突然更深地认识到,以上种种,在《毛泽东选集》中都有讲到…… 教员威武!*\n4. 组织协调 _组织管理的要素:\n明确意义重大的事件; 排序(构成因素、先后顺序、重要程度); 必要程度的详述。 5. 下一步行动 如果一项工作具有可操作性,其下一步行动方案就必须予以落实,必须有切实可行的具体行动。\n= 实践,是实现一切计划的根本途径!*\n= 综上来看,纵向管理就是界定成功(明晰目标)、预演前景结果、想尽可能多的方案,从中取优协调,进行形成一个科学合理的、可明确执行的、有意义的行动方案,以保证具体执行过程顺利,达到预期目标!*\n","date":"2022-05-01","permalink":"https://loveminimal.github.io/posts/get-things-done/","summary":"\u003cimg src=\"/posts/get-things-done/imgs/gtd-1.jpg\" width=\"800\" style=\"border-radius: 6px;\" /\u003e\r\n\u003cp\u003eGTD 分为 \u003cem\u003e=横向管理\u003c/em\u003e 和 \u003cem\u003e=纵向管理\u003c/em\u003e 两个方面。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 横向管理,其实就是事物在时空层面上的大搜集;纵向管理,则是事物在某一时空节点上的纵深。 \u003cstrong\u003e横向收集,纵向执行!\u003c/strong\u003e 纵向管理偏重在横向管理的行动阶段!*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"gtd 管理系统"},{"content":" = 好的吧,我承认,对于 c 语言,我有一种莫名其妙的偏爱!是因为可以直接操作内存吗?或许是!可能吧,计算机有什么神奇的东西呢?最终就是围绕那几个概念在来回打转!\n编译器 1 = 计算机说:“无论你怎么写,怎么编,怎么译,最终都要爷能看得懂才行!😈”\n什么是编译器 可执行程序(executable program) 的内部是一系列计算机指令和数据的集合,它们是二进制形式的,cpu 可以直接识别,但对于程序员来说几乎不具备可读性。\n比如,在屏幕上输出“vip 会员”,c 语言的写法为: puts(\u0026quot;vip 会员\u0026quot;); ,但其二进制写法如下:\n直接使用二进制指令编程对程序员来说简直是噩梦!\n于是,编程语言就诞生了。比如,c 语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于 cpu 来说,c 语言代码就是天书(cpu 只认识几百个二进制形式的指令)!\n= 💻:“不好意思,爷只认二进制!”\n这就需要一个工具,将 c 语言代码转换成 cpu 能够识别的二进制指令(即可执行程序),这个工具是一个特殊的软件,叫做 编译器(compiler) 。\n用来保存代码文件叫做 源文件 ,它就一个纯文本文件,内部并没有特殊格式(其后缀仅仅是为了表明文件中保存的是某种语言的代码,易于程序员区分和编译器识别)。\n= 就一纯文本文件 📝\n在实际开发中,程序员将代码分门别类地放到多个源文件中。\nide(集成开发工具) 会为每一个程序都创建一个专门的目录,将用到的所有文件都集中到这个目录下进行管理。 不同的程序对应不同的项目类型(i.e. 工程类型) ,不同的工程类型本质上是对 ide 各个参数的不同设置 。当然,我们也可以创建一个空白的工程类型,然后自己去设置各种参数(不过一般没有会这样做)。\n源代码要经过编译(compile)和链接(link)两个过程才能变成可执行文件。\n编译器一次只能编译一个源文件(如果当前程序包含了多个源文件,那么就需要编译多次),编译器每次编译的结果是产生一个中间文件(不是最终的可执行文件,但已经非常接近可执行文件了,它们都是二进制格式,内部结构也非常相似)。\n将当前程序的所有中间文件以及系统库组合在一起,才能形成最终的可执行文件,这个组合的过程就叫做 链接(link) ,完成链接功能的软件叫做链接器(linker)。\n不管有多少个源文件(哪怕只有一个),都必须经过编译和链接两个过程才能生成可执行文件。(为什么呢?因为你至少还需要和系统库组合。)\n综上可以发现,一个完整的编程过程是:\n编写源文件(保证代码语法正确,否则编译不通过); 预处理(processing); 编译(compile 将源文件转换为目标文件); 汇编(assembly); 链接(linking 将目标文件和系统库组合在一起,转换为可执行文件); 运行(可以检验代码的正确性)。 默认情况下, gcc 指令会直接将源代码转变为可执行代码(2-5 四个过程),且不会保留各个阶段产生的中间文件。\ngcc 是什么 gcc 编译器是 linux 系统下最常用的 c/c++ 编译器,大部分 linux 发行版中都会默认安装。\n早期的 gcc 全拼为 gnu c compiler ,最初定位确实只用于编译 c 语言。经过不断迭代扩展,gcc 现在还可以处理 c++、go、ojbect-c 等多种编译语言编写的程序,故其全称被重新定义为 gnu compiler collection,即 gnu 编译器套件。\n👉 更多 gcc 和 clang / llvm 的区别\n可以通过 gcc --help 查看其常用指令选项如下:\n--version display compiler version information.\r-std=\u0026lt;standard\u0026gt; assume that the input sources are for \u0026lt;standard\u0026gt;.\r-e preprocess only; do not compile, assemble or link.\r-s compile only; do not assemble or link.\r-c compile and assemble, but do not link.\r-o \u0026lt;file\u0026gt; place the output into \u0026lt;file\u0026gt;.\r-pie create a dynamically linked position independent\rexecutable.\r-shared create a shared library.\r-x \u0026lt;language\u0026gt; specify the language of the following input files.\rpermissible languages include: c c++ assembler none\r\u0026#39;none\u0026#39; means revert to the default behavior of\rguessing the language based on the file\u0026#39;s extension. 前面说过 gcc 是支持编译多种编程语言的,可以通过 -x 选项指定要编译的语言类型,如 gcc -xc++ xxx 表示以编译 c++ 代码的方式编译 xxx 文件。\n使用 gcc 编译器编译 c 或者 c++程序,必须经历 4 个过程: 预处理 → 编译 → 汇编 → 链接 (通常 gcc/g++ 支持该过程的自动化)。\ng++ 是什么?可以认为 g++ →(等价于) gcc -xc++ -lstdc++ -shared-libgcc (因为 gcc 不会自动引入 c++ 相关的库,必须手动引入)。\n*用于手动指定链接环节中程序可以调用的库文件,如 -lstdc++ ,不建议 -l 和 library 之间有空格。\n默认情况下, gcc 指令会一气呵成将源代码历经这 4 个过程转变为可执行代码,且不会保留各个阶段产生的中间文件。\n如果我们想查看这 4 个阶段各自产生的中间文件,该怎么办呢?最简单直接的方式就是对源代码进行“分步编译”。即控制 gcc 编译器逐步对源代码进行预处理、编译、汇编及链接操作。\ngcc/g++ 指令选项 功能 -e 预处理指定的源文件,不进行编译 -s 编译指定的源文件,不进行汇编 -c 编译、汇编指定的源文件,但是不进行链接 -o 指定生成文件的文件名 -llibrary 用于手动指定链接环节中程序可以调用的库文件,如 -lstdc++ ,不建议 -l 和 library 之间有空格 -ansi 对于 c 语言程序来说,其等价于 -std=c90;对于 c++ 程序来说,其等价于 -std=c++98 -std= 手动指令编程语言所遵循的标准 \u0026gt; gcc 常用的编译选项\n假如我们编写了一个 source.c 的源程序,如下:\n#include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026#34;c program.\\n\u0026#34;); return 0; } 1. 预处理 - 生成预处理文件 *.i\n通过为 gcc 指令添加 -e 选项,即可控制 gcc 编译器仅对源代码做预处理操作。默认情况下, gcc -e 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件,因此,该指令往往会和 -o 选项连胜,将结果导入到指定文件中。\njack@jk:~/cemo/cporj$ gcc -e source.c -o source.i jack@jk:~/cemo/cporj$ ls source.c source.i jack@jk:~/cemo/cporj$ cat source.i # 1 \u0026#34;source.c\u0026#34; # 1 \u0026#34;\u0026lt;built-in\u0026gt;\u0026#34; # 1 \u0026#34;\u0026lt;command-line\u0026gt;\u0026#34; # 31 \u0026#34;\u0026lt;command-line\u0026gt;\u0026#34; # 1 \u0026#34;/usr/include/stdc-predef.h\u0026#34; 1 3 4 # .... linux 系统中通常用 \u0026quot;.i\u0026quot; 作为 c 语言程序预处理后所得文件的后缀名。显然, source.i 中的内容不是那么容易看懂的,好在可以为 gcc 指令再添加一个 -c 选项,来阻止 gcc 删除源文件和头文件中的注释,即 gcc -e -c source.c -o source.i 。\n2. 编译 - 生成汇编文件 *.s\njack@jk:~/cemo/cporj$ gcc -s source.i jack@jk:~/cemo/cporj$ ls source.c source.i source.s jack@jk:~/cemo/cporj$ cat source.s .file \u0026#34;source.c\u0026#34; .text .section .rodata .lc0: .string \u0026#34;c program.\u0026#34; .... 通过执行 gcc -s 指令,生成了个名为 source.s 的文件,这就是经过编译的汇编代码文件。(默认情况下,编译操作会自行新建一个文件名和指定文件相同、后缀名为 .s 的文件,并将编译的结果保存在该文件中。)\n同样,想要提高文件内汇编代码的可读性,可以借助 -fverbose-asm 选项,gcc 编译器会自动为汇编代码添加必要的注释,即 gcc -s source.i -fverbose-asm 。\n3. 汇编 - 生成目标文件 *.o\njack@jk:~/cemo/cporj$ gcc -c source.s jack@jk:~/cemo/cporj$ ls source.c source.i source.o source.s 上面生成的 source.o 文件就是目标文件,其本质为二进制文件(但尚未经过链接操作,所以无法直接运行)。\n4. 链接 - 生成可执行文件\ngcc 通过 -o 选项来指定输出文件,缺省默认输出 a.out ,其语法格式如下:\ngcc [-e|-s|-c] [infile] [-o outfile] _通过 -l 选项手动添加链接库\n链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。\n而且,链接器也必须将程序中所用到的所有 c 标准库函数加入其中。对于链接器来说,链接库不过是一个具在许多目标文件的集合,它们放在一个文件中以方便处理。\n标准库的大部分函数通常放在文件 libc.a 中(文件名后缀 .a 代表 achieve 读取),或者放在共享的动态链接文件 libc.so 中(文件名后缀 .so 代表 share object 共享对象)。\n如,通过 gcc source.c -o source.out -lm 链接数学库 libm.a ,前缀 lib 和后缀 .a 是标准的, m 是基本名称。(gcc 会在 -l 选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀)\n_gcc 使用静态链接库和动态链接库\n库文件的产生,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 c 语言中的 printf() 输出函数为例,程序中只需引入 \u0026lt;stdio.h\u0026gt; 头文件,即可调用 printf() 函数。\n调用库文件为什么还要牵扯到头文件呢?\n头文件和库文件并不是一码事,它们最大的区别在于:\n头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分; 所有的库文件都提供有相应的头文件作为调用它的接口,即库文件是无法直接使用的,只能通过头文件间接调用。 头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。\n= 其实,就是一种封装。\n事实上,库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。要知道,虽然库文件用于程序的链接阶段,但编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中采用静态链接方式实现链接操作的库文件,称为 静态链接库 ;采用动态链接方式实现链接操作的库文件,称为 动态链接库 。\n它们有什么不同呢?\n_静态链接库 实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,gcc 编译器就会 将该模板代码直接复制到程序文件的适当位置 ,最终生成可执行文件。\n好处是生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强),坏处如果程序文件中多次调用库中的同一个模块,则该模块代码会被复制多次(冗余),生成的可执行文件体积更大(与使用动态链接库生成的可执行文件相比)。\n在 linux 发行版中,静态链接库文件的后缀通常用 .a 表示;在 windows 系统中,静态链接库文件的后缀名为 .lib 。\n_动态链接库 ,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,gcc 编译器不会直接将该功能模块的代码拷贝到文件中,而是 将功能模块的位置信息记录到文件中,直接生成可执行文件。\n显然,这样生成的可执行文件是无法独立运行的。\n采用动态链接库生成的可执行文件运行时,gcc 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。\n在 linux 系统中,动态链接库的后缀名通常用 .so 表示;在 windows 系统中,动态链接库的后缀名为 .dll 。\n值得一提的是,gcc 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,gcc 编译器才会选择相应的静态链接库。如果两种都没有(或者 gcc 编译器未找到),则链接失败。\ngdb 调试器 gnu symbolic debugger,简称「gdb 调试器」,是 linux 平台下最常用的一款程序调试器。\n要知道,哪怕是开发经验再丰富的程序员,编写的程序也避免不了出错。程序中的错误主要分为 2 类,分别为:\n语法错误(可以借助编译器解决); 逻辑错误(只能程序员\u0026lt;自己或借助调试工具\u0026gt;调试解决)。 调试是每个程序员必须掌握的基本技能,没有选择的余地!\n所谓调试(debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。\ngdb 就是 linux 下使用最多的一款调试器,也有 windows 的移植版。\n总的来说,借助 gdb 调试器可以实现以下几个功能:\n程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量; 可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(如当前变量的值,函数执行的结果等),即支持断点调试; 程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。 默认情况下,程序不会进行调试模式,代码会瞬间从开关执行到末尾。要想观察程序运行的内部细节,可以借助 gdb 调试器在程序中的某个地方设置断点(breakpoint),如此当程序执行到这个地方时就会停下来。\ngdb 调试器支持在程序中打 3 种断点:\n普通断点(break):指定打断点的具体位置; 观察断点(watch):可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行; 捕捉断点(catch):监控程序中某一事件的发生。 ……\n= 具体调试细节,略过……\n数据 关于数据 数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。\n诸如数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别。我们需要用数据类型用来说明数据的类型,确定了 数据的解释方式 ,让计算机和程序员不会产生歧义。另外在 c 语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了 数据的长度 。\n= 反正就是一个二进制串,解释权在编译器,反正乱解释肯定出问题。\n数据是放在内存中的,在内存中存取数据要明确三件事情:数据存储在哪里、数据的长度以及数据的处理方式。\n_打印输出各种类型的数据\nputs (output string) 只能用来输出字符串; printf (print format)格式化输出,功能强大,不仅可以输出字符串,还可以输出整数、小数、单个字符等,并且输出格式可以自定义。\n数据类型 1. 整数类型 short、 int、 long\nshort(短整型)、int(整型)、long(长整型) 是 c 语言中常见的整数类型。c 语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:\n2 ≤ short ≤ int ≤ long 其中,int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。\n获取某个数据类型的长度可以使用 sizeof 操作符,如下:\n#include \u0026lt;stdio.h\u0026gt; int main() { short a = 10; int b = 100; int short_length = sizeof a; int int_length = sizeof(b); int long_length = sizeof(long); int char_length = sizeof(char); printf(\u0026#34;short=%d, int=%d, long=%d, char=%d\\n\u0026#34;, short_length, int_length, long_length, char_length); return 0; } // 在 64 位 linux 下的输出 → short=2, int=4, long=8, char=1 *注意, sizeof 是 c 语言中的操作符,不是函数(故可不带括号)。\n2. 浮点类型 float、 double\n一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。\n……\n3. 字符类型 char\n字符类型由单引号 ' ' 包围,字符串由双引号 \u0026quot; \u0026quot; 包围。\n计算机在存储字符时并不是真的要存储字符实体,而是存储该字符在字符集中的编号(也可以叫编码值)。对于 char 类型来说,它实际上存储的就是字符的 ascii 码。\n可以说,是 ascii 码表将英文字符和整数关联了起来。\n无论在哪个字符集中,字符编号都是一个整数;从这个角度考虑,字符类型和整数类型本质上没有什么区别。\n= 在 c 语言中,并没有单独定义字符串类型,字符串实际上是使用空字符 \\0 结尾的一维字符数组,如 char site[7] = {'r', 'u', 'n', 'o', 'o', 'b', '\\0'}; 。\n4. 构造类型 - 数组\n数组(array)就是一些列具有相同类型的数据的集合,这些数据在内存中依次挨着存放,彼此之间没有缝隙。\n数组的定义方式:\ndatatype arrayname[length];\r// - datatype 为数据类型\r// - arrayname 为数组名称\r// - length 为数组长度\r// 数组中每个元素都有一个索引(下标),从 0 开始,使用元素时指明下标即可:\rarrayname[index]\r// - index 为下标 数组的初始化:\n// 当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 0 int arr[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; // 给全部元素赋值,那么在定义数组时可以不给出数组长度 int arr[] = {1, 2, 3, 4, 5}; // 等价于 int arr[5] = {1, 2, 3, 4, 5}; 如何获取数组的长度呢? 通过 sizeof arr / sizeof arr[0] 。\n5. 字符数组\n在 c 语言中,没有专门的字符串变量,没有 string 类型,通常就用一个字符数组来存放一个字符串。\n在 c 语言中,字符串总是以 '\\0' 作为结尾,所以 '\\0' 也被称为字符串结束标志,或者字符串结束符。\n'\\0' 是 ascii 码表中的第 0 个字符,英文称为 nul,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在 c 语言中唯一的作用就是作为字符串结束标志。\n由 \u0026quot; \u0026quot; 包围的字符串会自动在末尾添加 '\\0' 。例如,\u0026ldquo;abc123\u0026rdquo; 从表面看起来只包含了 6 个字符,其实不然,c 语言会在最后隐式地添加一个 '\\0',这个过程是在后台默默地进行的,所以我们感受不到。\nchar str[6] = \u0026#34;abc123\u0026#34;; // ✘ char str[7] = \u0026#34;abc123\u0026#34;; // ✔,别忘记 \u0026#39;\\0\u0026#39;,使用 \u0026#34;xyzbnm..\u0026#34; 赋值会自动在末尾添加 \u0026#39;\\0\u0026#39; char str[7] = { \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;\\0\u0026#39; }; // 不嫌烦,你也可以这样 *另外,要注意字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。如:\nchar str[7]; str = \u0026#34;abc123\u0026#34;; // ✘ //正确 ✔ str[0] = \u0026#39;a\u0026#39;; str[1] = \u0026#39;b\u0026#39;; str[2] = \u0026#39;c\u0026#39;; str[3] = \u0026#39;1\u0026#39;; str[4] = \u0026#39;2\u0026#39;; str[5] = \u0026#39;3\u0026#39;; 所谓 _字符串长度 ,就是字符串包含了多少个字符( 不包含最后的结束字符 '\\0' ),如 \u0026quot;abc\u0026quot; 的长度是 3 ,而不是 4。(注意和定义时的数组长度做区分)\n在 c 语言中,我们使用 string.h 头文件中的 strlen() 函数来求字符串的长度,它的用法为:\nlength strlen(strname);\r// - length 字符串长度,一个整数\r// - strname 字符串的名字或字符数组的名字 6. 指针\n所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。\n7. 构造类型 - 结构体\nc 语言结构体(struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。\n结构体定义形式为:\nstruct tag { member-list } variable-list;\r// - tag 结构体名(标签)\r// - member-list 结构体成员(列表)\r// - variable-list 该结构体定义的类型变量 *注意,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。\n#include \u0026lt;stdio.h\u0026gt; int main() { struct student { char *name; // 姓名 int age; // 年龄 float score; // 成绩 } stu1, stu2; // 可以通过 =.= 获取和操作单个结构体成员 stu1.name = \u0026#34;tom\u0026#34;; str1.age = 18; stu1.score = 99.5; printf(\u0026#34;%s 的分数是: %d\\n\u0026#34;, stu1.name, stu1.score); return 0; } 8. 构造类型 - 共用体(联合体)\n联合体定义形式为:\nunion tag { member-list } variable-list;\r// - tag 联合体名(标签)\r// - member-list 联合体成员(列表)\r// - variable-list 该联合体定义的类型变量 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。\n输入输出 i/o 在控制台程序中,输出一般是指将数据(包括数字、字符等)显示在屏幕上,输入一般是指获取用户在键盘上输入的数据。\n输出 在 c 语言中,有三个函数可以用来在显示器上输出数据:\nputs() - 只能输出字符串,并且输出结束后自动换行; putchar() - 只能输出单个字符; printf() - 格式化输出各种类型的数据。 printf() 格式控制符的完整形式如下:\n%[flag][width][.precision]type\r// - type 表示输出类型\r// - width 表示最小输出宽度(不足时左边以空格补齐)\r// - .precision 表示输出精度,也就是小数的位数;\r// 也可以用于整数和字符串,但是功能相反:\r// - 用于整数时,表示最小输出宽度(不足时左边以 0 补齐)\r// - 用于字符串时,表示最大输出宽度\r// - flag 是标志字符:\r// - - 表示左对齐(默认右对齐)\r// - + 表示输出正负号(默认只有负数输出符号)\r// - 空格 输出正时以空格,输出负时以负号\r// - # 输出八进制、十六进制前缀;对于小数表示强迫输出小数点 关于 printf() 不能立即输出的问题\n从本质上讲, printf() 执行结束后数据并没有直接输出到显示器上,而是放入了缓冲区,直到遇见换行符 \\n 才将缓冲区中的数据输出到显示器上。\n输入输出的“命门”就在于缓存。\n输入 在 c 语言中,有多个函数可以从键盘获得用户输入:\ngets() - 获取一行数据,并作为字符串处理(可以读取含有空格的字符串); getchar() - 用于输入单个字符(就是 scanf(\u0026quot;%c\u0026quot;, c) 的简化版); scanf() - 可以格式化输入多种类型的数据。 对于 scanf() 输入数据的格式要和控制字符串的格式保持一致。\n从本质上讲,从键盘输入的数据并没直接交给 scanf() ,而是放了缓冲区中,直到我们按下回车键, scanf() 才到缓冲区中读取数据。\n文件操作 c 语言具有操作文件的能力,比如打开文件、读取/追加/插入/删除数据、关闭文件、删除文件等。\nc 语言中的文件是什么 文件是数据源的一种,最主要的作用是保存数据。\n在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。如:\n文件 硬件设备 stdin 标准输入文件,一般指键盘; scanf()、getchar() 等函数默认从 stdin 获取输入 stdout 标准输出文件,一般指显示器; printf()、putchar() 等函数默认从 stdout 输出数据 stderr 标准错误文件,一般指显示器; perror() 等函数默认向 stderr 输出数据 stdprn 标准打印文件,一般指打印机 \u0026gt; 常见硬件设备所对应的文件\n此处不去探讨硬件设备是如何被映射成文件的,只需记住,在 c 语言中硬件设备可以看成文件,有些输入输出函数不需要你指明到底读写哪个文件,系统已经为它们设置了默认的文件(当然你也可以更改,如让 printf 向磁盘上的文件输出数据)。\n操作文件的正确流程为:打开文件 → 读写文件 → 关闭文件(使用完毕要记得关闭哦)。\n关于文件流 所有的文件都要载入内存才能处理,所有的数据必须写入文件才不会丢失。\n数据在文件和内存之间传递的过程叫做文件流 ,数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。\n文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到 c 语言的变量(如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做 数据流(data stream) ,相应的,数据从数据源到程序(内存)的过程叫做输入流(input stream),从程序(内存)到数据源的过程叫做输出流(output stream)。\n输入输出(input outpt, io)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。\n我们可以说,打开文件就是打开了一个流。\n打开/关闭文件 在 c 语言中,操作文件之前必须先打开文件。\n所谓 _“打开文件” ,就是让程序和文件建立连接的过程,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 file 类型的结构体变量中; _“关闭文件” 就是断开与文件之间的联系,释放结构体变量,同时禁止再对文件进行操作。\n标准输入文件 stdin(表示键盘)、标准输出文件 stdout(表示显示器)、标准错误文件 stderr(表示显示器)是由系统打开的,可直接使用。\n使用 \u0026lt;stdio.h\u0026gt; 头文件中的 fopen() 函数即可打开文件,它的用法为:\nfile *fopen(char *filename, char *mode);\r// - filename 表示文件名称\r// - mode 表示打开方式 fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 file 类型的结构体变量中,然后将该变量的地址返回。\nfile 是 \u0026lt;stdio.h\u0026gt; 头文件中的一个结构体,它专门用来保存文件信息。如果希望接收 fopen() 的返回值,就需要定义一个 file 类型的指针。\n下面我们来看一段文件操作的规范写法:\nfile *fp; if ((fp = fopen(\u0026#34;d:\\\\demo.txt\u0026#34;, \u0026#34;rb\u0026#34;)) == null) { printf(\u0026#34;fail to open file!\\n\u0026#34;); exit(0); // 结束程序 } 我们在打开文件时 一定要 通过判断 fopen() 的返回值是否和 null 相等来判断是否打开失败。\n_关于文件打开方式 mode\n不同的操作需要不同的文件权限(只读、读写等),另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。\n在调用 fopen() 函数时,这些信息都必须提供,称为 文件打开方式 ,具体如下:\n打开方式 说明 控制读写权限的字符串 必须指明 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026ldquo;r\u0026rdquo; (read) 以“只读”方式打开文件。文件必须存在,否则打开失败。 \u0026ldquo;w\u0026rdquo; (write) 以“写入”方式打开文件。文件若不存在,新建;若存在,则清空文件内容。 \u0026ldquo;a\u0026rdquo; (append) 以“追加”方式打开文件。文件若不存在,新建;若存在,则(保留原有的文件内容)将写入的数据追加到文件的末尾。 \u0026ldquo;r+\u0026rdquo; 以“读写”方式打开文件。文件必须存在,否则打开失败。 \u0026ldquo;w+\u0026rdquo; 以“写入/更新”方式打开文件。相当于 w 和 r+ ,文件若不存在,新建;若存在,清空。 \u0026ldquo;a+\u0026rdquo; 以“追加/更新”方式打开文件。相当于 a 和 r+ ,文件若不存在,新建;若存在,追加。 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 控制读写方式的字符串 可选 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026ldquo;t\u0026rdquo; (text) 文本文件(默认) \u0026ldquo;b\u0026rdquo; (binary) 二进制文件 *注意,读写权限和读写方式可以组合使用,但是不能将读写方式放在读写权限的开头(可以放末尾和中间)。\n文件一旦使用完毕,应该使用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。文件正常关闭时, fclose() 的返回值 为 0 ,如果返回非零值则表示有错误发生。\nint fclose(file *fp); 读写文件 在 c 语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读写一个字符串,还可以读取若干个字节(数据块)。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。\n先来个完整的轮廓看看,如下:\n// 以字符形式读、写文件\rint fgetc(file *fp);\rint foutc(int ch, file *fp);\r// 以字符串形式读、写文件\rchar *fgets(char *str, int n, file *fp);\rint fputs (char *str, file *fp);\r// 以数据块形式读、写文件\rsize_t fread (void *ptr, size_t size, size_t count, file *fp);\rsize_t fwrite(void *ptr, size_t size, size_t count, file *fp);\r// 格式化读、写文件\rint fscanf (file *fp, char * format, ... );\rint fprintf(file *fp, char * format, ... );\r// 随机读、写文件\rvoid rewind(file *fp); // 用来将位置指针移动到文件开关\rint fseek(file *fp, long offset, int origin); // 用来将指针移动到任意位置 1. 以字符形式读写文件\n// int fgetc(file *fp); // - 成功时返回读取到的字符; // - 读取到文件末尾或读取失败时返回 eof (一个负数,通常为 -1) // 从 demo.txt 文件中读取一个字符,并保存到变量 ch 中 char ch; file *fp = fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;r+\u0026#34;); ch = fgetc(fp); 关于 eof (end of line),表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1 。 fgetc() 的返回值类型之所以为 int ,就是为了容纳这个负数(char 不能是负数)。\n在文件内部有一个位置指针,用来指向当前读写到的位置,也就是读写到第几个字节。在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc() 函数后,该指针会向后移动一个字节,所以可以连续多次使用 fgetc() 读取多个字符。\n*注意:这个文件内部的位置指针与 c 语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。\n下面我们来看一个示例,在屏幕上显示 demo.txt 文件的内容。\n#include \u0026lt;stdio.h\u0026gt; int main() { file *fp; char ch; // 如果文件不存在,给出提示并退出 if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;rt\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } // 每次读取一个字节,直到读取完毕 while ((ch=fgetc(fp)) != eof) { putchar(ch); } putchar(\u0026#39;\\n\u0026#39;); // 输出换行符 fclose(fp); // 关闭文件 return 0; } 写字符函数 fputc\n再看一个写入的示例,从键盘输入一行字符,写入文件。\n// int foutc(int ch, file *fp); // - 成功时返回写入的字符 // - 失败时返回 eof (一个负数) #include \u0026lt;stdio.h\u0026gt; int main(){ file *fp; char ch; // 判断文件是否打开成功 if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;wt+\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } printf(\u0026#34;input a string:\\n\u0026#34;); // 每次从键盘读取一个字符并写入文件 while ((ch=getchar()) != \u0026#39;\\n\u0026#39;) { fputc(ch, fp); } fclose(fp); return 0; } 2. 以字符串形式读写文件\nfgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中,用法如下:\nchar *fgets(char *str, int n, file *fp);\r// - str 为字符数组(长度为 n+1 ,不要忘了读取到的字符串会在末尾自动添加 \u0026#39;\\0\u0026#39;)\r// - n 为要读取的字符数目\r// - fp 为文件指针\r// 读取成功时返回字符数组的首地址,也即 str\r// 读取失败时,返回 null\r// 如果开始读取时,文件内部指针已经指向了文件末尾,将读取不到任何字符,也返回 null 来看一个示例:\n// 从 demo.txt 中读取 100 个字符,并保存到字符数组 str 中 #define n 101 char str[n]; file *fp = fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;r\u0026#34;); fgets(str, n, fp); *需要重点说明的是,在读取 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大, fgets() 最多只能读取一行数据,不能跨行。\n在 c 语言中,没有按行读取文件的函数,我们可以借助 fgets() ,将 n 的值设置地足够大,每次就可以读取到一行数据了。\n再来看一个示例,一行一行地读取文件:\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define n 100 int main() { file *fp; char str[n+1]; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;rt\u0026#34;)) == null) { puts(\u0026#34;fial to open file!\u0026#34;); exit(0); } while(fgets(str, n, fp) != null) { printf(\u0026#34;%s\u0026#34;, str); } fclose(fp); return 0; } 写字符串函数 fputs\nfputs() 函数用来向指定的文件写入一个字符串,它的用法为:\nchar *fputs(char *str, file *fp);\r// - str 为要写入的字符串)\r// - fp 为文件指针\r// 写入成功时返回非负数\r// 写入失败时,返回 eof 来看一个示例,向上例中建立的 demo.txt 文件中追加一个字符串:\n#include \u0026lt;stdio.h\u0026gt; int main() { file *fp; char str[102] = {0}, strtemp[100]; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;at+\u0026#34;)) == null) { puts(\u0026#34;fail to open file!\u0026#34;); exit(0); } printf(\u0026#34;input a string:\u0026#34;); gets(strtemp); strcat(str, \u0026#34;\\n\u0026#34;); strcat(str, strtemp); fputs(str, fp); fclose(fp); return 0; } 3. 以数据块形式读写文件\n// 以数据块形式读、写文件\rsize_t fread (void *ptr, size_t size, size_t count, file *fp);\rsize_t fwrite(void *ptr, size_t size, size_t count, file *fp);\r// - ptr 为内存区块的指针,它可以是数组、变量、结构体等\r// - fread() 中的 ptr 用来存放读取到的数据\r// - fwrite() 中的 ptr 用来存放要写入的数据\r// - size 表示每个数据块的字节数\r// - count 表示要读写的数据块的块数\r// - fp 表示文件指针\r// 理论上,每次读写 size*count 个字节的数据\r// 返回成功读写的块数,即 count\r// 如果返回值小于 count\r// - 对于 fwrite() 来说,不用发生了写入错误,可以用 ferror() 函数检测\r// - 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测 size_t 是什么呢?\nsize_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,即非负数,常用来表示数量。\n来看一个示例,从键盘输入一个数组,将数组写入文件,再读取出来:\n#include \u0026lt;stdio.h\u0026gt; #define n 5 int main() { // 从键盘输入的数据放入 a,从文件读取的数据放入 b int a[n], b[n]; int i, size = sizeof(int); file *fp; if((fp=open(\u0026#34;demo.txt\u0026#34;, \u0026#34;rb+\u0026#34;)) == null) { // 以二进制方式打开 puts(\u0026#34;fail to pen file!\u0026#34;); exit(0); } // 从键盘输入数据,并保存于数组 a for (i=0; i\u0026lt;n; i++) { scanf(\u0026#34;%d\u0026#34;, \u0026amp;a[i]); } // 将数组 a 的内容写入到文件 fwrite(a, size, n, fp); // 将文件中的位置指针重新定位到文件开头 rewind(fp); // 从文件读取内容并保存到数组 b fread(b, size, n, fp); // 在屏幕上显示数组 b 的内容 for (i=0; i\u0026lt;n; i++) { printf(\u0026#34;%d\u0026#34;, b[i]); } printf(\u0026#34;\\n\u0026#34;); fclose(fp); return 0; } 打开 demo.txt,发现文件内容根本无法阅读。这是因为我们使用 \u0026ldquo;rb+\u0026rdquo; 方式打开文件,数组会原封不动地以二进制形式写入文件,一般无法阅读。\n再来看一个示例,从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示到屏幕上:\n#include \u0026lt;stdio.h\u0026gt; #define n 2 struct stu { char name[10]; int num; int age; float score; } boya[n], boyb[n], *pa, *pb; int main() { file *fp; int i; pa = boya; pb = boyb; if ((fp=fopen(\u0026#34;demo.txt\u0026#34;, \u0026#34;wb+\u0026#34;)) == null) { puts(\u0026#34;fail to pen file!\u0026#34;); exit(0); } // 从键盘输入数据 printf(\u0026#34;input data:\\n\u0026#34;); for (i=0; i\u0026lt;n; i++, pa++) { scanf(\u0026#34;%s %d %d %f\u0026#34;, pa-\u0026gt;name, \u0026amp;pa-\u0026gt;num, \u0026amp;pa-\u0026gt;age, \u0026amp;pa-\u0026gt;score); } // 将数组 boya 的数据写入文件 fwrite(boya, sizeof(struct stu), n, fp); // 将文件中的位置指针重置到文件开头 rewind(fp); // 从文件读取数据并保存到数据 boyb fread(boyb, sizeof(struct stu), n, fp); // 输出数组 boyb 中的数据 for (i=0; i\u0026lt;n; i++, pb++) { printf(\u0026#34;%s %d %d %f\\n\u0026#34;, pb-\u0026gt;name, pb-\u0026gt;num, pb-\u0026gt;age, pb-\u0026gt;score); } fclose(fp); return 0; } 4. 格式化读写文件\nfscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。\n// 格式化读、写文件\rint fscanf (file *fp, char * format, ... );\rint fprintf(file *fp, char * format, ... );\r// - fp 为文件指针\r// - format 为格式控制字符串\r// - ... 表示参数列表\r// 成功 返回写入的字符的个数\r// 失败 返回负数 来看一个简单的示例:\nfile *fp; int i, j; char *str, ch; fscanf(fp, \u0026#34;%d %s\u0026#34;, \u0026amp;i, str); fprintf(fp, \u0026#34;%d %c\u0026#34;, j, ch); 5. 随机读写文件\n移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek() 。\n// 随机读、写文件\rvoid rewind(file *fp); // 用来将位置指针移动到文件开关\rint fseek (file *fp, long offset, int origin); // 用来将指针移动到任意位置\r// - fp 为文件指针,也就是被移动的文件\r// - offset 为偏移量,也就是要移动的字节数,正向后移,负向前移\r// - origin 为起始位置,c 语言规定起始位置有三种:\r// - 文件开头, 常量名 seek_set, 值为 0\r// - 当前位置, 常量名 seek_cur, 值为 1\r// - 文件末尾, 常量名 seek_end, 值为 2 如 fseek(fp, 100, 0); 表示把位置指针移动到离文件开头 100 个字节处。\n","date":"2022-03-01","permalink":"https://loveminimal.github.io/posts/c/","summary":"\u003cblockquote\u003e\n\u003cp\u003e= 好的吧,我承认,对于 C 语言,我有一种莫名其妙的偏爱!是因为可以直接操作内存吗?或许是!可能吧,计算机有什么神奇的东西呢?最终就是围绕那几个概念在来回打转!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"c"},{"content":" = 这篇 promise 仅是摘录使用,内容很散碎……\npromise 是一个对象(可以用来绑定回调函数),它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 promise 实例对象,所以我们首先说明怎样使用 promise,再说明如何创建 promise 。\n使用 promise1 本质上 promise 是一个函数返回的对象 ,我们可以在它上面绑定回调函数,如此就不需要在一开始就把回调函数作为参数传入给这个函数了。\n= 看了后面的 🌰 ,就容易理解了。一定程度上避免了回调地狱 👻\n下面来看一个示例,假设现在有一个名为 createaudiofileasync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。\n// 成功时的回调函数 function successcallback(result) { console.log(\u0026#39;音频文件创建成功:\u0026#39; + result); } // 失败的回调函数 function failurecallback(error) { console.log(\u0026#39;音频文件创建失败:\u0026#39; + error); } createaudiofileasync(audiosettings, successcallback, failurecallback); 更现代的函数会返回一个 promise 对象,使得你可以将你的回调函数绑定在该 promise 上。下面我们重写函数 createaudiofileasync() 使其返回 promise,如下:\nconst promise = createaudiofileasync(audiosettings); promise.then(successcallback, failurecallback); // or 简写为 createaudiofileasync(audiosettings).then(successcallback, failurecallback); 我们把这个称为 异步函数调用 ,这种形式有若干优点,下面我们将会逐一讨论。\n不同于“老式”的传入回调,在使用 promise 时,会有 以下约定 :\n在本轮 事件循环 运行完成之前,回调函数是不会被调用的; 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用; 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。 promise 很棒的一点就是 _链式调用(chaining) 。\n链式调用 连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 _promise 链 来实现这种需求。\n!!! then() 函数会返回一个和原来不同的新的 promise 。\nconst promise = dosomething(); const promise2 = promise.then(successcallback, failurecallback); // or const promise2 = dosomething().then(successcallback, failurecallback); 其中, promise2 不仅表示 dosomething() 函数的完成,也代表了你传入的 successcallback 或者 failurecallback 的完成,这两个函数也可以返回一个 promise 对象,从而形成另一个异步操作,如此,在 promise2 上新增的回调函数会排上这个 promise 对象的后面。\n基本上,第一个 promise 都代表了链中另一个异步过程的完成。\n来看一下,过去要想做多重的异步操作,会导致经典的回调地狱,如下:\ndosomething(function(result) { dosomethingelse(result, function(newresult) { dothirdthing(newresult, function(finalresult) { console.log(\u0026#39;got the final result: \u0026#39; finalresult); }, failurecallback); }, failurecallback); }, failurecallback); 而现在,我们可以把回调绑定到返回的 promise 上,形成一个 promise 链,如下:\ndosomething().then(function(result) { return dosomethingelse(result); }) .then(function(newresult) { return dothirdthing(newresult); }) .then(function(finalresult) { console.log(\u0026#39;got the final result: \u0026#39; + finalresult); }) .catch(failurecallback); // 也可以用箭头函数来表示 dosomething() .then(result =\u0026gt; dosomethingelse(result)) .then(newresult =\u0026gt; dothirdthing(newresult)) .then(finalresult =\u0026gt; { console.log(`got the final result: ${finalresult}`); }) .catch(failurecallback); !!! 注意:一定要有返回值 ,否则,callback 将无法获取上一个 promise 的结果。\nthen 里的参数是可选的, catch(failurecallback) 是 then(null, failurecallback) 的缩略形式。\n有可能会在一个回调失败之后继续使用链式操作,如下:\nnew promise((resolve, reject) =\u0026gt; { console.log(\u0026#39;初始化\u0026#39;); resolve(); }) .then(() =\u0026gt; { throw new error(\u0026#39;有哪里不对了\u0026#39;); console.log(\u0026#39;执行「这个」”\u0026#39;); }) .catch(() =\u0026gt; { console.log(\u0026#39;执行「那个」\u0026#39;); }) .then(() =\u0026gt; { console.log(\u0026#39;执行「这个」,无论前面发生了什么\u0026#39;); }); // → // 初始化 // 执行“那个” // 执行“这个”,无论前面发生了什么 错误传递 在之前 的回调地狱示例中,我们有 3 次 failurecallback 的调用,而在 promise 链中只有尾部的一次调用。\n通常,一遇到异常抛出,浏览器会顺着 promise 链寻找下一个 onrejected 失败回调函数或者由 .catch() 指定的回调函数。它和同步代码 try...catch... 的工作原理(执行过程)非常相似。\n在 es2017 标准的 async/await 语法糖中,这种异步代码的对称性得到了极致的体现,如下:\nasync function foo() { try { const result = await dosomething(); const newresult = await dosomethingelse(result); const finalresult = await dothirdthing(newresult); console.log(`got the final result: ${finalresult}`); } catch (error) { failurecallback(error); } } 通过捕获所有的错误,甚至抛出异常和程序错误,promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。\n当 promise 被拒绝时,会有下文所述的两个事件( rejectionhandled 、 unhandledrejection )之一被派发到全局作用域(通常而言,就是 window ;如果是在 web worker 中使用,就是 worker 或者其他 worker-based 接口)。\n此外暂时不深入,有兴趣的时候再了解。\r在旧式回调 api 中创建 promise 可以通过 promise 的构造器从零开始创建 promise 。这种方式(通过构造器的方式)应当只在封装旧 api 的时候用到。\n理想状态下,所有的异步函数都已经返回 promise 了,但有一些 api 仍然使用旧方式传入的成功(或失败)的回调。典型的例子就是 settimeout() 函数。\nsettimeout(() =\u0026gt; saysomething(\u0026#39;10 seconds passed\u0026#39;), 10000); 混用旧式回调和 promise 可能会造成运行时序的问题 ,如果 saysomething 函数失败了,或者包含了编程错误,就没有办法捕获它了。\n幸运地是,我们可以用 promise 来封闭它。最好的做法是,将这些有问题的函数封闭起来,留在底层,并且永远不要再直接调用它们。\nconst wait = (ms) =\u0026gt; new promise((resolve) =\u0026gt; settimeout(resolve, ms)); wait(10000) .then(() =\u0026gt; saysomething(\u0026#39;10 seconds passed\u0026#39;)) .catch(failurecallback); 通常,promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 promise 。既然 settimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject 。\npromise.resolve() 和 promise.reject() 是手动创建一个已经 resolve 或者 reject 的 promise 快捷方法,它们有时很有用。\n_关于 promise.resolve()\npromise.resolve(value) 方法返回一个以给定值解析后的 promise 对象。如果这个值是一个 promise,那么将返回这个 promise ;如果这个值是 thenable (即带有 then 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终形态;否则返回的 promise 将以此值完成。此函数将类 promise 对象的多层嵌套展平。\n注意,不要在解析为自身的 thenable 上调用 promise.resolve ,这将导致无限递归,因为它试图展平无限嵌套的 promise 。如下:\nlet thenable = { then: (resolve, reject) =\u0026gt; { resolve(thenable); }, }; promise.resolve(thenable); // 这会造成一个死循环 我们看一些使用静态 promise.resolve 方法的示例:\n// 1. resolve 一个字符串 promise.resolve(\u0026#34;success\u0026#34;).then(function(value) { console.log(value); // → \u0026#34;success\u0026#34; }, function(value) { // 不会被调用 }); // 2. resolve 一个数组 var p = promise.resolve([1, 2, 3]); p.then(function(v) { console.log(v[0]); // → 1 }) // 3. resolve 另一个 promise var original = promise.resolve(33); var cast = promise.resolve(original); cast.then(function(value) { console.log(\u0026#39;value: \u0026#39; + value); }); console.log(\u0026#39;original === cast ? \u0026#39; + (original === cast)); // → value: 33 // → original === cast ? true // 4. resolve thenable 并抛出错误 var p1 = promise.resolve({ then: function(onfulfill, onreject) { onfulfill(\u0026#34;fulfilled!\u0026#34;); } }); console.log(p1 instanceof promise); // → true,这是一个 promise 对象 p1.then(function(v) { console.log(v); // → \u0026#34;fulfilled!\u0026#34; }, function(e) { // 不会被调用 }); // thenable 在 callback 之前抛出异常 // promise rejects var thenable = { then: function(resolve) { throw new typeerror(\u0026#34;throwing\u0026#34;); resolve(\u0026#34;resolving\u0026#34;); }}; var p2 = promise.resolve(thenable); p2.then(function(v) { // 不会被调用 }, function(e) { console.log(e); // typeerror: throwing }); // thenable 在 callback 之后抛出异常 // promise resolves var thenable = { then: function(resolve) { resolve(\u0026#34;resolving\u0026#34;); throw new typeerror(\u0026#34;throwing\u0026#34;); }}; var p3 = promise.resolve(thenable); p3.then(function(v) { console.log(v); // 输出\u0026#34;resolving\u0026#34; }, function(e) { // 不会被调用 }); 时序 为了避免意外,即使是一个已经变成 resolve 状态的 promise,传递给 then() 函数也总是会被异步调用:\npromise.resolve().then(() =\u0026gt; console.log(2)); console.log(1); // → 1 // → 2 传递到 then() 中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 javascript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:\nconst wait = (ms) =\u0026gt; new promise((resolve) =\u0026gt; settimeout(resolve, ms)); wait().then(() =\u0026gt; console.log(4)); promise.resolve() .then(() =\u0026gt; console.log(2)) .then(() =\u0026gt; console.log(3)); console.log(1); // → 1, 2, 3, 4 仔细观察你会发现,settimeout 和 then 的执行时机是有区别的,什么区别呢?不妨去探索一下。\r除了上述提到的这些,promise 还有 promise.all() 、promise.race() 等方法,用时再查即可。我们已经对 promise 的使用有了初步的了解,那么 promise 到底是什么呢?\npromise 是什么 promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。\n一个 promise 对象代表一个在这个 promise 被创建出来时不一定已知的值 。异步方法并不会立即返回最终的值,而是会返回一个 promise ,以便在未来某个时候把值交给使用者。\n可以把 promise 理解为一个承载异步请求响应状态的容器。\r异步操作都有那些状态呢?\npending 待定 - 初始状态,既没有兑现,也没有拒绝; fulfilled 已兑现 - 意味着操作成功完成; rejected 已拒绝 - 意味着操作失败。 这里需要注意的地方在于, new promise(executorfunc) 中的参数函数 excutorfunc 只会在 promise 创建的时候执行一次,并固定执行后的结果不再改变。\n其实,这里很容易理解,一个操作要么是在运行中,还没出结果;要么就是成功了,或者失败了。\r我们可以用 promise.then()、 promise.catch() 和 promise.finally() 这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。这些方法还会返回一个新生成的 promise 对象,这个对象可以被非强制性的用来做链式调用。\n不要和惰性求值混淆: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如 scheme。javascript 中的 promise 代表的是已经正在发生的进程, 而且可以通过回调函数实现链式调用。 如果您想对一个表达式进行惰性求值,就考虑一下使用无参数的\u0026quot;箭头函数\u0026quot;: f = () =\u0026gt;表达式 来创建惰性求值的表达式,使用 f() 求值。\n构造函数 promise()\n创建一个新的 promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。\npromise 原型\npromise 对象是由关键字 new 及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数 resolve 和 reject 作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用 reject 函数。\nconst myfirstpromise = new promise((resolve, reject) =\u0026gt; { // ?做一些异步操作,最终会调用两者之一: // resolve(somevalue); // fulfilled // ?or // reject(\u0026#34;failure reason\u0026#34;); // rejected }); 想要某个函数拥有 promise 功能,只需让其返回一个 promise 即可。\n// 示例 1 function myasyncfunction(url) { return new promise((resolve, reject) =\u0026gt; { const xhr = new xmlhttprequest(); xhr.open(\u0026#39;get\u0026#39;, url); xhr.onload = () =\u0026gt; resolve(xhr.responsetext); xhr.onerror = () =\u0026gt; reject(xhr.statustext); xhr.send(); }); } // 示例 2 let myfirstpromise = new promise(function (resolve, reject) { // 当异步代码执行成功时,我们才会调用 resolve(...), 当异步代码失败时就会调用 reject(...) // 在本例中,我们使用 settimeout(...) 来模拟异步代码,实际编码时可能是 xhr 请求或是 html5 的一些 api 方法。 settimeout(function () { resolve(\u0026#39;成功!\u0026#39;); // 代码正常执行! }, 250); }); myfirstpromise.then(function (successmessage) { // successmessage 的值是上面调用 resolve(...) 方法传入的值。 // successmessage 参数不一定非要是字符串类型,这里只是举个例子 console.log(\u0026#39;yay! \u0026#39; + successmessage); }); 众所周知,javascript 也是一门面向对象的编程语言,只不过它是基于原型的。promise() 本身是一个构造函数(可以作个不恰当的类比 - promise 是一个类),其上包含一些静态方法(即类本身的静态方法,与实例无关),如: promise.all(iterable)、 promise.allsettled(iterable)、 promise.any(iterable)、 promise.race(iterable)、 promise.reject(reason) 和 promise.resolve(value) 等。\n另外,如 then()、 catch() 和 finally() 等方法则是定义在 promise.prototype 原型上的。\ntodo 延伸阅读 并发模型与事件循环 继承与原型链 ","date":"2022-02-08","permalink":"https://loveminimal.github.io/posts/promise/","summary":"\u003cblockquote\u003e\n\u003cp\u003e= 这篇 Promise 仅是摘录使用,内容很散碎……\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise\"\u003ePromise\u003c/a\u003e 是一个对象(可以用来绑定回调函数),它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以我们首先说明怎样使用 Promise,再说明如何创建 Promise 。\u003c/p\u003e","title":"promise"},{"content":"mvvm ? mvc ? mvp ?\nmvvm1 mvvm 也被称为 model-view-binder ,其中 vm 是 viewmodel。view 和 viewmodel 之间采用数据绑定(双向的),避免了开发人员写一些同步 viewmodel 和 view 的重复逻辑。通过数据绑定,view 发生变化会自动反映到 viewmodel,viewmodel 产生的变化也会自动更新 view。\nviewmodel 创建了一个视图的抽象,将视图中的状态和行为抽离出来。\n在 mvvm 的实现中,还引入了隐式的一个 binder 层,声明式的数据和命令的绑定在 mvvm 模式中就是通过它完成的。\n前端比较流行的 vuejs 就是一个提供了 mvvm 风格的双向数据绑定的框架,它的核心是 mvvm 中的 vm(viewmodel),vm 负责连接 view 和 model,保证视图和数据的一致性。\n在 vue 中,model 和 vm,vm 和 view 之间都是双向数据绑定,实现方式是 _数据劫持 (实现“数据支持”的基础是 es5.1 中标准化的 object.defineproperty() 方法)。\nobject.defineproperty()2 object.defineproperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。\n先来看一个简单的示例:\nconst obj = {}; object.defineproperty(obj, \u0026#39;prop\u0026#39;, { value: 42, writable: false, }); obj.prop = 76; // 在 strict mode 下会抛出错误 console.log(obj.prop); // → 42 _语法格式 :\nobject.defineproperty(obj, prop, descriptor)\r// obj - 要定义属性的对象\r// prop - 要定义或修改的属性的名称或 symbol\r// descriptor - 要定义或修改的属性描述符\r// 返回被传递给函数的对象 我们主要来看一下 descriptor 是什么?有什么?怎么用?\nconfigurable enumerable value writable get set 数据描述符 ✔ ✔ ✔ ✔ ✘ ✘ 存取描述符 ✔ ✔ ✘ ✘ ✔ ✔ 对象里目前存在的属性描述符有两种形式:数据描述符和存取描述符。\n其中, configurable、enumerable 是共享的,但 _数据描述符 是一个具有值的属性( value ),其值可以是可写的/不可写的( writable ); _存取描述符 则是由 getter 函数和 setter 函数所描述的属性。\n拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false ; 属性值和函数的键 value、get 和 set 字段的默认值为 undefined 。 如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。\n其实,很简单,就是 `value、writable` 和 `get、set` 不能共存。\rvue 中的“数据劫持”就是通过这里的存取描述符 get/set 的 getter/setter 函数实现的。\n下面我们来看一个简单的自定义 setters 和 getters :\n// 下例展示了如何实现一个自存档对象 // 当设置 temperature 属性时,archive 数组会收到日志条目 function archiver() { var temperature = null; var archive = []; object.defineproperty(this, \u0026#39;temperature\u0026#39;, { get: function () { console.log(\u0026#39;get!\u0026#39;); return temperature; }, set: function (value) { temperature = value; archive.push({ val: temperature }); }, }); this.getarchive = function () { return archive; }; } var arc = new archiver(); arc.temperature; // → \u0026#39;get!\u0026#39; arc.temperature = 11; arc.temperature = 13; arc.getarchive(); // → [{ val: 11 }, { val: 13 }] 在对象的属性被访问或者修改时,它的 get/set 方法就会被调用。\nget - 属性的 getter 函数,如果没有 getter,则为 undefined 。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。\nset - 属性的 setter 函数,如果没有 setter,则为 undefined 。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。\n深入 vue 响应式原理3 vue 最独特的特性之一,是其非侵入性(即对原来的代码 vue 的加入是无感知的)的响应式系统。数据模型仅仅是普通的 javascript 对象,当你修改它们时,视图就会更新。\nvue 是如何追踪变化的呢?\n当你把一个普通的 javascript 对象传入 vue 实例作为 data 选项,vue 将遍历此对象所有的 property,并使用 object.defineproperty 把这些 property 全部转为 getter/setter 。\n这些 getter/setter 对用户来说是不可见的,但是在内部它们让 vue 能够追踪依赖,在 property 被访问和修改时通知变更。\n每个组件都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。\n由于 vue 不允许动态添加响应式 property,所以你必须在初始化实例前声明所有要组响应式 property,哪怕只是一个空值。\nvue 在更新 dom 时是异步执行的,只要侦听到数据变化,vue 将开启一个队列,并缓冲在同一事件中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种中缓冲时去除重复数据对于避免不必要的计算和 dom 操作是非常重要的。然后,在下一个的事件循环 tick 中,vue 刷新队列并执行实际(已去重的)工作。\nvue 在内部对异步队列尝试使用原生的 promise.then、mutationobserver 和 setimmediate ,如果执行环境不支持,则会采用 settimeout(fn, 0) 代替。\n","date":"2022-02-07","permalink":"https://loveminimal.github.io/posts/mvx/","summary":"\u003cp\u003eMVVM ? MVC ? MVP ?\u003c/p\u003e","title":"mvx"},{"content":"开个坑,大点的……\n🔔 摘录自 廖雪峰老师的 java 教程\njavaee javaee 并不是一个软件产品,它更多的是一种软件架构和设计思想,是在 javase 的基础上,开发的一系列基于服务器的组件、api 标准和通用架构。\njavaee 最核心的组件就是基于 servlet 标准的 web 服务器 ,开发者编写的应用程序是基于 servlet api 并运行在 web 服务器内部的。\n目前流行的基于 spring 的轻量级 javaee 开发架构,使用最广泛的是 servlet 和 jms(java message service),以及一系列开源组件。\nhttp server web 应用是 bs(browser/server)架构的,浏览器和服务器之间的传输协议是 http。http 协议是一个基于 tcp 协议之上的“请求-响应”协议。\n一个 http server 本质上是一个 tcp 服务器。 具体实现可以参考 编写一个简单的 http 服务器 。\nservlet 在 javaee 平台上,处理 tcp 连接,解析 http 协议这些底层工作统统扔给现成的 web 服务器去做,我们只需要把自己的应用程序跑在 web 服务器上。为了实现这一目的,javaee 提供了 servlet api, 我们只需要使用 servlet api 编写自己的 servlet 来处理 http 请求 ,web 服务器实现 servlet api 接口,实现底层功能。\n我们来实现一个最简单的 servlet,如下:\n// webservlet 注解表示这是一个 servlet,并映射到地址 / @webservlet(urlpatterns = \u0026#34;/\u0026#34;) public class helloservlet extends httpservlet { @override protected void doget(httpservletrequest req, httpservletresponse resp) throws servletexception, ioexception { // 设置响应类型 resp.setcontenttype(\u0026#34;text/html\u0026#34;); // 获取输出流 printwriter ps = resp.getwriter(); // 写入响应 pw.writer(\u0026#34;\u0026lt;h1\u0026gt;hello, servlet!\u0026lt;/h1\u0026gt;\u0026#34;); // 最后不要忘记 flush 强制输出 pw.flush(); } } 一个 servlet 总是继承自 httpservlet ,然后覆写 doget() 或 dopost() 方法。\n思考一下:servlet api 是谁提供?\n普通的 java 程序是通过启动 jvm,然后执行 main() 方法开始运行。但是 web 应用程序有所不同,我们无法直接运行 war 文件,必须先启动 web 服务器,再由 web 服务器加载我们编写的 helloservlet ,这样就可以让 helloservlet 处理浏览器发送的请求。\n如此,一个完整的 web 应用程序的开发流程如下:\n1. 编写 servlet;\r2. 打包为 war 文件;\r3. 复制到 tomcat 的 webapps 目录下;\r4. 启动 tomcat。 这很繁琐!!!\ntomcat 实际上也是一个 java 程序,我们看看 tomcat 的启动流程:\n1. 启动 jvm 并执行 tomcat 的 main() 方法;\r2. 加载 war 并初始化 servlet;\r3. 正常服务。 启动 tomcat 无非就是设置好 classpath 并执行 tomcat 某个 jar 包的 main() 方法, 我们完全可以把 tomcat 的 jar 包全部引入进来,然后自己编写一个 main() 方法 ,先启动 tomcat,然后让它加载我们的 webapp 就行。如下:\npublic class main { public static void main(string[] args) throws exception { // 启动 tomcat tomcat tomcat = new tomcat(); tomcat.setport(integer.getinteger(\u0026#34;port\u0026#34;, 8080)); tomcat.getconnector(); // 创建 webapp context ctx = tomcat.addwebapp(\u0026#34;\u0026#34;, new file(\u0026#34;src/main/webapp\u0026#34;).getabsolutepath()); webresourceroot resources = new standardroot(ctx); resources.addpreresources(new dirresourceset( resources, \u0026#34;/web-inf/classes\u0026#34;, new file(\u0026#34;target/classes\u0026#34;).getabsolutepath(), \u0026#34;/\u0026#34;) ); ctx.setresource(resources); tomcat.start(); tomcat.getserver().await(); } } 如此,我们直接运行 main() 方法,即可启动嵌入式 tomcat 服务器,然后,通过预设的 tomcat.addwebapp(\u0026quot;\u0026quot;, new file(\u0026quot;src/main/webapp\u0026quot;),tomcat 会自动加载当前工程作为根 webapp,可直接在浏览器访问 http://localhost:8080/ 。\n思考一下:如何引入 tomcat 的 jar 包呢?\n一个 web app 就是由一个或多个 servlet 组成的,每个 servlet 通过注解说明自己能处理的路径。 浏览器发出的 http 请求总是由 web server 先接收,然后,根据 servlet 配置的映射,不同的路径转发到不同的 servlet 。\n_重定向 是指当浏览器请求一个 url 时,服务器返回一个重定向指令(如 resp.sendredirect(your_redirect_to_url); ),告诉浏览器地址已经变了,麻烦使用新的 url 再重新发送新请求(浏览器地址会显示新的 your_redirect_to_url )。\nforward 是指内部 _转发 (地址栏不会变)。当一个 servlet 处理请求的时候,它可以决定自己不继续处理,而是转发给另一个 servlet 处理(如 req.getrequestdispatcher(your_forward_to_url).forward(req, resp); )。\n在 web 应用程序中,如何跟踪用户身份?\nhttp 协议是一个无状态协议,即 web 应用程序无法区分收到的两个 http 请求是否是同一个浏览器发出的。为了跟踪用户状态,服务器可以向浏览器分配一个唯一 id,并以 cookie 的形式发送到浏览器,浏览器在后续访问时总是附带此 cookie,这样,服务器就可以识别用户身份。\n我们把这种基于唯一 id 识别用户身份的机制称为 _session,javaee 的 servlet 机制内建了对 session 的支持。如:\nhttpsession session = req.getsession(); // 存储信息到 httpsession session.setattribute(\u0026#34;user\u0026#34;, name); // 读取信息从 httpsession string user = (string) req.getsession().getattribute(\u0026#34;user\u0026#34;); 服务器识别 session 的关键就是依靠一个名为 jsessionid 的 cookie。在 servlet 中第一次调用 req.getsession() 时,servlet 容器自动创建一个 session id,然后通过一个名为 jsessionid 的 cookie 发送给浏览器。\n综上可知,servlet 提供的 httpsession 本质上就是通过一个名为 jsessionid 的 cookie 来跟踪用户会话的。下面我们来看看 cookie 的设置与读取:\n// 创建一个新的 cookie cookie cookie = new cookie(\u0026#34;lang\u0026#34;, lang); cookie.setpath(\u0026#34;/\u0026#34;); // 设置 cookie 生产的路径范围 cookie.setmaxage(8640000); // 设置 cookie 有效期 // 将该 cookie 添加到响应 resp.addcookie(cookie); // .... // 读取请求附带的所有 cookie cookie[] cookies = req.getcookies(); if (cookies != null) { for (cookie cookie : cookies) { if (cookie.getname().equals(\u0026#34;lang\u0026#34;)) { // 返回 cookie 的值 return cookie.getvalue(); } } } jsp jsp 是 java server pages 的缩写,它的文件必须放到 /src/main/webapp 下,文件名必须以 .jsp 结尾,整个文件与 html 并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令 \u0026lt;% ... %\u0026gt; 。\n整个 jsp 的内容实际上是一个 html,但是稍有不同:\n包含在 \u0026lt;%-- 和 --%\u0026gt; 之间的是 jsp 的注释,它们会被完全忽略; 包含在 \u0026lt;% 和 %\u0026gt; 之间的是 java 代码,可以编写任意 java 代码; 如果使用 \u0026lt;%= xxx %\u0026gt; 则可以快捷输出一个变量的值。 jsp 页面内置了几个变量(可以直接使用):\nout:表示 httpservletresponse 的 printwriter; session:表示当前 httpsession 对象; request:表示 httpservletrequest 对象。 jsp 和 servlet 有什么区别?其实它们没有任何区别,因为 jsp 在执行前首先被编译成一个 servlet。\n访问 jsp 页面时,直接指定完整路径(而无需手动配置映射路径)。例如, http://localhost:8080/hello.jsp 。\n= jsp 早已是明日黄花了……\nmvc 通过前面的章节可以看到:\nservlet 适合编写 java 代码,实现各种复杂的业务逻辑,但不适合输出复杂的 html; jsp 适合编写 html,并在其中插入动态内容,但不适合编写复杂的 java 代码。 能否将两者结合起来,发挥各自的优点,避免各自的缺点? \u0026ndash; mvc !!!\n使用 mvc 模式的好处是,controller 专注于业务处理,它的处理结果就是 model。 model 可以是一个 javabean,也可以是一个包含多个对象的 map ,controller 只负责把 model 传递给 view,view 只负责把 model 给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发 controller 时无需关注页面,开发 view 时无需关心如何创建 model。\n= controller 和 view 的纽带就是 model 喽(处理完之后转发即可)。🤨 emm\u0026hellip; 这就是前后端分离的起点?\n通过结合 servlet 和 jsp 的 mvc 模式,我们可以发挥二者各自的优点:\nservlet 实现业务逻辑; jsp 实现展示逻辑。 但是,直接把 mvc 搭在 servlet 和 jsp 之上还是不太好,原因如下:\nservlet 提供的接口仍然偏底层,需要实现 servlet 调用相关接口; jsp 对页面开发不友好,更好的替代品是模板引擎; 业务逻辑最好由纯粹的 java 类实现,而不是强迫继承自 servlet。 思考一下:能不能通过普通的 java 类实现 mvc 的 controller? \u0026ndash; 当然能 !\n如何设计一个 mvc 框架呢?\n上图中, dispatcherservlet 以及 render(modelandview) (如何渲染)均由 mvc 框架实现,有了 mvc 框架,我们只需要编写 controller 就可以了。\n具体如何呢?且往下看。\n在 mvc 框架中创建一个 接收所有请求 的 servlet \u0026ndash; dispatcherservlet ,它总是映射到 / ,然后根据不同的 controller 的方法定义的 @getmapping 或 @postmapping 的 path 决定调用哪个方法,最后获得方法返回的 modelandview 后,渲染模板,写入 httpservletresponse ,即完成了整个 mvc 的处理。\n我们的 dispatcherservlet 持有哪些信息呢?如下:\n@webservlet(urlpatterns = \u0026#34;/\u0026#34;) public class dispatcherservlet extends httpservlet { // 持有存储请求路径到某个具体方法的映射 private map\u0026lt;string, getdispatcher\u0026gt; getmappings = new hashmap(); private map\u0026lt;string, postdispatcher\u0026gt; postmappings = new hashmap(); // 持有模板引擎 private viewengine viewengine; // ... @override public void init() throws servletexception { // dispatcherservlet 初始化时,扫描所有的 controller 中 // 带有 @getmappings 和 @postmappings 标记的方法 this.getmappings = scangetincontrollers(); this.postmappings = scanpostincontrollers(); this.viewengine = new viewengine(getservletcontext()); } // ... } 思考一下:如何扫描并获取? \u0026ndash; 反射!\n是的,关于 dispatcherservlet ,其持有信息就是上面这些。\n下面,我们再深入一下,看看其中 getdispatcher 、 postdispatcher 、 viewengine 都是什么结构。\n处理一个 get 请求是通过 getdispatcher 对象完成的,它需要如下信息:\nclass getdispatcher { object instance; // controller 实例 method method; // controller 方法 string[] parameternames; // 方法参数名称 class\u0026lt;?\u0026gt;[] parameterclasses; // 方法参数类型 } 类似的, postdispatcher 需要如下信息:\nclass postdispatcher { object instance; // controller 实例 method method; // controller 方法 class\u0026lt;?\u0026gt;[] parameterclasses; // 方法参数映射 objectmapper objectmapper; // json 映射 } viewengine 其实非常简单,只需要实现一个简单的 render() 方法,如下:\npublic class viewengine { public void render(modelandview mv, writer writer) throws ioexception { string view = mv.view; map\u0026lt;string, object\u0026gt; model = mv.model; // 根据 view 找到模板文件 template template = gettemplatebypath(view); // 渲染并写入 wrinter template.write(writer, model); } } 其中 modelandview 定义也比较简单,如下:\npublic class modelandview { map\u0026lt;string, object\u0026gt; model; // 一个 javabean 或一个包含多个对象的 map string view; // 模板的路径 } 看,只要有了 mvc 框架,我们只需要编写包含返回 modelandview 对象方法的 controller 类就可以了。\n详细代码参考 - 实现一个 mvc 框架 。\n这个 mvc 框架主要参考就是 spring mvc,通过实现一个“简化版”mvc,可以掌握 java web mvc 开发的核心思想与原理。\n当然,在实际工作中,已经有像 spring mvc 这种比较成熟的框架了,但是了解原理是重要的,比只会简单的运用要重要的多。\rspring mvc 首先,来看一个标准的 maven web 工程目录结构示例,如下:\n其中, src/main/webapp 是标准 web 目录, web-inf 存放 web.xml ,编译的 class,第三方 jar,以及不允许浏览器直接访问的 view 模版, static 目录存放所有静态文件。\n在 src/main/resources 目录中存放的是 java 程序读取的 classpath 资源文件,除了 jdbc 的配置文件 jdbc.properties 外,我们又新增了一个 logback.xml ,这是 logback 的默认查找的配置文件。\n在 src/main/java 中就是我们编写的 java 代码了。\n在前面已经讲过了 java web 的基础:servlet 容器,以及标准的 servlet 组件:\nservlet:能处理 http 请求并将 http 响应返回; jsp:一种嵌套 java 代码的 html,将被编译为 servlet; filter:能过滤指定的 url 以实现拦截功能; listener:监听指定的事件,如 servletcontext、httpsession 的创建和销毁。 servlet 容器为每个 web 应用程序自动创建一个唯一的 servletcontext 实例,这个实例就代表了 web 应用程序本身。\n我们知道 spring 提供的是一个 ioc 容器,所有的 bean ,包括 controller,都在 spring ioc 容器中初始化,而 servlet 容器由 javaee 服务器(如 tomcat)提供,它对 spring 一无所知。\nweb 应用程序总是由 servlet 容器创建,那么,spring 容器应该由谁创建?在什么时候创建?spring 容器中的 controller 又是如何通过 servlet 调用的?\n在 web 应用中启动 spring 容器有很多种方法,可以通过 lister 启动,可以通过 servlet 启动,可以使用 xml 配置,也可以使用注解配置。这里,我们介绍一种最简单的启动 spring 容器的方式。\n第一步,在 web.xml 中配置 spring mvc 提供的 dispatcherservlet :\n\u0026lt;!doctype web-app public \u0026#34;-//sun microsystems, inc.//dtd web application 2.3//en\u0026#34; \u0026#34;http://java.sun.com/dtd/web-app_2_3.dtd\u0026#34; \u0026gt; \u0026lt;web-app\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.dispatcherservlet\u0026lt;/servlet-class\u0026gt; \u0026lt;!-- 1.初始化参数 contextclass 指定使用注解配置的 annotationconfigwebapplicationcontext --\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextclass\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;org.springframework.web.context.support.annotationconfigwebapplicationcontext\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;!-- 2.配置文件的位置参数 contextconfiglocation 指向 appconfig 的完整类名 --\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextconfiglocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;com.itranswarp.learnjava.appconfig\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;load-on-startup\u0026gt;0\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;!-- 3.把这个 servlet 映射到 /*,即处理所有 url --\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;dispatcher\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; 上述配置可以看作一个样板配置,有了这个配置,servlet 容器会首先初始化 spring mvc 的 dispatcherservlet ,在 dispatcherservlet 启动时,它根据配置 appconfig 创建一个类型是 webapplicationcontext 的 ioc 容器,完成所有 bean 的初始化,并将容器绑到 servletcontext 上。\n因为 dispatcherservlet 持有 ioc 容器,能从 ioc 容器中获取所有 @controller 的 bean,因此, dispatcherservlet 接收到所有 http 请求后,根据 controller 方法配置的路径,就可以正确把请求转发到指定方法,并根据返回的 modelandview 决定如何渲染页面。\n我们已经知道了如何结合 servlet 容器和 spring 容器,那么,如何配置一个可用的 spring mvc 呢?\n和普通 spring 配置一样,我们编写正常的 appconfig 后,只需要加上 @enablewebmvc 注解,就“激活”了 spring mvc,如下:\n@configuration @componentscan @enablewebmvc // 启用 spring mvc @enabletransactionmanagement @propertysource(\u0026#34;classpath:/jdbc.properties\u0026#34;) public class appconfig { // ... } 当然,还需要创建 datasource 、 jdbctemplate 、 platformtransactionmanager 等 bean 。\n其中,有一个必须要创建的 bean 是 viewresolver ,因为 spring mvc 允许集成任何模板引擎,使用哪个引擎,就实例化一个对应的 viewresolver :\n@bean viewresolver createviewresolver(@autowired servletcontext servletcontext) { pebbleengine engine = new pebbleengine.builder().autoescaping(true) .cacheactive(flase) .loader(new servletloader(servletcontext)) .extension(new springextension()) .build(); pebbleviewresolver viewresolver = new pebbleviewresolver(); // viewresolver 通过指定 prefix 和 suffix 来确定如何查找 view viewresolver.setprefix(\u0026#34;/web-inf/templates/\u0026#34;); viewresolver.setsuffix(\u0026#34;\u0026#34;); viewresolver.setpebbleengine(engine); return viewresolver; } 一切完备之后,就可以编写我们自己的 controller 了,如下:\n@controller // 注意是 @controller ,不是 @component @requestmapping(\u0026#34;/user\u0026#34;) // 对 url 分组(推荐) public class usercontroller { @getmapping(\u0026#34;/profile\u0026#34;) // 实际 url 映射是 /user/profile public modelandview profile() { // ... } } 实际方法的 url 映射总是前缀+路径,这种形式还可以有效避免不小心导致的重复的 url 映射。\n注意,返回的 modelandview 通常包含一个 view 的路径和一个 map 作为 model,但也可以没有 model ,如 return new modelandview(\u0026quot;signin.html\u0026quot;) 。返回重定向时既可以写 new modelandview(\u0026quot;redirect:/signin\u0026quot;) ,也可以直接返回 string ,如 return \u0026quot;redirect:/signin\u0026quot; 。\n如果在 controller 方法内部直接操作 httpservletresponse 发送响应,返回 null 表示无需进一步处理。如下:\npublic modelandview download(httpservletresponse response) { byte[] data = ...; response.setcontenttype(\u0026#34;application/octet-stream\u0026#34;); outputstream output = response.getoutputstream(); output.write(data); output.flush(); return null; } 详细源码参考 使用 spring mvc 。\nrest 使用 spring mvc 开发 web 应用程序的主要工作就是编写 controller 逻辑。\n在 web 应用中,除了需要使用 mvc 给用户显示页面外,还有一类 api 接口,我们称之为 rest,通常输入输出都是 json,便于第三方调用或者使用页面 javascript 与之交互。\n直接在 controller 中处理 json 是可以的,因为 spring mvc 的 @getmapping 和 @postmapping 都支持指定输入和输出的格式。然而 直接用 spring 的 controller 配合一大堆注解写 rest 太麻烦了 ,如下:\n@postmapping(value = \u0026#34;/rest\u0026#34;, // 路径 consumes = \u0026#34;application/json;charset=utf-8\u0026#34;, // 声明能接收的类型 produces = \u0026#34;application/json;charset=utf-8\u0026#34;) // 声明输出的类型 @responsebody // !!! public string rest(@requestbody user user) { // !!! return \u0026#34;{\\\u0026#34;restsupport\\\u0026#34;:true}\u0026#34;; // 返回 json } 其中, @responsebody 表示返回的 string 无需额外处理,直接作为输出内容写入 httpservletresponse ,输入的 json 则根据注解 @requestbody 直接被 spring 反序列化为 user 这个 javabean 。\n_太麻烦了,怎么办呢?\nspring 还额外提供了一个 @restcontroller 注解,使用 @restcontroller 替代 @controller 后,每个方法自动变成 api 接口方法。如下:\n@restcontroller @requestmapping(\u0026#34;/api\u0026#34;) public class apicontroller { @autowired userservice userservice; @getmapping(\u0026#34;/users\u0026#34;) public list\u0026lt;user\u0026gt; users() { return userservice.getusers(); } @getmapping(\u0026#34;/users/{id}\u0026#34;) public user user(@pathvariable(\u0026#34;id\u0026#34;) long id) { return userservice.getuserbyid(id); } @postmapping(\u0026#34;/signin\u0026#34;) public map\u0026lt;string, object\u0026gt; signin(@requestbody signinrequest signinrequest) { try { user user = userservice.signin(signinrequest.email, signinrequest.password); return map.of(\u0026#34;user\u0026#34;, user); } catch (exception e) { return map.of(\u0026#34;error\u0026#34;, \u0026#34;signin_failed\u0026#34;, \u0026#34;message\u0026#34;, e.getmessage()); } } public static class signinrequest { public string email; public string password; } } 编写 rest 接口只需要定义 @restcontroller ,然后,每个方法都是一个 api 接口,输入和输出只要能被 jackson 序列化或反序列化为 json 就没有问题。\n另外,还有 @jsonignore 和 @jsonproperty(access = access.write_only) 等有意思的注解,它们有什么作用呢?\npublic class user { // 避免 user 序列化为 json 时,暴露 password 属性, 但是 // 这会导致想写入 password 时也找不到了 @jsonignore public string getpassword() { return password; } // 这种就比较好了,只需写入,禁用读取(如, 注册账号时) @jsonproperty(access = access.write_only) public string getpassword() { return password; } } 详细代码参考 使用 rest 实现 api 。\n虽然,后续 spring-boot 为我们的开发带来了革命性的变化,但 spring、mvc 这些基本的思想,才是根本,前者只是把一些东西自动化了,计算机帮你做了你可以不做的。\r","date":"2022-01-26","permalink":"https://loveminimal.github.io/posts/web-dev/","summary":"\u003cp\u003e开个坑,大点的……\u003c/p\u003e\n\u003cp\u003e🔔 摘录自 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1252599548343744/1255945497738400\"\u003e廖雪峰老师的 Java 教程\u003c/a\u003e\u003c/p\u003e","title":"web 开发"},{"content":"🔔 摘录自 http://c.biancheng.net/spring/\n虽然我们在生产环境中基本上使用 spring boot 了,但是深入了解 spring 是非常必要的,因为它是“地基”。\nspring 是什么 spring 由 rod johnson 创立,2004 年发布了 spring 框架的第一版,其目的是用于简化企业级应用程序开发的难度和周期。\nspring 自诞生以来一直备受青睐,它包括许多框架,例如 spring framework、springmvc、springboot、spring cloud、spring data、spring security 等,所以有人将它们亲切的称之为:spring 全家桶。\nspring framework 就是我们平时说的 spring 框架,spring 框架是全家桶内其它框架的基础和核心。\nspring 是分层的 java se/ee 一站式轻量级开源框架, 以 ioc(inverse of control,控制反转)和 aop(aspect oriented programming,面向切面编程)为内核。\nioc 指的是将对象的创建权交给 spring 去创建。使用 spring 之前,对象的创建都是由我们使用 new 创建,而使用 spring 之后,对象的创建都交给了 spring 框架。aop 用来封装多个类的公共行为,将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,减少系统的重复代码,降低模块间的耦合度。另外,aop 还解决一些系统层面上的问题,比如日志、事务、权限等。\n共享、解耦、去冗。\r在 spring 中,认为一切 java 类都是资源,而资源都是类的实例对象(bean),容纳并管理这些 bean 的是 spring 所提供的 ioc 容器,所以 spring 是一种基于 bean 的编程 ,它深刻地改变着 java 开发世界,迅速地取代 ejb 成为了实际的开发标准。\n在实际开发中,服务器端通常采用三层体系架构,分别为表现层(web)、业务逻辑层(service)、持久层(dao)。\nspring 致力于 java ee 应用各层的解决方案,对每一层都提供了技术支持。在表现层提供了与 spring mvc、struts2 框架的整合,在业务逻辑层可以管理事务和记录日志等,在持久层可以整合 mybatis、hibernate 和 jdbctemplate 等技术。这就充分体现出 spring 是一个全面的解决方案,对于已经有较好解决方案的领域,spring 绝不做重复的事情。\nspring 体系结构 spring 框架采用分层的理念,根据功能的不同划分成了多个模块,这些模块大体可分为:\ndata access/integration(数据访问与集成) \u0026ndash;\u0026gt; jdbc、orm、oxm、jms、transactions web \u0026ndash;\u0026gt; websocket、servlet、web、portlet aop、aspects、instrumentation(检测)、messaging(消息处理) core container(核心容器) \u0026ndash;\u0026gt; beans、core、context、spel test 如下图所示,spring framework 4.x 版本后的系统框架图:\n上图中包含了 spring 框架的所有模块,这些模块可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。\n1. data access/integration(数据访问/集成)\n数据访问/集成层包括 jdbc、orm、oxm、jms 和 transactions 模块,具体介绍如下:\njdbc 模块:提供了一个 jdbc 的样例模板,使用这些模板能消除传统冗长的 jdbc 编码,还有必须的事务控制,而且能享受到 spring 管理事务的好处; orm 模块:提供与流行的“对象-关系”映射框架无缝集成的 api,包括 jpa、jdo、hibernate 和 mybatis 等,而且还可以使用 spring 事务管理,无需额外控制事务; oxm 模块:提供了一个支持 object/xml 映射的抽象层实现,如 jaxb、castor、xmlbeans、jibx 和 xstream 。将 java 对象映射成 xml 数据,或者将 xml 数据映射成 java 对象; jms 模块:指 java 消息服务,提供一套“消息生产者、消息消费者”模板用于更加简单的使用 jms,jms 用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信; transactions 事务模块:支持编程和声明式事务管理。 2. web 模块\nspring 的 web 层包括 web、servlet、websocket 和 portlet 组件,具体介绍如下:\nweb 模块:提供了基本的 web 开发集成特性,例如多文件上传功能、使用的 servlet 监听器的 ioc 容器初始化以及 web 应用上下文; servlet 模块:提供了一个 spring mvc web 框架实现。spring mvc 框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的 jsp 标签,完全无缝与 spring 其他技术协作; websocket 模块:提供了简单的接口,用户只要实现响应的接口就可以快速的搭建 websocket server ,从而实现双向通讯; portlet 模块:提供了在 portlet 环境中使用 mvc 实现,类似于 web-servlet 模块的功能。 3. core container(spring 的核心容器)\nspring 的核心容器是其他模块建立的基础,由 beans 模块、core 核心模块、context 上下文模块和 spel 表达式语言模块组成,没有这些核心容器,也不可能有 aop、web 等上层的功能。具体介绍如下:\nbeans 模块:提供了框架的基础部分,包括控制反转和依赖注入; core 核心模块:封装了 spring 框架的底层部分,包括资源访问、类型转换及一些常用的工具类; context 上下文模块:建立在 core 和 beans 模块的基础之上,集成 beans 模块功能并添加资源绑定、数据验证、国际化、java ee 支持、容器生命周期、事件传播等。applicationcontext 接口是上下文模块的焦点; spel 模块:提供了强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从 spring 容器获取 bean,它也支持列表投影、选择和一般的列表聚合等。 4. aop、aspects、instrumentation 和 messaing\n在 core container 之上是 aop、aspects 等模块,具体介绍如下:\naop 模块:提供了面向切面编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态地把这些功能添加到需要的代码中,如此,降低业务逻辑和通用功能的耦合; aspects 模块:提供与 aspectj 的集成,是一个功能强大且成熟的面向切面编程(aop)框架; instrumentation 模块:提供了类工具的支持和类的加载器的实现,可以在特定的应用服务器中使用; messaging 模块:spring 4.0 以后新增了消息(spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。 5. test 模块\ntest 模块:spring 支持 junit 和 testng 测试框架,而且还额外提供了一些基于 spring 的测试功能,比如在测试 web 框架时,模拟 http 请求的功能。\nhmm... 先有个感性了解即可,有利于后续引入相应的包及源码阅读。\r","date":"2022-01-19","permalink":"https://loveminimal.github.io/posts/spring/","summary":"\u003cp\u003e🔔 摘录自 \u003ca href=\"http://c.biancheng.net/spring/\"\u003ehttp://c.biancheng.net/spring/\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e虽然我们在生产环境中基本上使用 \u003ca href=\"../spring-boot/\"\u003eSpring Boot\u003c/a\u003e 了,但是深入了解 Spring 是非常必要的,因为它是“地基”。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"spring"},{"content":"🔔 摘录自 廖雪峰老师的 sql 教程\nsql,结构化查询语言(structured query language ),是一种数据库查询和程序设计语言,用于访问和操作数据库系统。sql 语句既可以查询数据库中的数据,也可以插入、更新和删除数据库中的数据,还可以对数据库进行管理和维护操作。\n= 数据库能听懂的语言 \u0026ndash; sql*\nsql 关系数据库概述 为什么需要数据库? 为了优化数据的读写及管理。 应用程序不需要自己管理数据,而是通过数据库软件提供的接口来读写数据。至于数据本身如何存储到文件,那就是数据库软件的事情了。\n数据库按照数据结构来组织、存储和管理数据,有三种模型:层次模型、网关模型和关系模型。\n关系数据库的关系模型是基于数学理论建立的(有兴趣的可以深入下)。其支持的标准数据类型包括数值、字符串、时间等,如下表:\n名称 类型 说明 int 整型 4 字节,约 +/-21 亿 bigint 长整型 8 字节,约 +/-922 亿 real/float(24) 浮点型 4 字节 double 浮点型 8 字节 decimal(m,n) 高精度小数 decimal(20,10) 表示一共 20 位,其中小数 10 位,通常用于财务计算 char(n) 定长字符串 char(100) 总是存储 100 个字符的字符串 varchar(n) 变长字符串 varchar(100) 可以存储 0~100 个字符的字符串 boolean 布尔类型 存储 true 或者 false date 日期类型 2018-06-22 time 时间类型 12:20:59 datetime 日期和时间类型 2018-06-22 12:20:59 通常来说, bigint 能满足整数存储的需求, varchar(n) 能满足字符串存储的需求。其他,还有一些不常用的数据类型,如 tinyint (范围在 0~255)。各数据库厂商还会支持特定的数据类型,如 json 。\nsql 语言关键字不区分大小写!\n一般约定 sql 关键字总是大写,以示突出,表名和列名均使用小写。\r安装 mysql1 选择国内镜像下载(如 华为的 mysql 镜像),官网太慢了……\n如果,你要也要下载 mysql-8.0.26 ,可以直接 点此下载 。\n安装完成之后,将执行路径,如 c:\\program files\\mysql\\mysql server 8.0\\bin 加入到电脑环境变量中(具体方法自行百度),以管理员权限打开 cmd 或 powershell:\n输入 mysqld --initialize --console ,进行初始化,该步可以得到 mysql 的初始密码; 输入 mysqld --install ,如果需要安装多个 mysql 服务,这步输入 mysql --install [服务名]; 输入 net start mysql ,启动 mysql 服务; 输入 mysql -u root -p ,使用初始密码登录 mysql ; 登录后,把初始密码改掉,输入 alter user 'root'@'localhost' identified with mysql_native_password by '你自己的密码'; (注意带分号); 输入 show databases; ,可以查看包含哪些数据库了。 win10 具体安装 mysql 8.0.26 如下:\n# 1. 初始化 ps c:\\windows\\system32\u0026gt; mysqld --initialize --console 2022-01-17t12:32:47.721393z 0 [system] [my-013169] [server] c:\\program files\\mysql\\mysql server 8.0\\bin\\mysqld.exe (mysqld 8.0.26) initializing of server in progress as process 16088 2022-01-17t12:32:47.739476z 1 [system] [my-013576] [innodb] innodb initialization has started. 2022-01-17t12:32:48.147715z 1 [system] [my-013577] [innodb] innodb initialization has ended. 2022-01-17t12:32:49.184664z 0 [warning] [my-013746] [server] a deprecated tls version tlsv1 is enabled for channel mysql_main 2022-01-17t12:32:49.187008z 0 [warning] [my-013746] [server] a deprecated tls version tlsv1.1 is enabled for channel mysql_main 2022-01-17t12:32:49.384847z 6 [note] [my-010454] [server] a temporary password is generated for root@localhost: gmlai+axi2rn # 2. 安装服务 ps c:\\windows\\system32\u0026gt; mysqld --install service successfully installed. # 3. 启动服务 ps c:\\windows\\system32\u0026gt; net start mysql mysql 服务正在启动 . mysql 服务已经启动成功。 # 4. 使用初始化密码登录 ps c:\\windows\\system32\u0026gt; mysql -u root -p enter password: ************ welcome to the mysql monitor. commands end with ; or \\g. your mysql connection id is 8 server version: 8.0.26 copyright (c) 2000, 2021, oracle and/or its affiliates. oracle is a registered trademark of oracle corporation and/or its affiliates. other names may be trademarks of their respective owners. type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; show databases; error 1820 (hy000): you must reset your password using alter user statement before executing this statement. # 5. 为 root 设置自己的密码,方便下次登录 mysql\u0026gt; alter user \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; identified with mysql_native_password by \u0026#39;your_password\u0026#39;; query ok, 0 rows affected (0.00 sec) mysql\u0026gt; show databases; +--------------------+ | database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.01 sec) # 使用 exit 退出 mysql\u0026gt; exit bye # 下次使用新设置的密码 \u0026#39;your_password\u0026#39; 登录即可 ps c:\\windows\\system32\u0026gt; mysql -u root -p enter password: ************* welcome to the mysql monitor. commands end with ; or \\g. your mysql connection id is 10 server version: 8.0.26 mysql community server - gpl copyright (c) 2000, 2021, oracle and/or its affiliates. oracle is a registered trademark of oracle corporation and/or its affiliates. other names may be trademarks of their respective owners. type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; 其他版本的安装过程,大同小异,出现的问题可能是形形色色,直接百度就 ok 了。\nubuntu20 安装相关3\nsudo apt update \u0026amp;\u0026amp; sudo apt install mysql-server # 安装 sudo service mysql status # 查看服务状态 # or systemctl status mysql sudo mysql # ubuntu 默认 root@localhost 是无密码的 mysql\u0026gt; exit; 如何为 =root@localhost= 设置密码呢?4\nmysql\u0026gt; alert user \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; identified with mysql_native_password by \u0026#39;你自己的mysql密码\u0026#39;; mysql\u0026gt; flush privileges; 如何当前 mysql 服务可能远程登录呢?5\nmysql\u0026gt; create user \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; identified by \u0026#39;你自己的mysql密码\u0026#39;; mysql\u0026gt; grant all privileges on *.* to \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39;; mysql\u0026gt; flush privileges; 然后:\ncd /etc/mysql/mysql.conf.d ; 修改相应的配置文件 mysqld.cnf ; 将 bind-address 127.0.0.1修改为bind-address 0.0.0.0 ; service mysql restart 重启 mysql 服务; netstat -lntp 查看状态。 关系模型 关系数据库是建立在关系模型上,关系模型本质上就是若干存储数据的二维表。表的每一行称为记录(record),每一列称为字段(column)。\n字段定义了数据类型,以及是否允许为 null ( 表示字段不存在)。\n通常情况下,字段应该避免允许为null。不允许为null可以简化查询条件,加快查询速度,也利于应用程序读取数据后无需判断是否为null。\r关系数据库的表和表之间需要建立“一对多”,“多对一”和“一对一”的关系,关系是通过主键和外键来维护的。\n1. 主键\n主键是关系表中记录的唯一标识,其选取非常重要,注意不要带有业务含义,而应该使用 bigint 自增或者 guid 类型,主键也不应该允许 null 。\n2. 外键\n在一张表中,通过某个字段,可以把数据与另一张表关联起来,这种列称为外键。关系数据库通过外键可以实现一对多、多对多(需要通过中间表)和一对一的关系。外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证。\n下面来看一个外键约束:\n-- 定义一个外键约束 alter table students add constraint fk_class_id -- 外键约束名称 fk_class_id,任意 foreign key (class_id) -- 指定了 class_id 作为外键 references classes (id); -- 指定了这个关键将关联到 classes 表的 id 列 -- 删除一个外键约束 alert table students drop foreign key fk_class_id; 3. 索引\n在查找记录的时候,想要获得非常快的速度,就需要使用索引。索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。无论是否创建索引,对于用户和应用程序来说,使用关系数据库不会有任何区别。\n查询数据 在关系数据库中,最常用的操作就是查询。\n1. 基本查询\n使用 select 查询的基本语句 select * from \u0026lt;表名\u0026gt; 可以查询一个表的所有行和所有列的数据,查询结果是一个二维表。\n-- 语法: -- select * from \u0026lt;表名\u0026gt; -- 如 select * from students; -- 所有的学生记录 2. 条件查询\nselect 语句可以通过 where 条件来设定查询条件,查询结果是满足查询条件的记录,而不是整个表的所有记录。\n-- 语法: -- select * from \u0026lt;表名\u0026gt; where \u0026lt;条件表达式\u0026gt; -- 如 select * from students where score \u0026gt;= 80; -- 分数大于 80 的学生记录 条件表达式可以包含逻辑运算,优先级由高到低为 not \u0026gt; and \u0026gt; or ,当然还有括号。\n此处可以用谐音记忆, not(not) at(and) all(or) - 一点也不,别客气。\r3. 投影查询\n如果我们只希望返回某些列的数据,而不是所有列的数据,可以使用投影查询。\n-- 语法: -- select 列1 [别名1], 列2, 列3 from \u0026lt;表名\u0026gt; [...] -- 如 select id, score, name from students; -- 没设置别名 select id, score points, name from students; -- 设置别名 select id, score points, name from students where gender = \u0026#39;m\u0026#39;; -- 可接 where条件 4. 排序\n使用 order by 可以对结果集进行排序,可以对多列进行升序、倒序排序。\nselect id, name, gender, score from students order by score; -- 默认 asc 升序,可以省略 select id, name, gender, score from students order by score desc; -- 降序 -- 多列,先按 score ,有相同分数的再按 gender select id, name, gender, score from students order by score desc, gender; -- 如果有 where 子句,那么 order by 子句要放到 where 子句后面(因为是对结果集排序嘛) select id, name, gender, score from students where class_id = 1 order by score desc; 5. 分页查询\n分页实际上就是从结果集中“截取”出第 m~n 条记录。\n-- 语法: -- limit \u0026lt;n-m\u0026gt; offset \u0026lt;m\u0026gt; -- 如 select * from students limit 3 offset 0; -- 查询第 1 页 select * from students limit 3 offset 3; -- 查询第 2 页 select * from students limit 3 offset 6; -- 查询第 3 页 -- ... 可见,分页查询的关键在于,首先要确定每页需要显示的结果数量 pagesize (这里为 3),然后根据当前页的索引 pageindex (从 1 开始,页码啦),确定 limit 和 offset 应该设定的值:\nlimit 总是设定为 pagesize ; offset 计算公式为 pagesize * (pageindex - 1) 。 另外:\noffset 是可选的,如果只写 limit 15 ,那么相当于 limit 15 offset 0 ; 在 mysql 中, limit 15 offset 30 还可以简写成 limit 30, 15 ; 使用 limit \u0026lt;m\u0026gt; offset \u0026lt;n\u0026gt; 分页时,随着 n 越来越大,查询效率也会越来越低。 6. 聚合查询\n对于统计总数、平均数这类计算,sql 提供了专门的聚合函数,使用聚合函数进行查询,就是聚合查询。\n函数 说明 count 查询所有列的行数 sum/avg 计算某一数值类型列的合计值/平均值 max/min 计算某一列的最大值/最小值 select count(*) from students; select avg(score) average from students where gender = \u0026#39;m\u0026#39;; 注意:\n如果 where 条件没有匹配到任何行, count() 会返回 0 ,而其他会返回 null ; 聚合的计算结果虽然是一个数字,但查询的结果仍然是一个二维表,只是这个二维表只有一行一列; 通常,使用聚合查询时,我们应该给列名设置一个别名,便于处理结果。 对于聚合查询,sql 还提供了“分组聚合”的功能。\nselect class_id, count(*) num from students group by class_id; -- count() 的结果不再是一个,因为 group by 子句指定了按 class_id 分组 执行上述 select 语句时,会把 class_id 相同的列先分组,再分别计算,因些会得到不止一行结果(当然,我们此处假设 class_id 有几个不同的值)。\n7. 多表查询\n使用多表查询可以获取 m x n 行记录,查询的结果也是一个二维表。\n-- 语法: -- select * from \u0026lt;表1\u0026gt; [表1别名], \u0026lt;表2\u0026gt; [表2别名]; -- 如 select * from students, classes; -- 可以利用投影查询设置别名 select students.id sid, students.name, students.gender, students.score, classes.id cid, classes.name cname from students, classes; -- sql还允许给表设置一个别名,以在投影查询中引用起来稍微简洁一点 select s.id sid, s.name, s.gender, s.score, c.id cid, c.name cname from students s, classes c; 8. 连接查询\n连接查询是另一种类型的多表查询,对多个表进行 join 运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择地“连接”在主表结果集上。\n-- 语法: -- select ... from \u0026lt;表1\u0026gt; inner join \u0026lt;表2\u0026gt; on \u0026lt;条件...\u0026gt;; -- 如 select s.id, s.name, s.class_id, c.name class_name, -- s.gender, s.score from students s -- 先确定主表,仍然使用 from \u0026lt;表1\u0026gt; inner join classes c -- 再确定需要连接的表,使用 inner join \u0026lt;表2\u0026gt; on s.class_id = c.id; -- 然后确定连接条件,使用 on \u0026lt;条件……\u0026gt; -- 可选,加上 where 子句、 order by 等子句 inner join 是最常用的一种 join 查询,除此之外还有 left/right/full outer join 。\n修改数据 关系数据库的基本操作就是 crud(create 增、retrieve 查、update 改、delete 删)。对于增、删、改,对应的 sql 语句分别是:\ninsert 插入新记录; update 更新已有记录; delete 删除已有记录。 1. insert\n-- 语法: -- insert into \u0026lt;表名\u0026gt; (字段1, 字段2, ...) values (值1, 值2, ...) -- 如 insert into students (class_id, name, gender, score) values (1, \u0026#39;大宝\u0026#39;, \u0026#39;m\u0026#39;, 87), (2, \u0026#39;二宝\u0026#39;, \u0026#39;m\u0026#39;, 81); -- 可以一次性添加多条记录 注意,字段顺序不必和数据库表的字段顺序一致,但值的顺序必须和字段顺序一致。\n2. update\n-- 语法: -- update \u0026lt;表名\u0026gt; set 字段1=值1, 字段2=值2, ... where ...; -- 如 update students set name=\u0026#39;大牛\u0026#39;, score=score+10 -- 可以一次性更新多条记录,且可以使用表达式 where id=1; 3. delete\n使用 delete ,我们就可以一次删除表中的一条或多条记录。\n-- 语法: -- delete from \u0026lt;表名\u0026gt; where ...; -- 如 delete from students where id=1; mysql 安装完 mysql 后,除了 mysql server,即真正的 mysql 服务器外,还附赠一个 mysql client 程序。mysql client 是一个命令行客户端,可以通过 mysql client 登录 mysql,然后,输入 sql 语句并执行。\nmysql client 的可执行程序是 mysql ,mysql server 的可执行程序是 mysqld (后台运行)。\n在 mysql client 中输入的 sql 语句通过 tcp 连接发送到 mysql server。默认端口号是 3306,即如果发送到本机 mysql server,地址就是 127.0.0.1:3306 。\n也可以只安装 mysql client,然后连接到远程 mysql server。\n# mysql -h \u0026lt;mysql-server-ip\u0026gt; -u root -p mysql -h 10.0.1.99 -u root -p 除了命令行客户端外,也有 gui 客户端可供使用,如 mysql workbench、 navicat 等。\n1. 数据库\n在一个运行的 mysql 服务器上,实际上可以创建多个数据库(database)。相关操作如下:\n操作 说明 show databases; 列出所有数据库 create database test; 创建一个新数据库 drop database test; 删除一个数据库 use test; 切换到数据库 test \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; 表 show tables; 列出当前数据库的所有表 desc students; 查看一个表的结构 show create table students; 查看创建表的 sql 语句 drop table students; 删除表 修改表就比较复杂。\n-- 给 students 表新增一列 birth alert table students add column birth varchar(10) not null; -- 修改 birth 列名为 birthday,类型改为 varchar(20) alert table students change column birth birthday varchar(20) not null; -- 删除列 alert table students drop column birthday; todo 2. 实用的 sql 语句\n……\n事务 这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些 sql 一样,不会对数据库数据有任何改动。\n对于单条 sql 语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。\n要手动把多条 sql 语句作为一个事务执行,使用 begin 开启一个事务,使用 commit 提交一个事务,这种事务被称为显式事务。如:\nbegin; update accounts set balance = balance - 100 where id = 1; update accounts set balance = balance + 100 where id = 2; commit; -- 试图把事务内的所有sql所做的修改永久保存 -- rollback; -- 可以主动用 rollback 回滚事务 数据库事务具有 acid 共 4 个特性:\natomic 原子性(要么全部执行,要么全部不执行)、 consistent 一致性(事务完成后,所有数据的状态都是一致的)、 isolation 隔离性(多个事务并发执行,每个事务作出的修改必须与其他事务隔离)、 duration 持久性(即事务完成后,对数据库的修改被持久化存储)。 对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。sql 标准定义了 4 种隔离级别:\nisolation level 脏读(dirty read) 不可重复读(non repeatable read) 幻读(phantom read) read uncommitted yes yes yes read committed - yes yes repeatable read - - yes serializable - - - jdbc2 jdbc 是什么 jdbc(java database connectivity),是 java 程序访问数据库的标准接口。\n使用 java 程序访问数据库时,java 代码并不是直接通过 tcp 连接去访问数据库,而是通过 jdbc 接口(java 标准库自带)来访问,而 jdbc 接口则通过 jdbc 驱动(数据库厂商提供,里面是接口的具体实现类)来实现真正对数据库的访问。如下:\n实际上,一个 mysql 的驱动就是一个 jar 包,它本身也是纯 java 编写的。我们自己编写的代码只需要引用 java 标准库提供的 java.sql 包下面的相关接口,由此再间接地通过 mysql 驱动的 jar 包通过网络访问 mysql 服务器,所有复杂的网络通讯都被封装到 jdbc 驱动中,因此,java 程序本身只需要引入一个 mysql 驱动的 jar 包就可以正常访问 mysql 服务器,如下:\n*注意:java 程序编译期仅依赖 java.sql 包,不依赖具体数据库的 jar 包,可随时替换底层数据库,访问数据库管理员的 java 代码基本不变。\n前面已经说过,所谓的 jdbc 驱动,其实就是一个第三方的 jar 包,我们可能通过添加一个 maven 依赖引入它:\n\u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;mysql\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;mysql-connector-java\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;!-- 与安装的 mysql 版本对应 --\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;!-- 注意是 runtime --\u0026gt; \u0026lt;/dependency\u0026gt; 其中,添加依赖的 scope 是 runtime ,因为编译 java 程序并不需要 mysql 的这个 jar 包,只有在运行期才需要使用。如果改为 compile ,虽然也能正常编译,但是在 ide 里写程序的时候,会多出一大堆类似 com.mysql.jdbc.connection 这样的类,非常容易与 java 标准库的 jdbc 接口混淆,所以坚决不要设置为 compile 。\njdbc 连接 connection 代表一个 jdbc 连接,它相当于 java 程序到数据库的连接(通常是 tcp 连接)。打开一个 connection 时,需要准备 url、用户名和口令,才能成功连接到数据库。\nurl 是由数据库厂商指定的格式,例如,mysql 的 url 是:\njdbc:mysql://\u0026lt;hostname\u0026gt;:\u0026lt;port\u0026gt;/\u0026lt;db\u0026gt;?key1=value1\u0026amp;key2=value2 假设数据库运行在本机 localhost ,端口使用标准的 3306 ,数据库名称是 learnjdbc ,那么 url 如下:\njdbc:mysql://localhost:3306/learnjdbc?usessl=false\u0026amp;characterencoding=utf8 其中,后面的两个参数表示不使用 ssl 加密,使用 utf-8 作为字符编码。\n要获取数据库连接,使用如下代码:\n// jdbc 连接的 url, 不同数据库有不同的格式: string jdbc_url = \u0026#34;jdbc:mysql://localhost:3306/test\u0026#34;; string jdbc_user = \u0026#34;root\u0026#34;; string jdbc_password = \u0026#34;password\u0026#34;; // 获取连接: connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password); // todo: 访问数据库... // 关闭连接: conn.close(); 核心代码是 drivermanager 提供的静态方法 getconnection() , drivermanager 会自动扫描 classpath,找到所有的 jdbc 驱动,然后根据我们传入的 url 自动挑选一个合适的驱动。\n因为 jdbc 连接是一种昂贵的资源,所以使用后要及时释放。使用 try (resource) 来自动释放 jdbc 连接是一个好方法:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { // ... } jdbc 查询 获取到 jdbc 连接后,下一步就可以查询数据库了。查询数据库分以下几步:\n通过 connection 提供的 createstatemetn() 方法创建一个 statement 对象,用于执行一个查询; 执行 statement 对象提供的 executequery(\u0026quot;select * from students\u0026quot;) 并传入 sql 语句,执行查询返回的结果集,使用 resultset 来引用这个结果集; 反复调用 resultset 的 next() 方法来读取每一行结果。如下: try (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (statement stmt = conn.createstatement()) { try (resultset rs = stmt.executequery(\u0026#34;select id, grade, name, gender from students where gender=1\u0026#34;)) { while (rs.next()) { long id = rs.getlong(1); // 注意:索引从1开始 long grade = rs.getlong(2); string name = rs.getstring(3); int gender = rs.getint(4); } } } } 需要注意以下几点:\nstatement 和 resultset 都是需要关闭的资源,因此嵌套使用 try (resource) 确保及时关闭; rs.next() 用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得 resultset 时当前行不是第一行); resultset 获取列时,索引从 1 开始而不是 0 ;(思考一下,这是为什么?😏) 必须根据 select 的列对应的位置来调用 getlong(1),getstring(2) 这些方法,否则对应位置的数据类型不对,将报错。 关于 sql 注入攻击\n使用 statement 拼字符串非常容易引发 sql 注入的问题,因为 sql 参数往往是从方法参数传入的。\n如何避免 sql 注入攻击呢?一个办法是对所有字符串进行转义(很麻烦,推荐),另一个办法就是使用 preparedstatement (比 statement 更快更安全)。\n使用 preparedstatement 可以完全避免 sql 注入的问题,因为 preparedstatement 始终使用 ? 作为占位符,并且把数据连同 sql 本身传给数据库,这样可以保证每次传给数据库的 sql 语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。\n如此,上面使用 statement 的代码改为使用 preparedstatement ,如下:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (preparedstatement ps = conn.preparestatement(\u0026#34;select id, grade, name, gender from students where gender=? and grade=?\u0026#34;)) { // 必须首先调用setobject() 设置每个占位符 ? 的值 ps.setobject(1, \u0026#34;m\u0026#34;); // 注意:索引从1开始 ps.setobject(2, 3); try (resultset rs = ps.executequery()) { while (rs.next()) { long id = rs.getlong(\u0026#34;id\u0026#34;); long grade = rs.getlong(\u0026#34;grade\u0026#34;); string name = rs.getstring(\u0026#34;name\u0026#34;); string gender = rs.getstring(\u0026#34;gender\u0026#34;); } } } } 在 resultset 获取列时,索引从 1 开始而不是 0 ,在 preparestatement 的设置占位符时索引也是从 1 开始。\r注意到 jdbc 查询的返回值总是 resultset ,即使我们写这样的聚合查询 select sum(score) from ... ,也需要按结果集读取:\nresultset rs = ... if (rs.next()) { double sum = rs.getdouble(1); } jdbc 更新 数据库操作总结起来就四个字:增查改删,crud(create,retrieve,update 和 delete)。\n查询,就是使用 preparedstatement 进行各种 select ,然后处理结果集。\n插入\n插入操作是 insert ,即插入一条新记录。通过 jdbc 进行插入,本质上了也是用 preparedstatement 执行一条 sql 语句,不过最后执行的不是 exectequery() ,而是 execteupdate() 。示例代码如下:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (preparedstatement ps = conn.preparestatement(\u0026#34;insert into students (id, grade, name, gendar) values (?,?,?,?)\u0026#34;)) { ps.setobject(1, 999); // 注意:索引从1开始 ps.setobject(2, 1); // grade ps.setobject(3, \u0026#34;bob\u0026#34;); // name ps.setobject(4, \u0026#34;m\u0026#34;); // gender int n = ps.executeupdate(); // 1 返回 int,表示插入的记录数量 } } 插入并获取主键\n如果数据库的表设置了自增主键,那么在执行 insert 语句时,并不需要指定主键,数据库会自动分配主键。\n对于使用自增主键的程序,有个额外的步骤,就是如何获取插入后的自增主键的值?要获取自增主键,不能先插入,再查询。因为两条 sql 执行期间可能有别的程序也插入了同一个表。获取自增主键的正确写法是在创建 preparedstatement 的时候,指定一个 return_generated_keys 标志位,表示 jdbc 驱动必须返回插入的自增主键。示例代码如下:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (preparestatement ps = conn.preparestatement( \u0026#34;insert into students (grade, name, gender) values (?,?,?)\u0026#34;, statement.return_generated_keys)) { ps.setobject(1, 1); // grade ps.setobject(2, \u0026#34;bob\u0026#34;); // name ps.setobject(3, \u0026#34;m\u0026#34;); // gender int n = ps.executeupdate(); // 1 try (resultset rs = ps.getgeneratedkeys()) { if (rs.next()) { long id = rs.getlong(1); // 注意:索引从1开始 } } } } 观察上述代码,有两点注意事项:\n调用 preparestatement() 时,第二个参数必须传入常量 statement.return_generated_keys ,否则 jdbc 驱动不会返回自增主键; 执行 execteupdate() 方法后,必须调用 getgeneratedkeys() 获取一个 resultset 对象,这个对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取自增主键的值。注意,如果一次插多条记录,那么这个 resultset 对象就会有多行返回值。如果插入时有多列自增,那么 resultset 对象的每一行都会对应多个自增值(自增列不一定必须是主键)。 更新\n更新操作是 update 语句,它可以一次更新若干列的记录。更新操作和插入操作在 jdbc 代码的层面上实际上没有区别,除了 sql 语句不同:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (preparedstatement ps = conn.preparestatement(\u0026#34;update students set name=? where id=?\u0026#34;)) { ps.setobject(1, \u0026#34;bob\u0026#34;); // 注意:索引从1开始 ps.setobject(2, 999); int n = ps.executeupdate(); // 返回更新的行数 } } *注意, execteupdate() 返回数据库实际更新的行数。返回结果可能是正数,也可能是 0 (表示没有任何记录更新)。\n删除\n删除操作是 delete 语句,它可以一次删除若干列。和更新一样,除了 sql 语句不同外,jdbc 代码都是相同的:\ntry (connection conn = drivermanager.getconnection(jdbc_url, jdbc_user, jdbc_password)) { try (preparedstatement ps = conn.preparestatement(\u0026#34;delete from students where id=?\u0026#34;)) { ps.setobject(1, 999); // 注意:索引从1开始 int n = ps.executeupdate(); // 删除的行数 } } jdbc 事务 要在 jdbc 中执行事务,本质上就是如何把多条 sql 包裹在一个数据库事务中执行。如下:\nconnection conn = openconnection(); try { // 关闭自动提交 conn.setautocommit(false); // 执行多条 sql 语句 insert(); update(); delete(); // 提交事务 conn.commit(); } catch (sqlexception e) { // 回滚事务 conn.rollback(); } finally { conn.setautocommit(true); conn.close(); } 实际上,默认情况下,我们获取到 connection 连接后,总是处于“自动提交”模式(一条 sql 语句其实就是一个“隐式事务”)。只要关闭了 connection 的 autocommit ,那么就可以在一个事务中执行多条语句,事务以 commit() 方法结束。\n如果要设定事务的隔离级别,可以使用如下代码:\n// 设定隔离级别为read committed: conn.settransactionisolation(connection.transaction_read_committed); 如果没有调用上述方法,那么会使用数据库的默认隔离级别。mysql 的默认隔离级别是repeatable read。\njdbc batch 通过一个循环来执行每个 preparedstatement 虽然可靠,但是性能很低。sql 数据库对 sql 语句相同但参数不同的若干语句可以作为 batch 执行(批量执行),这种操作有特别优化,速度远远快于循环执行每个 sql。示例代码如下:\ntry (preparedstatement ps = conn.preparedstatement(\u0026#34;insert into students (name, gender, grade, score) values (?,?,?,?)\u0026#34;)) { // 对同一个 preparedstatement 反复设置参数并调用 addbatch(): for (student s : students) { ps.setstring(1, s.name); ps.setboolean(2, s.gender); ps.setint(3, s.grade); ps.setint(4.s.score); ps.addbatch(); // 添加到 batch } // 执行 batch int[] ns = ps.executebatch(); for(int n : ns) { system.out.println(n + \u0026#34; insert.\u0026#34;); // batch 中每个 sql 执行的结果数量 } } 执行 batch 和执行一个 sql 不同点在于,需要对同一个 preparedstatement 反复设置参数并调用 addbatch() ,这样就相当于给一个 sql 加上了多组参数,相当于变成了“多行”sql。\n第二个不同点是调用的不是 executeupdate() ,而是 executebatch() ,因为我们设置了多组参数,相应地,返回结果也是多个 int 值,因此返回类型是 int[] ,循环 int[] 数组即可获取每组参数执行后影响的结果数量。\njdbc 连接池 在执行 jdbc 的增删改查的操作时,如果每一次操作都来一次“打开连接,操作,关闭连接”,那么创建和销毁 jdbc 连接的开销就太大了。\n为了避免频繁地创建和销毁 jdbc 连接,我们可能通过连接池(connection pool)复用已经创建好的连接。jdbc 连接池有一个标准的接口 javax.sql.datasource 。要使用 jdbc 连接池,我们必须选择一个 jdbc 连接池的实现。常用的 jdbc 连接池有:\nhikaricp(使用最广泛) c3p0 bonecp druid(阿里的,国内使用最多) 此处,我们以 hikaricp 为例,要使用 jdbc 连接池,先添加 hikaricp 的依赖:\n\u0026lt;dependency\u0026gt; \u0026lt;groupid\u0026gt;com.zaxxer\u0026lt;/groupid\u0026gt; \u0026lt;artifactid\u0026gt;hikaricp\u0026lt;/artifactid\u0026gt; \u0026lt;version\u0026gt;2.7.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 紧接着,创建一个 datasource 实例,这个实例就是连接池:\nhikariconfig config = new hikariconfig(); config.setjdbcurl(\u0026#34;jdbc:mysql://localhost:3306/test\u0026#34;); config.setusername(\u0026#34;root\u0026#34;); config.setpassword(\u0026#34;password\u0026#34;); config.adddatasourceproperty(\u0026#34;connectiontimeout\u0026#34;, \u0026#34;1000\u0026#34;); // 连接超时:1秒 config.adddatasourceproperty(\u0026#34;idletimeout\u0026#34;, \u0026#34;60000\u0026#34;); // 空闲超时:60秒 config.adddatasourceproperty(\u0026#34;maximumpoolsize\u0026#34;, \u0026#34;10\u0026#34;); // 最大连接数:10 datasource ds = new hikaridatasource(config); 有了连接池以后,我们通过 ds.getconnection() 来代替 drivermanager.getconnection() 获取一个连接,如下:\ntry (connection conn = ds.getconnection()) { // 在此处获取连接 // ... } // 在此处“关闭”连接(不是真正的关闭,只是释放到连接池中,以便下次获取连接时直接返回) 连接池内部维护了若干个 connection 实例,如果调用 ds.getconnection() ,就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对 connection 调用 close() ,那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的 sql 语句。\n通常连接池提供了大量的参数可以配置,例如,维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,需要根据应用程序的负载合理地配置这些参数。此外,大多数连接池都提供了详细的实时状态以便进行监控。\n","date":"2022-01-17","permalink":"https://loveminimal.github.io/posts/sql/","summary":"\u003cp\u003e🔔 摘录自 \u003ca href=\"https://www.liaoxuefeng.com/wiki/1177760294764384\"\u003e廖雪峰老师的 SQL 教程\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eSQL,结构化查询语言(Structured Query Language ),是一种数据库查询和程序设计语言,用于访问和操作数据库系统。SQL 语句既可以查询数据库中的数据,也可以插入、更新和删除数据库中的数据,还可以对数据库进行管理和维护操作。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e= 数据库能听懂的语言 \u0026ndash; SQL*\u003c/p\u003e\n\u003c/blockquote\u003e","title":"sql"},{"content":"i.e.reflection and generic\n反射1 反射,i.e.reflection,java 的反射是指程序在运行期可以拿到一个对象(实例)的所有信息。\n正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例。反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。\n除了 int 等基本类型外,java 的其他类型全部都是 class (包括 interface ), class 的本质是数据类型。\nclass 在由 jvm 在执行过程中动态加载的。\njvm 在第一次读取到一种 class 类型时,将其加载进内存。每加载一种 class ,jvm 就为其创建一个 class 类型的实例,并关联起来。\n*注意:这里的 class 类型是一个类名为 class 的 class (即 class.class ) ,它长这样:\npublic final class class { private class() {} } 以 string 类为例,当 jvm 加载 string 类时,它首先读取 string.class 文件到内存,然后,为 string 类创建一个 class 实例并关联起来:\nclass cls = new class(string); 这个 class 实例是由 jvm 内部创建的,其构造方法是 private ,只有 jvm 能创建 class 实例,我们自己的 java 程序是无法创建 class 实例的。\n所以,jvm 持有的每个 class 实例都指向一个数据类型( class 或 interface ),一个 class 实例包含了该 class 的所有完整信息:\n由于 jvm 为每个加载的 class 创建了对应的 class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获取了某个 class 实例,我们就可以通过这个 class 实例获取到该实例对应的 class 的所有信息。\n这种通过 class 实例获取 class 信息的方法称为反射(reflection)。\n如何获取一个 class 的 class 实例呢?\n// 方法一 // 直接通过一个 class 的静态变量 class 获取 class cls = string.class; // 方法二 // 通过该实例变量(如果有一个实例变量)提供的 getclass() 方法获取 string s = \u0026#34;hello\u0026#34;; class cls = s.getclass(); // 方法三 // 通过静态方法(如果知道一个 class 的完整类名) class.forname() 获取 class cls = class.forname(\u0026#34;java.lang.string\u0026#34;); jvm 在执行 java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。\n泛型2 泛型是一种“代码模板”,可以用一套代码套用各种类型。\n在讲解什么是泛型之前,我们先观察 java 标准提供的 arraylist ,可以看作“可变长度”的数组,实际上它内部就是一个 object[] 数组,配合存储一个当前分配的长度。\npublic class arraylist { private object[] array; private int size; public void add(object e) { ... } public void remove(int index) { ... } public object get(int index) { ... } } 如上所示,arraylist 的 get 方法返回的是一个 object 类型的数据。如此,当你用它存储 string 类型的时候,获取的结果其实需要强制转型(由 object 转型为 string )。如下:\narraylist list = new arraylist(); list.add(\u0026#34;hello\u0026#34;); // 获取到 object ,必须强制转型为 string string first = (string) list.get(0); list.add(new interger(123)); // error: classcastexception string second = (string) list.get(1); 并且很容易出现误转型 classcastexception 。\n要解决上述问题,我们可以为 string 单独编写一种 arraylist :\npublic class stringarraylist { private string[] array; private int size; public void add(string e) { ... } // 存 string public void remove(int index) { ... } public string get(int index) { ... } // 取 string } 存取 string 的问题暂时解决了,然而存取 integer 、 person 等其他类型呢?当然,我们可以用上述方式为其创建对应的类,然而实际上这实际上是不科学的,太多了。\n为了解决这个新的问题,我们必须把 arraylist 变成一种模板: arraylist\u0026lt;t\u0026gt; ,代码如下:\npublic class arraylist\u0026lt;t\u0026gt; { private t[] array; private int size; public void add(t e) { ... } public void remove(int index) { ... } public t get(int index) { ... } } 如此,我们在存取 string 类型的时候,就可以用 arraylist\u0026lt;string\u0026gt; 了,如下:\narraylist\u0026lt;string\u0026gt; strlist = new arraylist\u0026lt;string\u0026gt;(); strlist.add(\u0026#34;hello\u0026#34;); // ✔ string s = strlist.get(0); // ✔ strlist.add(new integer(123)); // ✘ compile error integer n = strlist.get(0); // ✘ compile error 看,泛型,其实是一种更高层次上的抽象。\r","date":"2021-12-02","permalink":"https://loveminimal.github.io/posts/reflection-and-generic/","summary":"\u003cp\u003ei.e.Reflection And Generic\u003c/p\u003e","title":"反射与泛型"},{"content":" “道可道,非常道。名可名,非常名。”\n无极1 _无极,是道家的概念,指无形无象的宇宙原始状态。 由收敛自然而然彰显出发散的状态。\n时空穿梭的极大魅力所在 -- 你可以亲眼看到!宇宙之初,有时、空吗?时、空的源头是什么呢?\r“无极”出自《道德经》,一种古代哲学思想,指称道的终极性的概念,代表着上古华人对事物产生之前状态的抽象理解。\n数学的运算过程本质上就是收敛与发散的过程。古人称收敛与发散这对矛盾为“阴阳”。收敛与发散是宇宙间最本质的矛盾,其他一切的矛盾都是在收敛与发散的基础上通过组合与迭代形成的,比如善恶、有无、动静等等。收敛可以自然而然彰显出发散就是“无极”,所以有了老子给出的“知其白,守其黑,为天下式。为天下式,常德不忒,复归于无极”的定义。《道德经》本身就是以“无极”为核心思想建立辩证法的书,不知“无极”,那么对“有无”“无为”“道德”的解释就都是无根之谈。\n谁能想到,竟然对数学学习还有启发……\r现代科学认为:宇宙还未诞生,虚无缥缈,宇宙还处于一种没有前后,没有左右,没有上下,没有中心,没有边界的混沌状态。这与上古华人对宇宙大爆炸之前的状态的抽象理解不谋而合。(古圣先贤把这种抽象理解的混沌状态称之为“无极”。)\n发散和收敛是“阳”与“阴”的本质,也是宇宙间最基本的矛盾,有与无、善与恶、动与静、外与内、正与反、前与后、荣与辱、明与暗、直与曲、实与虚、突破与积累、张扬与谦虚、散发与收藏等矛盾都是在发散与收敛的基础上建立的。科学也是如此,作为自然科学基础的数学的运算过程本身就是一个发散和收敛的过程。\n在“阴阳”的基础上可以给出“无极”的定义:阳代表的发散和阴代表的收敛是宇宙间最基本的矛盾,阴的收敛自然而然的便会彰显出阳的发散,从而使二者即对立又统一,处于一种由至阴而彰显至阳的“无极”状态。\n“无极”的状态是收敛而来的,收敛会导致消失,所以代表“无”。\n根据质量守恒定律和能量守恒定律可知,在一个系统内,守恒是一个重要的性质。那么,当代表阴的黑色不再保持其收敛的本性而开始发散从而彰显时,此消彼长,代表阳的白色也要相应的收敛来保持守恒,从而你中有我、我中有你,阴阳双方相互运动、相互作用、相互转换,形成了“太极”。\n“太极”是由发散得到的,发散便会彰显,所以代表“有”。\n道家认为单纯的白色就是“无极”(也被称为先天太极),而世人所熟知的太极阴阳鱼则被称为“太极”(也被称为后天太极),黑色从白色中显现并最终形成太极阴阳鱼的过程便是“无极生太极”。这也是“自无极而为太极”的道理。\n本质规律是收敛于内的“无”(负阴),外在现象是发散于外的“有”(抱阳),本质规律决定外在现象,本来互相冲突、矛盾、对立的阴阳二气(冲气)因为各自遵从本性的行为反而能相合(以为和),收敛自然而然会彰显出发散,阴阳既对立又统一,这就是“万物负阴而抱阳,冲气以为和”的道理。\n以“无极”为基本观得出了在哲学上普遍性达到极限程度的辩证法规律只有一个,即以“无极”为本质的对立统一规律。\n量变质变规律和否定之否定规律是对立统一规律的表现形式:量变是一个积累的过程,积累是收敛的一种表现形式,质变是一个突破的过程,突破是发散的一种表现形式;为了达到质变的目的,需要通过长期的曲折方式在量变的过程中积累足够的条件,“曲折”的出现是因为质变的目标虽然是“直”的,但是积蓄力量、等待时机的量变过程具有隐藏、蛰伏、迂回、示弱、积累、浓缩等收敛性,是一个由收敛自然而然彰显出发散的过程,也是一个“累曲为直”的过程。\n教员的矛盾论也是对立统一的集大成者!\r对立统一、量变质变和否定之否定规律之间的关系也可以看作是“天地人”的关系。\n对立统一规律天生存在于万事万物之中,是“天”。对立统一规律在事物发展上的基本表现形式是量变质变规律,所以量变质变规律是“地”。“天”与“地”的存在都是不以人类意志而改变的,人类的主观能动性只能影响否定之否定的过程,即曲折的量变过程要持续多久、要如何进行曲折的量变、曲折的量变最终能不能达到质变这个“直”的要求,可以受到人类主观能动性的影响,所以否定之否定规律是“人”。\n以“无极”为核心思想可以将对立统一、量变质变和否定之否定规律紧密的联系在一起,体现着联系的普遍性。\n“无极”代表的是对立统一的本质,是收敛而来的,是对立统一静止的、绝对的、先天的状态,是“无”;“太极”代表的是对立统一的表现形式,是发散而来的,是对立统一运动的、相对的、后天的状态,是“有”。\n无极者,极限的收敛,以至于无。\r了解了二者的辩证关系,《道德经》中 “无为”“有无相生”“天下万物生于有,有生于无”“有物混成,先天地生”“无,名天地之始;有,名万物之母。故常无,欲以观其妙;常有,欲以观其徼。此两者同出而异名,同谓之玄”“反者道之动”等理念就都很容易理解了。可见,《道德经》是一部以“无极”为核心思想组建辩证法的书。\n太极之图,也是为了便于人们理解的象,是理,真正的太极,阴阳浑然天成,并没有什么界限。\r太极2 所谓太极即是阐明宇宙从无极而太极,以至万物化生的过程。其中的太极即为天地未开、混沌未分阴阳之前的状态。\n易经系辞:“是故易有太极,是生两仪”。两仪即为太极的阴、阳二仪。“太极”是易学的基本概念,最初见于《上系》:“易有太极,是生两仪,两仪生四象,四象生八卦,八卦定吉凶,吉凶生大业。”同时与“大衍筮法”相关,即《上系》所载:“大衍之数五十,其用四十有九。分而为二以象两,挂一以象三,揲之以四,以象四时,归奇于扔以象闰。五岁再闰,故再扐而后挂……是故四营而成易,十有八变而成卦。”围绕大衍筮法,历代以不同侧重对太极的哲学涵义进行了探索。\n吉凶者,顺逆也。\r太极是中国文化史上的一个重要概念、范畴,就迄今所见文献看,《易传》:“易有太极,是生两仪。两仪生四象,四象生八卦。“太极观念这种迷离恍惚地看待万事万物的现象和本质的人生态度,以及这种思维方式本身,实则包含着清醒睿智的哲思,其终极目的是希望人类活动顺应大道至德和自然规律,不为外物所拘,“无为而无不为”,最终到达一种无所不容的宁静和谐的精神领域。\n《易系辞传》(西汉马王堆出土版本)记载有“古者伏羲氏之王天下也,仰则观象于天,俯则观法于地,观鸟兽之文与地之宜,近取诸身,远取诸物,于是始作八卦”的内容。意思是成卦的过程,先是有太极,尚未开始分开蓍草(易占卜用蓍草做工具),分蓍占后,便形成阴阳二爻,称做两仪。二爻相加,有四种可能的形象,称为四象。由它们各加一爻,便成八卦。这里讲的是八卦画出的过程。原与天文气象及地区远近方向相关,后来被宋代的理学家以哲理方式进一步阐释。\n太极,中国古代哲学用以说明世界本原的范畴,“太极”一词出于《系辞上》。“太极”有多重意义:一为生生之源;生生之源的生产方式犹如细胞裂变:一分为二,二分为四,四分为八,八八六十四;生生之源的生生是一个自然过程。\n关于太极的含义,有以下几种解释:\n(1)指宇亩最初浑然一体的元气。唐颖达《周易正义》:“太极谓天地未分之前,元气混而为一。”《易纬·乾凿度》认为“有形生于无形”,提出“有太易,有太初,有太始,有太素”四阶段来解释“易有太极”。以未见气时为太易,气初为太初,形之始为太始,质之始为太素。气形质混然一体而未分离的状态,称之为浑沌。浑沌之物即是古人所说的元气。以元气未分状态为太极,斌以其原始物质的含义。这种观点在两汉较为普遍,如刘歆:“太极中央元气”;王充引易学家的话说:“元气未分,混沌为一”。郑玄则以“淳合未分之气”解释大极。\n(2)以虚无本体为太极,如王弼解释“大衍之数,其一不用”说:“不用而用以之道,非数而数以之成,斯易之太极也。”以“一”为太极,认为此“一”不是数,而是“无”,“无”为四十九之策数形成的根据。韩康伯注释“易有太极,是生两仪”说:“夫有必始于无,故太极生两仪也。”\n(3)大衍之数的四十九数未分为太极。崔憬说:“四十九数合而未分,是象太极也。今分而为二,以象两仪矣。分揲其蓍,皆以四为数。一策一时故四策以象四时也。”(李鼎祚《周易集解》引,据《黄氏逸书考》本)崔憬对大衍之数的论述及其太极说,不以不用之一为虚无实体,以四十九数未分为大极,并且认为八卦涵蕴在大衍之数中。此种观点,在易学哲学史上有其重要意义,为宋易将汉易的宇宙生成论转为本体论提供了方法。\n(4)以阴阳混合未分为太极。周敦颐《太极图说》:“无极而太极,太极动而生阳,动极而静,静而生阴,静极复动,一动一静,互为其根,分阴分阳。两仪立焉。”周敦颐以后,对太极的解释分为三大流派。a、以邵雍为代表,以数说大极。指出“太极一也,不动生二,神也”;以一分而为奇偶解释太极生两仪。并认为在人则“心为大极”,在天地则“道为太极。”b、以朱熹为代表,以理说太极。“极是道理之极至,总天地万物之理便是太极。”(《朱子太极图说解》)“太极者,其理也。”(《周易本义·系辞上》)太极乃天地万事万物之理的总和,而在具体的事物中也有太极之理。故曰:“人人有一太极,物物有一太极。”(《朱子语类》)c、以张载“一物而两体,其大极之谓与?”为代表,认为无有阴阳,其合一则变化莫测,其对立则相互推移,以此说明太极乃天地万物运动变化的根源。王夫之阐发张载的学说,坚持以对立统一观说太极。断言太极和两仪是体用关系,非父子关系。并认为太极为阴阳二气合一的实体,此实体自身具有运动的本性和变化规律。且寓于天地万物之中,一切现象都是此阴阳统一体不同的表现形式,发挥了以“太和之气”为世界本原的思想。\n以对立统一观说太极。\r(5)太极也指:“一阴一阳之谓道”也正是对太极的解释。那么何为道呢?指天道,地道,人道。万事、万物都有两面,并且不断的变化,也有一定的关联。天道、地道大家根据科学已经证明并且好理解。关键是人道,人道又主要就是指思想,思想决定了行为、态度。讲的也是大家在尊重自己的思想,尊重自然规律。学会控制情绪,如不生恶念,不怕恶念。探求正确的方法才是我们应该努力。\n天、地与人,自然与社会者也。天地人,混为社会,人处其中,得道则知顺逆。\r一为无;二为阴阳两分而合和;三为生生之源;四为有序运动;五在时间表现为无时不在,在空间上表现为无处不在;六位于形而上却体现在形而下。\n太极无形无体。无,不意味着绝对空虚,不意味着有无相对相应。太极之无,充满着无限生机;太极之无,实际之有无法与之相对相应。无中生有,生出了天地万物与最初的一男一女。无之广大,成千上万个太阳系都可以轻松地容纳其中。\n太极阴阳两分而合和。太极,外表现为阴阳合和之大一,内表现为阴阳两分。一分为二又合二而一。两个元素之间相互矛盾又相互依存,须臾不可分离。合则生,散则死。\n生生之源。太极的出现,意味着中华民族找到了形而上的造物主——生生之源。在造物功能上,太极相似于《圣经》中的上帝。太极之生生,《系辞》表述为“一生二,二生四,四生八,八八六十四”;《道德经》则表述为“道生一,一生二,二生三,三生万物。”\n有序运动。绝对之动,相对之静,是太极的运动特征。太极之动,是有序的运动。运动的方式,《周易》界定为“反复”与“原始反终”,《道德经》界定为“反动”。太极之动,一是有序运动,二是整体运动。在有序运动之中,太极及其衍生物保持着整体性与系统性。八卦与六十四卦,独立之卦并不独立存在,牵一发而动全身。在八卦之中,一可以看到两两相互交合之动,二可以看到整体圆周循环之动。六十四卦之中,还可以看到一卦之中的穷上返下之动。八卦有序之动,展现出了春夏秋冬和东西南北。有序之动,不离时空。\n太极在时间上表现为无始无终,在空间上表现为无处不在。眼睛看不到它,但它确实存在。 太极位于形而上,但在形而下的万事万物之中处处可以看到太极。由太极衍生出来的天地万物以及一男一女,不但不能脱离母体,而且处处反映出了母体的特征——一阴一阳。\n何谓太极,先哲解释不一。虞翻说:“太极,太一也。”韩康伯说:“太极者,无称之称。”孔颖达说:“太极即是太初太一也。”苏子瞻说:“太极者,有物之先也。”朱熹说:“太极者,理也。”来知德说:“太极者,至极之理也。”焦循说:“太极犹言大中也。”各家注解虽不相同,其实都以太极为天地人物的本有之体,简称为本体,此体圆含无穷的形象与无尽的功用,本体形象功用不相分离。不学周易,不明易理,则不知吾人本有此体,不知万物皆备于我,以致凡事舍本逐末,向外驰求,造成一己与群众的种种灾祸。学易明理,则能趋吉避凶。凶之最大者为生死问题不能解决,吉之最大者即是学为圣人。欲学圣人,必须先求解决生死问题。《论语》记载,子路问死,孔子解答:“未知生,焉知死。”有生始有死,欲知如何解决死的问题,必须了解生的来源,生的来源就是太极。\n太极生两仪者,太极寂然不动,本无形象,唯为清净光明之体,此为无生之理性。动则显象起用,乃名为生。所生之象,其数无穷,但始动之际,只有一明一暗两种形色,明色为阳,暗色为阴,因此称为阴阳两仪,仪如《孔氏正义》所释,作容仪讲,因其尚未成象,故不称为两象,只称两仪。但这两仪实为四象以至万象的基本结构,万象即由两仪细分而成,所以万象无不有阴阳两仪。万象之数虽然无穷,但基本之数则为阳奇阴偶。伏羲氏画卦时便发明极为简单的两画,以示无穷无尽的象数之源。\n两仪3 两仪,是中国道教文化术语,在中国古典哲学中指的是“阴阳”,主要为黑白双色,乃大道之本。天地初开,一切皆为混沌,是为无极,无极生太极,太极生两仪,两仪为阴阳。《易经》:“易有太极,始生两仪,两仪生四象,四象生八卦。”\n两仪在《易经》中指阴、阳。\n关于“两仪”之说,综合历代易学家的理论,计有八说:一说为阴阳,一说为天地,一说为奇偶,一说为刚柔,一说为玄黄,一说为乾坤,一说为春秋,一说为不变与变。但通常都是指阴阳。天地万物,世间万事,古人概之为:阴阳。阴阳也是天地出分之时,清气向上为天。浊气向下为地。天为阳,地为阴。天地万物,世间万事,古人概之为:阴阳。\n两仪者,相对之表,交互之源也。\r阴、阳两字的古义是背日和向日,起初并无任何哲学内涵。阴,《说文解字》曰:“暗也,水之南、山之北也”,《说文系传》 曰:“山北水南,日所不及”。阳,《说文解字》曰:“高明也。”《说文解字义证》:“高明也,对阴言也。”\r无极是空,太极是圆,两仪是黑白,四象是老阴少阳老阳少阴。至此,道生一,无极变太极;一生二,太极变两仪;二生三,两仪变四象;三生万物,四象旋转,世界诞生。八卦,正如其名,是对宇宙后续变化的推演。\n阴阳是“对立统一或矛盾关系”的一种划分或细分,两者是种属关系。中国的传统学术中,有所谓“孤阴不生,独阳不长”及“无阳则阴无以生,无阴则阳无以化”的观念。老子在《道德经》中说:“道生一,一生二,二生三,三生万物。万物负阴而抱阳,冲气以为和”。\n阴阳的特性如下:\n两者互相对立:万物皆有其互相对立的特性。如热为阳,寒为阴;天为阳,地为阴,说明了宇宙间所有事物皆对立存在。然这种相对特性并非绝对,而是相对。如上为阳,下为阴,平地相对于山峰,山峰为阳,平地为阴;但平地若相对于地底,则平地属阳、地底属阴,可见阴阳的相对性关系。\n两者相互依靠、转化、消长:阴阳存在着互根互依,互相转化的关系,阴中有阳,阳中有阴,任何一方都不可能离开另一方单独存在,因彼此的消长,阴阳可以变化出许多不同的现象分类。\n四象4 四象,指《易传》中的老阳,少阴,少阳,老阴,易传四象与星宿四象相互融合,青龙表少阳主春,白虎表少阴主秋,玄武老阴主冬,朱雀老阳主夏。四象又衍生出来了八卦(乾、兑、离、震、巽、坎、艮、坤)。\n《周易》,主要是讲阴阳、八卦、 64 卦变化的古书。《黄帝内经》(或说《素问》和《灵枢》)中的藏象学说,是阴阳五行学说在中医方面的应用。《周易》里面讲八卦、 64 卦的变化,没有涉及五行,这是因为,八卦系统与五行系统是两个各自独立的系统。\n四象学说之后,出现五行学说与八卦学说两个发展方向。 八卦系统与五行系统,根源都是太极、阴阳、四象学说。\n认识到这一点,有助于我们拨开迷障,深入探究这些古老学说的本质问题。四象学说的出现,有助于古人认识客观世界。“象”,就是以“象”类物,是古人的一种分类和分析事物的方法,是一种认识世界的手段和方法。\n古人的认识世界的方法论。\r四象是指:少阳、太阳、少阴、太阴——分别可以代表春、夏、秋、冬;生、长、老、死等等四类事物和现象,将事物和现象分成四个阶段、四种相联系的情况。为了更便于理解,古人又用了四种常见的事物代替上述四象。\n这四种常见的事物就是“木火金水”。具体代替方法就是:木——少阳;火——太阳;金——少阴;水——太阴。“木火金水”代替“少阳、太阳、少阴、太阴”以后,就不再是具体的生活中的这四种常见事物,它们变成了“四象”,被人为地赋予了概念,变成了哲学理论上的东西。以后,当古人认为其它事物和现象分别与“木、火、金、水”类似时,就可以分别被归类于“木火金水” 四象了。“木火金水”,就是后来五行学说中的“四行”。\n可以认为“木火金水”是一种指针,它指代四象,是一种表征。\r如果用数字 1、2、3、4 表示四象,根据“奇数为阳,偶数为阴”,则太阳、少阳对应 1、3,太阴,少阴对应 2、4。然而 “阴极阳生,阳极阴生” ,太阴为阴之所终阳之所始,太阳相反,而 1 为阳之始,2 为阴之始,因此应改为太阴为 1,太阳为 2,少阳为 3,少阴为 4,故水 1、火 2、木 3、金 4。\n1、2、3、4 之和为 10,10 可分成 1、2、3、4,所以,10 可以看作太极。太极分阴阳,10 可分为 5、5,一个为阴气 5,一个为阳气 5。\n木火相生为阳,“木、火”都是由“阳气 5”二分而来,所以归类为“两仪”中的“阳”。金水相生为阴,“金、水”都是由“阴气 5”二分而来,所以归类为“两仪”中的“阴”。\n由水变化到木,是阴气渐少,阳气渐多的过程。由火变化到金,是阳气渐少,阴气渐多的过程。所以,四象(木、火、金、水)用于对应一年四季时,分别可以对应春、夏、秋、冬。由春季到夏季,阳气渐升;由秋季到冬季,阴气渐升。\n用于四象对应生命时,“木、火、金、水” 分别可以代表生、长、老、死——表示生命从无到有(生),从有到强(长),从强到弱(老),从弱到无(死)的四个连续的过程。\n生长老死,成住坏空,皆为四象。\r根据阴阳理论,阴气与阳气是互根互生的。所谓“阴极阳生,阳极阴生”是也。\n以四象为例,阳气分为“木、火”,阴气分为“金、水”。“木、火、金、水”四象循环,就是一个阳气与阴气不断地互根互生的过程。“水”为阴气的极点,“阴极阳生”,所以“水”变化为“木”,即生“木”;“木”阳气继续上升,变化为“火”;“火”为阳气极点,“阳极阴生”,所以“火”变为“金”,即火生金;“金”阴气继续上升,变化为“水”,金生水。\n哎,原来如此,以前太执着于它们的字面意思,换成四象表征及阴阳互生,一下子就明了了。以事物本来的面目去认识事物,真理矣。\r上述四象还可用另一种方式表示——就是根据阴阳互根互生理论,用其“所生”(即产生的原因)来表示。比如,“木——3,火——2”,都源于“阳气 5”一分为二。所以木为“阳 3”,火为“阴 2”。木是因为“阳 5”分出了“阴 2”才出现,所以“木”可以说成“阳 3 生木”;同理,火是因为“阳 5”分出了“阳 3” 才出现,所以“火”可以说成“阴 2 生火”。在古代,阳对应“天”,阴对应“地”,所以“阳 3 生木”“阴 2 生火”可以说成“天三生木,地二生火”。再比如,“金——4,水——1”都源于“阴气 5”一分为二。所以水为“阳 1”,金为“阴 4”。金是因为“阴 5” 分出了“阳 1”才出现,所以可以说成“阴 4 生金”;同理,水是因为“阴数 5”分出了“阴 4” 才出现,所以可以说成“阳 1 生水”。故“天一生水,地四生金”。\nhmm……\r如上所说,四象(木、火、金、水)的阴阳变化次序和规律就此确立了,事物变化的一般规律就此确立了。\n四象已经具有明显的周而复始的性质了。四象对应一年四季,明显具有周期性变化规律。如果将植物的种子、动物的后代看作生命的特殊再生方式,那么,四象对应生命的生、长、老、死四个阶段,生命也是具有周期性的。四象变化,周而复始,是事物发展变化的一般规律。有一般,就有特殊。\n以一年四季为例,其变化规律与日月有关。日月几十亿年运行不息,是相对永恒的东西。据说地球的气候变化,7500 万年是一个周期,在这个期间内,四季(春夏秋冬四象)变化规律是大致不变的。对动植物生命而言,情况就不一样了,因为偶然性因素大为增加。以动物为例,有胎死腹中的,有幼年夭折的,有壮年早逝的——生、长、老、死四个阶段未完成就结束了。有的动物未留下后代,那就谈不上生命的再生(周而复始)了。对动植物生命而言,偶然性因素确实存在,但我们不能因此否定生命的周期性变化规律。其他事物变化也一样,我们不能因为有特殊变化而否定其一般规律。\n在把握事物一般规律的基础上,合理把握和解决偶然性因素导致的特殊变化,是我们研究事物规律的目的。导致四象一般变化的原因是什么呢?导致四象发生特殊变化的原因是什么呢?这里,需要谈到五行学说。\n是故易有太极,是生两仪,两仪生四象,四象生八卦。\n太极生两仪,是宇宙根本力量的第一变。在这一变完成以后,就产生了一个一级的“阴”和一级的“阳”。然而这两个一级的“阴”和“阳”内部仍然存在阴阳的力量的作用,还是会继续演变。这样的结果,一级的“阴”就产生了“阴中之阴”——老阴和“阴中之阳”——少阳;一级的“阳”就产生了“阳中之阴”——少阴和“阳中之阳”——老阳。老阴、少阳、少阴、老阳是第二级的阴阳组合,统称为“四象”。这个过程就叫做“两仪生四象”。\n在四象的基础上,由于阴阳力量的继续作用,又生成了新的阴阳组合。太阳分解为太阳之阳——“乾”和太阳之阴——“兑”;少阴分解为少阴之阳——“离”和少阴之阴——“震”;少阳分解为少阳之阳——“巽”和少阳之阴——“坎”;太阴分解为太阴之阳——“艮”和太阴之阴——“坤”。\n乾、兑、离、震、巽、坎、艮、坤是第三级的阴阳组合,统称为“八卦”。这个过程则称为“四象生八卦”。八卦不过是阴阳,四象不过是二物,太极动生阴阳,天地不过一阴一阳而已!\n四象与五行\n子平真诠:“天地之间,一气而已,惟有动静,遂分阴阳。有老少,遂分四象。老者极动极静之时,是为太阳太阴;少者初动初静之际,是为少阴少阳。有是四象,而五行具于其中矣。水者,太阴也;火者,太阳也;木者,少阳也,金者,少阴也;土者,阴阳老少,木火金水冲气所结也。”道出了四象与五行的关系:水火即太阴太阳,金木即少阴少阳,土者木金水火所冲结。说明五行由四象演化而来,而这四象又是从阴阳衍生出的。于是乎,五行说虽然不见诸《周易》经文,但也是本乎阴阳。\n又有:“河图之数,以一二三四五配六七八九十,先天之道也。故始于太阴之水,而终于冲气之土,以气而语其生之序也。盖未有五行之先,必先有阴阳老少,而后冲气,故生以土。终之既有五行,则万物又生于土,而水火木金,亦寄质焉,故以土先之。”\n五行之土\n“土”位于四象中间,不包含在四象循环周期内,如何正确理解“土”的含义,是正确理解四象与五行的关键。《周易参同契》:“日月为易,刚柔相当,土旺四季,罗络始终,青赤黑白,各居一方,皆秉中宫,戊己之功。”\n将五行对应时间和过程,《周易参同契》持“土旺四季”,持“土贯穿始终”之说。将五行对应地理四方,《周易参同契》持“土主中宫”之说。在这里,“土”是太极,是可以分出阴阳的太极。我赞同《周易参同契》的上述观点,持“土为太极”之说。“土旺四季,罗络始终”。正是因为有了贯穿始终的“土”,有了时间和过程的始终,一年才可以划分出“春夏秋冬”四象;生命才可以划分出“生长老死”四个阶段。\n《周易参同契》“皆秉中宫,戊己之功”。“土”指中央,有了中央,人们才可以区分“东南西北”四方,“东南西北”四方才可以长久存在。《周易参同契》“土旺四季”,因为有“土”,四季才可以周而复始,运行不息。中医支持“土为太极”的说法。\n中医藏象学说认为脾胃为土。人食五谷经过脾胃的运化而生成的水谷精微之气,即为后.天之气。传统中医认为,胃为后.天之本, 有“胃气”则生,无“胃气”则死。脾胃之气为后.天太极,是生命所必需的。中医有“补脾胃,养五脏”的治疗法则。\n古代有土生万物、土养万物、土载万物、土纳万物之说。这万物自然是指可以归类于“木火金水”四象的万物。实质就是——土生木火金水,土养木火金水,土载木火金水,土纳木火金水。\n将五行对应一年四季,“木火金水”分别对应“春夏秋冬”,土为阴阳——就是日月。日月分则为阴阳,合则为太极。有日月,才有春夏秋冬四季。将来,太阳月亮的寿命到了尽头,日月没有了,“春夏秋冬”自然就没有了——这是因为后.天太极变化了。\n将五行对应四方,“木火金水”分别对应“东南西北”,土为中央。中央可以无限小,小到一个点;可以无限大,大到与天地相同。“土”,可以理解为就是“天地”——有天地才有四方,没有天地就没有四方。其实,即使简单的只将“土”理解为“中央”,四方也是由中央生发的,没有中央原点,根本无法划分东南西北四方。\n在五行中,“土”为太极,太极分两仪,就是阴阳(也可说是阴土,阳土),阴阳分四象,就是“木火金水”。“土”不一分为二时,是“太极”。“土”一分为二时,是“阴,阳”,是“阴土”和“阳土”。“土”可四分为“木火金水”,是太极(阴阳)分为四象;“木火金水”可以合为“土”,是四象统一回归到太极。——这就是五行学说的实质、本质。\n八卦5 八卦,见于《周易·系辞下》云:“古者包牺氏之王天下也,仰则观象于天,俯则观法于地;观鸟兽之文与地之宜;近取诸身,远取诸物,于是始作八卦,以通神明之德,以类万物之情。”八卦生自太极、两仪、四象中,“四象生八卦”。\n它也是中国古老文化的深奥概念,是一套用三组阴阳组成的形而上的哲学 符号 。其深邃的哲理解释自然、社会现象。八卦成列,象在其中矣;因而重之,爻在其中矣;刚柔相推,变在其中矣;系辞焉而命之,动在其中矣。八卦成列的基础是易象,重卦的基础则在于爻变,“爻在其中矣”便是易道周流的内在动因。 八卦表示事物自身变化的阴阳系统 ,用“一”代表阳,用“- -”代表阴,用这两种符号,按照大自然的阴阳变化平行组合,组成八种不同形式,叫做八卦。八卦其实是最早的文字表述符号。\n它在中国文化中与“阴阳五行”一样用来推演世界空间时间各类事物关系的工具。每一卦形代表一定的事物。乾代表天,坤代表地,巽(xùn)代表风,震代表雷,坎代表水,离代表火,艮(gèn)代表山,兑代表泽。八卦就像八只无限无形的大口袋,把宇宙中万事万物都装进去了,八卦互相搭配又变成六十四卦,用来象征各种自然现象和人事现象,基于当今社会人事物繁多。\n乾(☰)、坎(☵)、艮(☶)、震(☳)、巽(☴)、离(☲)、坤(☷)、兑(☱)\n八卦有两种符号形式:一是三画卦的八经卦(三爻卦);一是六画卦的八卦(六爻卦)。\n八卦:乾 qián、坤 kūn、震 zhèn、巽 xùn、坎 kǎn、离 lí、艮 gèn、兑 duì。亦称经卦、单卦、三爻卦、小成之卦,由阳爻 yáo“——”、阴爻 yáo“- -”排列而成,是由三个爻 yáo 自下而上排列而成的记号。\n八卦有先天八卦与后天八卦之分。\n将两个单卦上下组合,即为六十四卦中的一卦,称重卦。\n宋代朱熹的《周易本义》写了《八卦取象歌》帮人记卦形:乾三连,坤六断;震仰盂,艮覆碗;离中虚,坎中满;兑上缺,巽下断。\n八卦的五行:只论后天五行:乾、兑为金,坤、艮为土,震、巽为木,坎为水,离为火。\n卦象也称类象,是象征类型而非某个单体。易因象设辞,象数是义理的基础,《易经》、《易林》、梅花易断法,都是因象而断,易象广大,具体断法看问题项目而确定一个小范围。\n《周易·说卦传》介绍了一些卦象,是基础卦象。\n宇宙观上:乾为天,坤为地,震为雷,巽为风,坎为水,离为火,艮为山,兑为泽。\r家庭观上:乾父也,坤母也,震长男,巽长女,坎中男,离中女,艮少男,兑少女。\r动物观上:乾为马,坤为牛,震为龙,巽为鸡,坎为豕,离为雉,艮为狗,兑为羊。\r身体观上:乾为首,坤为腹,震为足,巽为股,坎为耳,离为目,艮为手,兑为口。\r运动观上:乾健也,坤顺也,震动也,巽入也,坎陷也,离丽也,艮止也,兑说也。\r权力观上:乾为君,坤为众。 八卦有不同类型,宋代邵雍理论经朱熹传播以来,先天八卦、后天八卦为大众所知。\n先天八卦的主要依据在《周易·说卦传》:“天地定位,山泽通气,雷风相薄,水火不相射。八卦相错,数往者顺,知来者逆,是故易逆数也”。\n以乾配(代表)天,坤配地,兑配泽,离配火,震配雷,巽配风,坎配水,艮配山,依着阳自左边转,阴自右边转的原则,天尊而地卑,天居上,在南方,阳爻组成的四卦在左边,依次逆时针由乾到兑、兑到离、离到震排列出来;坤居下,在北方,阴爻产生的巽、坎、艮、坤四卦自右边顺时针方向,由乾到巽、巽到坎、坎到艮、艮到坤排列出来,由此产生了先天八卦图。\n后天八卦的主要依据在《周易·说卦传》:“帝出乎震,齐乎巽,相见乎离,致役乎坤,说言乎兑,战乎乾,劳乎坎,成言乎艮……”。\n后天八卦谈后天事象。以乾为父,坤为母,震为长男,巽为长女,坎为中男,离为中女,艮为少男,兑为少女。\n后天八卦配洛书数:乾六、兑七、离九、震三、巽四、坎一、艮八、坤二。歌诀:一数坎来二数坤,三震四巽是中分,五数中宫六乾是,七兑八艮九离门。\n八卦起源久远,夏商周时代就有连山,归藏,周易三种八卦体系,等到了北宋时,方确定了两种八卦,一个定名为先天八卦,另一个则定名为后天八卦。这两种八卦不仅名称不同,卦的方位也不相同。八卦乃是远古圣人,仰观天文,俯察地理所得,必然合乎天地规律,犹如道德经云:人法地,地法天,天法道,道法自然。现今人考察,果然如此:先天八卦顺应天时,后天八卦顺应地利,人和之而已!\n先天八卦,在时间上,从坤始,到艮止,顺时针排一圈就是一个循环:\n坤-震-离-兑-乾-巽-坎-艮 一天中,以坤乾离坎为子午卯酉,一年中,则为冬至、夏至、春分、秋分,余皆类推。一卦三爻,八卦共二十四爻,一爻一节气,一爻一小时。\n#+caption: 先天八卦\n兑 乾 巽 离 坎 震 坤 艮 八卦配九宫。九宫即洛书所指的九个方位,一般将后天八卦按方位装入洛书,中间空开,即形成所谓的“九宫八卦”。 后天八卦配洛书,其对应关系为“一宫坎(北),二宫坤(西南),三宫震(东),四宫巽(东南),五宫(中),六宫乾(西北),七宫兑(西),八宫艮(东北),九宫离(南)。”\n#+caption: 后天八卦\n巽 离 坤 震 兑 艮 坎 乾 所谓卦,其实是中国古代劳动人民通过测量太阳位置,从而知季节、记录劳作规律的手段。\n卦字的右边“卜”字,是象形,表示在地上竖杆子,右边那一点是太阳的影子。“卦”字左边的“圭”字是尺子,用来测量影子的长度位置。\n通过长期测量,古代劳动人民掌握了春耕秋收的各种季节规律。所谓八卦,应该是在地之八方测量结果的记录。\n八卦所属\n乾、兑(金);震、巽(木);坤、艮(土);离(火);坎(水)。 八卦生克\n金生水,水生木,木生火,火生土,土生金\r乾、兑(金)生坎(水),坎(水)生震、巽(木),震、巽(木)生离(火),\r离(火)生坤、艮(土),坤、艮(土)生乾、兑(金)。\r金克木,木克土,土克水,水克火,火克金\r乾、兑(金)克震、巽(木),震、巽(木)克坤、艮(土),坤、艮(土)克坎(水),\r坎(水)克离(火),离(火)克乾、兑(金)。 太极八卦图,以同圆内的圆心为界,画出相等的两个阴阳鱼表示万物相互关系。阴鱼用黑色,阳鱼用白色,这是白天与黑夜的表示法。阳鱼的头部有个阴眼,阴鱼的头部有个阳眼,表示万物都在相互转化,互相渗透,阴中有阳,阳中有阴,阴阳相合,相生相克,即现代哲学中和矛盾对立统一规律表示法。\n以圆心可分为四份定为四象。四象为太阳、太阴、少阳、少阴。四象表述空间的东西南北,时间的春夏秋冬。任何一组矛盾加中心,就构成为三才。古代哲学认为天、地、人为三才,又在四象的学说基础上,更进一层,又增加了阳明、厥阴两面项,它与四象组成六合之说。\n四象若加圆心就构成五行之说,南方为火,北方为水,东方为木,西方为金,中间为土。(也有用左青龙,即东方,为木;右白虎,即西方,为金;前朱雀,即南方,为火;后玄武,即北方,为水。)六合加圆心称为七星。四象通过“一分为二”的切分,又构成八卦图,先天八卦方位表示为:乾南、坤北、离东、坎西,兑东南,震东北,巽西南,艮西北。\n八卦加轴心称之为九宫。配九宫数为乾九,坤一,巽二,兑四,艮六,震八,离三,坎七,中央为五。\n“古者伏羲氏之王天下也,仰则观象于天,俯则观法于地,观鸟兽之文与地之宜,近取诸身,远取诸物,于是始作八卦。以通神明之德,以类万物之情。作结绳而为网罟,以佃以渔,盖取诸离。”伏羲因何而作八卦,这里说的很清楚-“作结绳而为网罟”,而不是为了什么虚无缥缈的事。\n“易有太极,是生两仪,两仪生四象,四象生八卦,八卦定吉凶,吉凶生大业。”八卦之所以能生大业,在于它本身就是被创造来有用的。八卦是从四象中分离出来,以便于应用。\n“上古结绳而治”就是依靠了八卦。用打结绳子来记事与传递信息有个基本条件:结绳的方式与含义,需要有一个软系统支持它,即依照什么规律来打结才能让人能看得懂。结绳需要八卦的支持配合,其实结绳是八卦的载体,否则就类似有电脑而无软件了。八卦在初期及很长的时段里是一个记事系统,而非作筮占用,上世纪已有学者意识到了这个事,但未见与结绳联系起来作深入探索。\n以事物本来的面目去认识事物,不可不深思之。\r在这个记事系统里,八八六十四卦的每个卦都被赋予了特定的含义,《周易》中的《说卦》、《序卦》、《杂卦》中大致保留了这类原始遗存。基础的八个卦中,如乾卦:“乾为天,为圆,为君,为父,为玉,为金,为寒,为冰,为大赤,为良马,为老马,为瘠马,为驳马,为木果”这些看似杂乱不相干的东西同在一个卦里的记载,恰恰证明了在未有文字之前,每个原卦都包含着古老的多种含义。这些含义被有文字后记录下来之前,应附加有口口相传的内容,在多卦编组下,相应于每一组结绳的示意,就可以表达和记录一个完整的意思。\n网罟就是渔网一类的网状物,用来“以佃以渔”。直观四象就明显看到三三线组合的网状格子,田字形是网的最小单元。网格状在龟甲上很常见,但要利用网状“为网罟”则需要易,要引入刚柔的概念,即“盖取诸离”。\n离卦是两个阳爻的中间为阴爻,中虚,为阴为柔。“盖取诸离”就是用多个离卦组合的形,创造了最早的编织物-网罟。大概就是交替用树枝与绳做编织,及用树枝捆成方形的硬边,用绳编织中间的软网。要网得住鱼,网的中部需要柔软扩展才可以。“盖取诸离”的重点是-网要大,中间要柔软。这就是“以制器者尚其象”取象的形产生的;最早的完全可于实用的器。\n伏羲之所以能在中华大地上第一个“王天下”,正是依靠了结绳记事和大量的渔获。这与史前的鸟崇拜鱼崇拜相一致。\n先天八卦6 先天八卦,起源于远古时代,其辩证思想是矛盾对立统一。\n先天八卦,相传来自于河图。它是乾坤定南北,坎离定东西,是天南地北为序,上为天为乾,下为地为坤,左为东为离,右为西为坎。故先天八卦数是:乾一、兑二、离三、震四、巽五、坎六、艮七、坤八。它的中间数为 0,以代表五或十。0 象征着宇宙的元气。它的序数对宫相加之和为九数。先天八卦演变过程中,首先是太极,其次是两仪,接着是四象,最后是八卦,它们是宇宙形成的过程。\n后天八卦,相传来自于洛书。它是离坎定南北,震兑定东西。故后天八卦数是:坎一、坤二、震三、巽四、中五、乾六、兑七、艮八、离九。它的中间数为五,与对宫纵横相加之和为十五数。\n太极就是一,是道,是天地未分时物质性的浑沌元气。太极动而生阳,静而生阴,是生两仪,一阴一阳就是两仪,故《易·系辞说》说:“一阴一阳之谓道”,古人观天下万物之变化,不外乎由太极而生阴阳,故画一奇以象阳,画一偶以象阴。阳就是阳爻,用\u0026quot;—\u0026ldquo;表示,单为阳之数;阴就是阴爻,用\u0026rdquo;\u0026ndash;\u0026ldquo;表示,双为阴之数。这就是构成八卦的基本符号,是(阴阳)矛盾的形态和万物演变过程中的最基本的阴阳二气的基本符号。\n一阴一阳这个两仪又各生一阴一阳之象,也就是一分为二,生出四象,四象即少阳、老阳、少阴、老阴,是谓\u0026quot;两仪生四象\u0026rdquo;。四象再各自生阴生阳(一分为二),生出八卦。即四象生八卦,也就是说在少阳、老阳、少阴、老阴这四象上,分别各加一阳爻或阴爻,“叠之为三”,即产生八种新的符号,如在少阴上加一阳爻,生成叫做离卦;在其上加一阴爻,生成叫做震卦,依次类推,生成乾一、兑二、离三、震四、巽五、坎六、艮七、坤八,这种八卦排列次序及其卦数,就是先天八卦之数,由左至右,称做先天八卦横图。先天数的产生,是由浑沌太极,无形无象也无定位,只是一气相生,阴阳次第相加,而自然造化一至八数,故谓“先天”。\n应用: 原则上很简单:先天八卦取数,后天八卦取向。 先天八卦数:乾一、兑二、离三、震四、巽五、坎六、艮七、坤八。后天八卦方向:震卦,正东;巽卦,东南;离卦,正南;坤卦,西南;兑卦,正西;乾卦,西北;坎卦,正北;艮卦,东北。实际应用时最多就是后天八卦,先天卦用得很少。\n卦序是:一乾、二兑、三离、四震、五巽、六坎、七艮、八坤。《周易·说卦传》说:“天地定位,山泽通气,雷风相薄,水火不相射。八卦相错,数往者顺,知来者逆,是故易逆数也”。“薄”字通假于“搏”。这是先天八卦方位的理论依据之一,是讲八卦自身匹配对待之体的,依据之二是《周易·系辞传》说:\u0026ldquo;易有太极,是生两仪,两仪生四象,四象生八卦。\u0026rdquo;\n按其所代表的东西的性质两两相对,分成四时,每对都是二个性质相反的东西,相对立的站在各一端,即阴阳相对,这四对东西交错起来,就构成了先天八卦方位图。我们从图中分析出阴阳相对的关系。\n天地定位:乾南坤北,天居上,地居下,南北对峙,上下相对。从两卦爻象来看,乾是三阳爻组成,为纯阳之卦;坤是三阴爻组成,为纯阴之卦,两卦完全相反。\n山泽通气:艮为山居西北,兑为泽居东南,泽气于山,为山为雨;山气通于泽,降雨为水为泉。从两卦爻象来看,艮是一阳爻在上,二阴爻在下;兑是一阴爻在上,二阳爻在下,两卦成对待之体。\n雷风相搏:震为雷居东北,巽为风居西南,相搏者,其势相迫,雷迅风益烈,风激而雷益迅。从两卦爻象来看,震是二阴爻在上,一阳爻在下;巽是二阳爻在上,一阴爻在下,八卦成反对之象。\n水火不相射:离为日居东,坎为月居西,不相射者,离为火,坎为水,得火以济其寒,火则得水以其热,不相熄灭。从八卦爻象来看,离是上下为阳爻,中间为阴爻;坎是上下为阴爻,中间为阳爻,两卦亦成对待之体。\n从八卦卦爻明显看出,乾坤两卦为纯阳纯阴卦外,震、坎、艮卦都是由一阳爻两阴爻组成,而且爻画均为五,为奇数,为阳数,故此三卦为阳卦。巽、离、兑三卦都是出一阴爻两阳爻组成,而且爻画均为四,为偶数,为阴数,故此三卦为阴卦。\n先天八卦方位与先天卦数的排列形式,由乾一至震四,系由上而下,再由下而上旋至巽五,由巽五至坤八又由上而下,其路线形成 s 形的曲线,这种运动方式称为\u0026quot;逆行\u0026quot;,从 s 的迹形运动中,由乾至坤是按先天卦数乾一、兑二、离三、震四、巽五、坎六、艮七、坤八排列的,这种从上而下,先左后右,由少至多的数字排列方式,称作“逆数”,反之,由坤至乾,从下面的开始,由下而上,先右后左,由多至少的数字形成倒行的方式,称作\u0026quot;顺数\u0026quot;。\n按先天八卦乾坤、艮兑、震巽、坎离两两对待之本,每一对中都含有顺逆、奇偶、阴阳,即阴中含阳,阳中含阴,阴阳错综交变,这就是先天八卦方位图中的矛盾对立统一的辩证思想,是八卦本着阴阳消长,顺逆交错,相反相成的宇宙生成自然之理,来预测推断世间一切事物,数不离理,理不离数。\n后天八卦7 后天八卦图也叫后天八卦,因为它是图文并茂的一个古代术语。 后天八卦讲流行, 周期循环,如水流行 ,用以表示阴阳的依存与互根,五行的母子相生。后天图是从四时的推移,万物的生长收藏得出的规律。 后天八卦图从《周易.说卦传》中可以看出,万物的春生,夏长,秋收,冬藏,每周天 360 日有奇,八卦用事各主 45 日,其转换点就表现在四正四隅的八节上,这就构成了按顺时针方向运转的后天八卦图。每卦有三爻,三而八之,即指一年二十四个节气,于此可见这些卦图的实质了。\n八卦分为先天与后天。后天八卦图,也有人将其与周文王联系起来,称为文王八卦图。后天八卦即震卦为起始点,位列正东。按顺时针方向,依次为巽卦,东南;离卦,正南;坤卦,西南;兑卦,正西;乾卦,西北;坎卦,正北;艮卦,东北;震卦,正东。如象征节气,则震为春分,巽为立夏,离为夏至,坤为立秋,兑为秋分,乾为立冬,坎为冬至,艮为立春。 即是序数为:坎一、坤二、震三、巽四、五为中宫,乾六、兑七、艮八、离九。\n后天八卦是由先天八卦演变出来。后天八卦图与先天八卦图不同,后天事象的八卦,以乾为父,坤为母,震为长男,巽为长女,坎为中男,离为中女,艮为少男,兑为少女。\n《说卦传》曰:“帝出乎震,齐乎巽,相见乎离,致役乎坤,说言乎兑,战乎乾,劳乎坎,成言乎艮”。邵子曰:“乾统三男于东北,坤统三女于西南,乾、坎、艮、震为阳,巽、离、坤、兑为阴”。\n至于八卦之五行顺序,前人已经言及,宋项安世曰:“后天之序,据太极既分之后,播五行于四时也。震巽二木主春,故震在东方,巽东南次之;离火主夏,故为南方之卦;兑乾二金主秋,故兑为正秋,乾西北次之;坎水主冬,故为北方之卦;土王四季,故坤土在夏秋之交,为西南方之卦。艮土在冬春之交,为东北方之卦。”\n坤为湿土,其性属阴,故介于金火之间,既可晦火又能生金;艮为燥土,其性属阳,故介于水木之间,即可制水又能养木。此正所以合天地自然之理而成造化。\n帝出乎震9,齐乎巽,相见乎离,致役乎坤,说言乎兑,战乎乾,劳乎坎,成言乎艮。万物出乎震,震,东方也。齐乎巽,巽,东南也,齐也者,言万物之洁齐也。离也者,明也,万物皆相见,南方之卦也,圣人南面而听天下,向明而治,盖取诸此也。坤也者,地也,万物皆致养焉,故曰致役乎坤。兑正秋也,万物之所说也,故曰说言乎兑。战乎乾,乾,西北之卦也,言阴阳相薄也。坎者水也,正北方之卦也,劳卦也,万物之所归也,故曰劳乎坎。艮,东北之卦也,万物之所成终而所成始也,故曰成言乎艮。\n如上图所示,阐述的其实是八卦所对应的八方和八节。有何详解?\n万物始生之卦\n《说卦》的原文后面进一下解释说:“万物出乎震,震,东方也。”\n一边说“帝出乎震”,一边又说“万物出乎震”,可见这个“帝”与“万物”有关。巧的是,《周易》中也有一个与万物有关的卦,就是“三生万物”的第三卦: 屯卦。\n“有天地然后万物生焉。盈天地之间者唯万物,故受之以屯;屯者盈也,屯者物之始生也。” \u0026ndash; 《序卦》\n屯的下卦,正是震卦,也就是“帝出乎震”的震卦。\n古人之思妙,若不细揣之,岂不若持金行乞否?\r八卦的最基本的单位是爻, 多是记述日影变化的专门符号。爻有阴阳两类,阳爻表示阳光,阴爻表示月光。每卦又有三爻, 代表天地人三才。三才的天部,包括整个天体运行和气象变化,这些星象之学,古称天文。地部指观测日影来计算年周期的方法,用地之理了解生长化收藏的全过程。人部指把天文、地理和人事结合,以便按照这些规律进行生产和生活。每卦的次序是自下而上的,最下一横叫初爻,中一横叫二爻,上一横叫三爻。\n八卦代表八种基本物象:乾为天,坤为地,震为雷,巽为风,艮为山,兑为泽,坎为水,离为火,总称为经卦,由八个经卦中的两个为一组的排列组合,则构成六十四卦。\n也有学者说是:测日影不可能有阴阳互包的卦,出不了八卦,八卦之前是六卦体系,八卦是六卦的产物,六象筮法论考证远古数千年的易卦史是六卦生八卦。\n后天八讲流行,形容周期循环,如水流行,用以表示阴阳的依存与互根,五行的母子相生。后天图是从四时的推移,万物的生长收藏得出的规律。\n从《周易。说卦传》中可以看出,万物的春生,夏长,秋收,冬藏,每周天 360 日有奇,八卦用事各主 45 日,其转换点就表现在四正四隅的八节上,这就构成了按顺时针方向运转的后天八卦图。每卦有三爻,三而八之,即指一年二十四个节气,于此可见这些卦图的实质了。\n以离南坎北,震东兑西,后天八卦是讲流行。“对待者数”指先天八卦而言。“流行者气”指后天八卦而言。 后天八卦问世后,人们进行了预测,按后天的卦数预测则准确。然而,用先天的卦数则不准确。对此有人认为,先天八卦为体,后天八卦为用。这样一来就出现了先后天八卦两者缺一不可的局面。从坎离两卦的定位来看,后天八卦没有脱离开先天的老框框。理由是,先天把天阳(火)在上,地阴(水)在下,这里的火与水指乾纯阳,和坤纯阴而言。后天离火在上,坎水在下,和先天乾上坤下并没有实质上的区别。也表明后天八卦火在上,水在下。自然中是阳升阴降,所谓“阳升”,只有在“下”才有“升”字可言,若把阳(火)在最上,也就没有阳向上升的空间了,只是死火。\n先看看先天八卦,“天地者万物之上下也”,即天在上,地在下。“天动地静”即动为阳静为阴。卦爻以“-”为阳爻,“\u0026ndash;”为阴爻,乾是三个阳爻所组成,坤是三个阴爻所组成,所以先天八卦把乾为天在上,坤为地在下。先天八卦即天地对待(天和地对、男和女对)。以“气终而象变”的说法而言,即事物走到终点(极端)则变向反面,所以夏至一阴生,冬至一阳生又显得格外有哲理性。\n六十四卦8 #+caption: 分宫卦象次序歌\n乾为天 天风姤 天山遁 天地否 风地观 山地剥 火地晋 火天大有 坎为水 水泽节 水雷屯 水火既济 泽火革 雷火丰 地火明夷 地水师 艮为山 山火贲 山天大畜 山泽损 火泽睽 天泽履 风泽中孚 风山渐 震为雷 雷地豫 雷水解 雷风恒 地风升 水风井 泽风大过 泽雷随 巽为风 风天小畜 风火家人 风雷益 天雷无妄 火雷噬嗑 山雷颐 山风蛊 离为火 火山旅 火风鼎 火水未济 山水蒙 风水涣 天水讼 天火同人 坤为地 地雷复 地泽临 地天泰 雷天大壮 泽天夬 水天需 水地比 兑为泽 泽水困 泽地萃 泽山咸 水山蹇 地山谦 雷山小过 雷泽归妹 上下经卦名次序歌:\n乾坤屯蒙需讼师, 比小畜兮履泰否,\r同人大有谦豫随, 蛊临观兮噬嗑贲,\r剥复无妄大畜颐, 大过坎离三十备。\r咸恒遁兮及大壮, 晋与明夷家人睽,\r蹇解损益夬姤萃, 升困井革鼎震继,\r艮渐归妹丰旅巽, 兑涣节兮中孚至,\r小过既济兼未济, 是为下经三十四。 卦序规律\n《易经》64 卦的卦序是建立在 8 卦的基础之上的,前人的主要认识有“先天八卦说”,“后天八卦说”,各有特点,但它们与 64 卦的联系却显得附会。后来,有人对 64 卦提出了“两两相偶,非反即覆”的概念,可谓是一个大的进步,它指出了 64 卦的一些内在联系。但这一概念也有缺陷,就在于它的提法“只见树木,不见森林”,没能系统地解决《易经》的卦序问题。\n易松云先生站在前人的基础上,对 64 卦的卦序提出了全新的认识,下面的这张图表即完全反映了《易经》64 卦的内在规律性:\n易经六十四卦是透过六十四卦的组合,去反应六十四种不同的事务、情境、现象、特定环境下的人生哲理、以及大自然的运作法则。每一卦都会有六爻和爻辞,以及三百八十四种对应的状态,透过这些变化可以知道世间万物的运作以及人生的哲理。\n","date":"2021-11-05","permalink":"https://loveminimal.github.io/posts/dao/","summary":"\u003cblockquote\u003e\n\u003cp\u003e“道可道,非常道。名可名,非常名。”\u003c/p\u003e\n\u003c/blockquote\u003e","title":"道述"},{"content":" 认识世界,改造世界。\n绪论 马克思主义是关于工人阶级和人类解放的科学 p1 → p41\n马克思主义是时代的产物,它产生于资本主义社会化大生产已经成为主导趋势,资本主义社会内部各种社会矛盾充分暴露,无产阶级以独立的政治力量登上历史舞台争取自身和人类解放的历史时代。\n= 时代?!大势(时势),主次矛盾(社会),核心变革力量(个人、组织)。\n马克思主义是对人类文明成果的继承与创新,德国古典哲学、英国古典经济学、19 世纪英法两国的空想社会主义,是它的直接理论来源,马克思主义的产生还与当时自然科学的巨大成就密切相关。\n马克思主义既然是时代的产物和实践经验的总结,它就必然随着时代的发展、实践的拓展、科学的进步而不断丰富和发展自身。\n= 穷,变,通,达。 居安思危,未雨绸缪。*\n马克思主义是由马克思、恩格斯创立的。为他们的后继者所发展的,以反对资本主义、建设社会主义和共产主义为目标的科学的理论体系。简要地说,它是关于工人阶级和人类解放的科学。\n以实践为基础的科学性和革命性的统一,是马克思主义的根本理论特征。其革命性表现为彻底的批判精神和鲜明的政治立场;其科学性主要表现为它按照世界的本来面具认识世界,揭示了自然界和人类社会的发展规律。它的科学性和革命性在实践的基础上达到统一。\n= 实践是什么?马克思主义又是如何在实践的基础上实现了科学性(实事求是、揭示规律)和革命性(彻底批判、立场鲜明)的统一?\n= 实践,是主观见之于客观,是人们能动地改造和探索现实世界一切客观物质的社会性活动。\n与时俱进是马克思主义的理论品质。与时俱进就是党的全部理论和工作要体现时代性,把握规律性,富于创造性。马克思主义的创始人和继承者都是与时俱进的典范。\n= 时代,规律,创造(抓住时代脉搏,认识规律、利用规律)。\n理想是人生的奋斗目标。马克思主义最高的社会理想是推翻资本主义,实现共产主义。最高理想和共同理想既相互区别又相互联系,二者是辩证统一的关系。\n马克思主义不是教条,而是行动指南。它提供研究的方法,而不提供对一切问题的现成答案。必须把马克思主义的一般原理与本国的具体实际相结合。\n= 实事求是!具体问题,具体分析!\n学习马克思主义的目的在于树立正确的世界观、人生观、价值观,掌握认识世界和改造世界的伟大工具,全面提高人的素质。理论联系实际是学习马克思主义的根本方法。\n= 认识世界,改造世界。\n现今时代发生了许多引人注目的变化,向马克思主义提出了许多新的研究课题,并提供了不少回答这些课题的实践经验,为在实践中发展马克思主义提供了契机。\n第一章 物质世界及其发展规律 p43 → p120\n哲学是系统化、理论化的世界观,思维与存在的关系问题是哲学的基本问题,物质是世界的本质或本原,世界统一于物质。物质是不依赖于意识又能为意识所反映的客观实在。运动是物质的根本属性,时间和空间是物质运动的存在方式。\n把实践的观点作为根本观点是马克思主义哲学同旧唯物主义和唯心主义的根本区别。实践是主体能动地改造和探索客体的客观物质活动,它具有客观性、自觉能动性、社会历史性等基本特点,物质生产实践、处理社会关系的实践和科学实验是实践的三种基本形式。人的实践活动使自然分化为自在自然和人化自然,社会历史是人们的实践活动创造的,实践是人的存在方式,社会生活在本质上是实践的。\n= 实践,讲求的是放弃空想,实事求是,不断地探索、认识、更新!\n物质世界是普遍联系和永恒发展的,联系的观点和发展的观点是唯物辩证法的总特征。唯物辩证法和形而上学是两种根本对立的发展观。对立统一规律、质量互变规律、否定之否定规律是联系和发展的基本规律,唯物辩证法的范畴是提示事物联系和发展的基本环节,对立统一规律是唯物辩证法的实质与核心。\n= 💡 五大基本范畴,是如何定义的呢?基本环节?透过现象看本质,思其因果,了‘然’(必然?偶然?),有无可能性、现实否?以何内容、表现形式?\n物质的运动发展是有规律的。规律是物质运动发展过程中本质的、必然的、稳定的联系。规律具有客观性和普遍性两个特点。社会规律与自然规律有本质区别。社会规律同自然规律一样,也是客观的。意识是自然界长期发展的产物,意识依赖于物质。从本质上看,意识是人脑的机能,是客观世界的主观映像,是社会的产物。意识对物质的具有能动的反作用。尊重客观规律和发挥主观能动性是辩证的统一。\n= 世界是物质的,物质是运动的(联系和发展),运动是有规律的(对立统一、质量互变、否定之否定)。\n第二章 认识的本质及其规律 p121 → p176\n认识是指人们特有的一种能力和活动,认识的产生是一个长期的历史过程。个人思想的发展史再现着整个人类思维的发展过程。\n实践的观点是马克思主义认识论之第一的和基本的观点。马克思把实践观点引入认识论,引起了认识论的革命。实践是认识的基础,认识是主体对客体的反映,马克思主义认识论是革命的能动反映论。\n= 认识是主体对客体的反映,所以要正确认识,实践、认识,再实践、再认识……\n感性认识是认识的初级阶段,包括感觉、知觉、表象三种形式。理性认识是认识的高级阶段,包括概念、判断、推理三种形式。认识的辩证过程包括从感性认识到理性认识和从理性认识到实践两个阶段。一个正确的认识需要经过“实践-认识-实践”多次反复才能完成,整个人类的认识是有限和无限的辩证统一。\n= 感性(感觉、知觉、表象) → 理性(概念、判断、推理)\n真理是主体对客观的正确反映,谬误是主体对客体的歪曲反映。真理和谬误是认识运动中既对立又统一的两个方面。真理是客观的,既有绝对性又有相对性,是绝对性和相对性的统一。人的认识通过相对真理而走向绝对真理。实践是检验真理的唯一标准。真理与价值是统一的。\n认识世界与改造世界是紧密联系在一起的。马克思主义认识论是党的思想路线的理论基础。党的群众路线是马克思主义认识论在实际工作中的运用。\n第三章 人类社会及其发展 p177 → p245\n社会存在和社会意识的关系问题是历史观的基本问题,社会存在决定社会意识,社会意识对社会存在具有相对独立和能动的反作用,生产方式是社会存在和发展的基础。\n生产方式是人类借以向自然界谋取必需的生活资料的方式;生产力是人类利用自然、改造自然、从自然界获取物质资料的能力(反映的是人与自然界的关系);生产关系是指人们在物质生产过程中结成的经济关系(生产资料的所有制形式 - 起决定性的作用、人在生产中的地位及相互关系、产品的分配方式)。\n= 只是人与自然的关系吗?一直在攫取自然界?\n科学技术是第一生产力,生产资料的所有制形式是整个生产关系的基础。生产关系必须适合生产力性质的规律包括生产力决定生产关系、生产关系反作用于生产力、生产力和生产关系之间的矛盾运动三项内容。\n= 呜呼,生产资料?!?!\n经济基础是指一个社会中占统治地位的生产关系各个方面的总和,上层建筑分为政治上层建筑和观念上层建筑两部分。上层建筑必须适合经济基础发展要求的规律包括经济基础决定上层建筑、上层建筑反作用于经济基础、经济基础和上层建筑之间的矛盾运动三项内容。\n社会形态最基本的划分法有两种,即经济社会形态划分法和技术社会形态划分法。社会形态的发展是一种自然历史过程。社会形态的发展既具有统一性,又具有多样性。\n生产力和生产关系之间的矛盾、经济基础和上层建筑之间的矛盾是人类社会的基本矛盾,这两对矛盾互相制约,互相影响。社会基本矛盾是社会发展的基本动力。\n物质利益的对立是阶级斗争的根源,阶级斗争是阶级社会发展的直接动力。阶级斗争推动社会发展的作用,既表现在社会形态更替的过程中,也表现在同一社会形态的量变过程中。\n= 社会的所有问题,归根结底在于资源分配(物质利益)问题!\n社会革命是社会制度的根本质变,社会改革是同一社会制度量变过程中的部分质变。革命的根本问题是国家政权问题,社会主义社会的改革是社会主义制度的自我完善。\n从本质上说,文化是人和社会的具体存在方式,文化具有创造性、自由性、兼容性等根本特性,文化在社会发展中具有多方面的功能。要大力发展社会主义先进文化。\n= 文化是什么?它是如何产生的?解决了什么问题?文化是相对于经济、政治而言的人类全部精神活动及其产物,教育、科学、艺术皆属广义的文化。\n马克思从三个方面对人的本质作了界定,即劳动是人的本质,人的本质是一切社会关系的总和,人的需要即人的本质。人与社会是具体的历史的统一。\n人有自然属性(人的肉体及其特性)和社会属性(在社会实践活动中人与人结成的各种社会关系),人的本质是由人的社会属性决定的 \u0026ndash; 人的本质是一切社会关系(劳动中结成的)的总和。\n历史唯物主义和历史唯心主义在谁是历史的创造者的问题上的观点是根本对立的。人民群众是物质财富和精神财富的创造者,是实现社会变革的决定力量。坚持无产阶级政党的群众观点和群众路线的工作方法。\n个人分为普通个人和历史人物,历史人物又区分为正面人物和反面人物。正面人物即杰出人物,杰出人物在历史发展中起着重要作用,要用正确的观点和方法分析和评价杰出人物的历史作用。\n第四章 资本主义的形成及其本质 p247 → p309\n资本主义经济是在商品经济基础上产生和发展起来的,商品经济成为资本主义社会中占统治地位的和最普遍的经济形式。建立在分析商品货币关系基础上的科学劳动价值论,深刻阐明了劳动二重性、商品价值构成与创造、货币本质与职能、商品经济基本矛盾,以及价值规律等一系列重要经济理论问题。\n资本主义生产关系最初是在封建社会末期的小商品生产者两极分化基础上产生的,商业的发展促进了资本主义生产关系的产生,而资本原始积累则大大加速了资本主义生产关系的形成。通过资产阶级革命初步确立了资本主义制度,而资本主义制度的完全确立是通过产业革命实现的。\n\u0026gt; 生产关系 = 生产资料的所有制形式 + 人在生产中的地位及相互关系 + 产品的分配方式\n资本主义经济制度是以生产资料资本主义私有制为基础,通过雇佣劳动制度剥削工人创造的剩余价值的经济制度。资本主义生产过程的本质是价值增殖过程,生产剩余价值是资本主义生产方式的基本规律。\n资本积累的实质是资本家利用无偿占有的剩余价值进行资本积累,从而占有更多的剩余价值。随着资本积累的进行,必然造成大量失业人口和严重的贫富两极分化。资本积累的历史趋势是社会主义公有制必然取代资本主义私有制。\n= 剥削?分配!分配!分配!\n资本主义的政治制度是资产阶级为实现其阶级专政而采取的统治方式和方法及各种相关制度的总和,它主要包括资本主义的国家制度、政党制度、选举制度、三权分立制度、民主制度等,其中国家制度是资本主义政治制度的核心。\n资本主义的意识形态是资产阶级对世界和社会的系统看法和见解,具有鲜明的阶级性。利己主义是资本主义意识形态的核心。利己主义体现在资产阶级的人生观、价值观、道德观等各个方面。\n第五章 资本的流通过程和剩余价值的分配 p311 → p365\n单个资本的运动包括资本的循环和周转。资本只有连续不断地循环运动,才能使资本家源源不断地获得剩余价值,而资本周转速度的快慢则影响着剩余价值生产的数量和年剩余价值率的高低…… 社会资本的运动表现为社会资本再生产。社会资本再生产的顺利实现,必须按照社会资本再生产实现条件的客观要求,保持两大类之间的一定比例关系,使社会总产品的各个构成部分都能在实物形态上得到替换,在价值形态上得到补偿。资本主义基本矛盾导致社会资本再生产的实现条件遭到破坏,周期性地爆发经济危机。在剩余价值各种具体形式中,产业利润是其他各种剩余价值具体形式(如商业利润、利息、银行利润、股息、地租等)的基础。随着资本主义的发展,产业资本的剩余价值转化为利润,剩余价值率利润率,利润转化为平均利润,商品的价值转化为生产价格。商业资本是在资本主义流通领域中发生作用的职能资本,它所执行的就是产业资本中的商品资本的职能,即销售商品,实现价值和剩余价值。商业利润相当于平均利润,它来源于雇佣工人所创造的剩余价值的一部分…… 借贷资本是从职能资本运动中的货币资本独立出来,并服务于职能资本的资本形式,它是为取得利息而暂时贷给职能资本家使用的货币资本。利息和银行利润来源于产业工人所创造的剩余价值的一部分。股份公司是现代企业的一种重要经营组织形式。\n= emm\u0026hellip; 资本的流通和剩余价值的分配……\n资本主义地租是农业资本家为取得土地的使用权,而付给土地所有者的超出平均利润的超额利润。农业资本家获得平均利润。资本主义地租有级差地租和绝对地租两种基本形式。\n第六章 资本主义发展的历史进程 p366 → p411\n资本主义的发展可分为自由竞争资本主义和垄断资本主义两个阶段。在自由竞争中生产和资本不断集中,当生产集中和资本集中发展到一定程度时就会走向垄断。垄断是为了获取高额垄断利润,它是通过各种垄断组织和垄断价格取得的。垄断并没有消除竞争。垄断是帝国主义的经济实质。\n垄断资本主义的经济特征是:垄断在经济生活中占统治地位;金融资本和金融寡头的统治;资本输出在经济生活中占重要地位;国际垄断同盟在经济上瓜分世界;垄断资本主义列强瓜分和重新瓜分世界。\n私人垄断资本主义进一步发展为国家垄断资本主义,即垄断资本与国家政权相结合的资本主义。它的基本形式有资本主义的国有经济、国家与私人资本在企业内外的结合等。国家垄断资本主义实行对国民经济的干预和调控,一定程度上调整了经济运行、缓和了资本主义社会中的各种矛盾。它是资本主义经济关系的局部调整和部分质变。垄断资本主义的发展,促进了生产社会化、国际化程序的提高,加强了各国间的经济联系,形成了经济全球化的发展趋势,其主要内容是生产全球化、贸易全球化和资本全球化。在生产国际化、经济全球化的条件下,当代资本主义生产力、生产关系和上层建筑各方面,较之自由竞争和私人垄断资本主义时期,发生了很多变化。\n在资本主义发展进程中,生产社会化程序不断加强,这与资本主义私人占有制形成了资本主义的基本矛盾,它决定了资本主义终将为社会主义所取代。\n第七章 社会主义社会及其发展 p412 → p461\n社会主义经历了从空想到科学、从理论到实践的发展。科学社会主义理论是马克思主义的重要组成部分,是无产阶级、社会主义和共产主义事业的指导思想和理论武器。\n社会主义理论与实践是通过社会主义革命实现的。对于社会主义革命的发生,马克思主义者有一个认识不断深化的过程,马克思恩格斯曾提出同时胜利论,列宁则根据垄断资本主义时期的新形势提出了一国或数国首先胜利论,并成功地领导了俄国十月革命。列宁和斯大林在向社会主义过渡和建设社会主义等方面进行了探索。社会主义从一国到多国发展壮大,第二次世界大战后出现了一批社会主义国家,他们在社会主义建设中进行了多方面的探索。\n无产阶级专政和社会主义民主是科学社会主义的核心内容。实行无产阶级专政是最终目标是消灭剥削、消灭阶级,进入无阶级社会。建设高度的社会主义是我们重要的奋斗目标和任务。\n社会主义在实践中发展完善。人们对社会主义的基本特征的认识不断深化。社会主义的本质是解放生产力,发展生产力,消灭剥削,消除两极分化,最终达到共同富裕。\n社会主义在一些经济文化落后的国家取得胜利有其历史必然性,这些国家建设社会主义的任务更重更艰巨。社会主义的发展道路是多样的,要在实践中不断探索。\n马克思主义政党是社会主义事业的领导核心,是工人阶级的先锋队,是新型的革命政党,是为实现共产主义而奋斗的政党,是为人民谋利益的政党。\n第八章 共产主义社会是人类最崇高的社会理想 p462 → p492\n共产主义社会是人类最理想最美好的社会制度。马克思主义依据社会发展规律、分析了资本主义社会内存矛盾的运动,阐明了由资本主义转向社会主义的必然性,并对未来共产主义社会进行了展望,科学预见了共产主义社会的基本特征。\n共产主义社会形态包括社会主义社会和共产主义社会前后相互衔接的两个阶段,人类社会发展的历史趋势,必然由资本主义社会过渡到社会主义社会,将来再由社会主义社会发展为共产主义社会。实现共产主义是人类最伟大的事业,需经过一个不断实践的长期过程。\n社会主义是走向共产主义的必由之路,只有经过社会主义历史阶段长期和充分发展,为实现共产主义创造出各种主客观条件,才能最终过渡到共产主义社会。\n共产主义远大理想是人们从事各种社会实践活动的精神支柱和思想动力。共产主义远大理想与中国特色社会主义共同理想是相互联系和相互促进的。要积极投身于中国特色社会主义事业,在建设中国特色社会主义事业中为实现共产主义而奋斗。\n","date":"2021-08-16","permalink":"https://loveminimal.github.io/posts/marxism-lite/","summary":"\u003cblockquote\u003e\n\u003cp\u003e认识世界,改造世界。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"马克思主义简览"},{"content":"i.e. 自学计算机科学\n🔔 转载 英文原文、中文译本\n无意中看到这篇文章,感觉挺不错的,转载一下,方便摘录!\n简介 本文档是对 teachyourselfcs 内容的中文翻译,原作者为 ozan onay 和 myles byrne 。\n如果你是一个自学成才的工程师,或者从编程培训班毕业,那么你很有必要学习计算机科学。幸运的是,不必为此花上数年光阴和不菲的费用去攻读一个学位:仅仅依靠自己,你就可以获得世界一流水平的教育 💸。\n互联网上,到处都有很多的学习资源,然而精华与糟粕并存。你所需要的,不是一个诸如“200+免费在线课程”的清单,而是以下问题的答案:\n你应该学习 哪些科目 ,为什么? 对于这些科目, 最好的书籍或者视频课程 是什么? 解决了“学什么?从哪儿学?”的问题!\r在这份指引中,我们尝试对这些问题做出确定的回答。\n书籍视频 大致按照列出的顺序,借助我们所建议的教材或者视频课程(但是最好二者兼用),学习如下的九门科目。目标是先花 100 到 200 个小时学习完每一个科目,然后在你职业生涯中,不时温习其中的精髓 🚀。\n100 - 200 小时,hmm…… 可以入门吧,比起 10000 小时,这不算什么。\r科目 为何要学? 最佳书籍 最佳视频 编程 不要做一个“永远没彻底搞懂”诸如递归等概念的程序员 《计算机程序的构造和解释》 brian harvey’s berkeley cs 61a 计算机系统结构 如果你对于计算机如何工作没有具体的概念,那么你所做出的所有高级抽象都是空中楼阁 《深入理解计算机系统》 berkeley cs 61c 算法与数据结构 如果你不懂得如何使用栈、队列、树、图等常见数据结构,遇到有难度的问题时,你将束手无策 《算法设计手册》 steven skiena’s lectures 数学知识 计算机科学基本上应用数学的一个“跑偏的”分支,因此学习数学将会给你带来竞争优势 《计算机科学中的数学》 tom leighton’s mit 6.042j 操作系统 你所写的代码,基本上都由操作系统来运行,因此你应当了解其工作原理 《操作系统导论》 berkeley cs 162 计算机网络 互联网已然势不可挡:理解工作原理才能解锁全部潜力 《计算机网络:自顶向下方法》 stanford cs 144 数据库 对于多数重要程序,数据是其核心,然而很少人理解数据库系统的工作原理 《readings in database systems》 (暂无中文译本) joe hellerstein’s berkeley cs 186 编程语言与编译器 若你懂得编程语言和编译器如何工作,你就能写出更好的代码,更轻松地学习新的编程语言 《crafting interpreters》 alex aiken’s course on lagunita 分布式系统 如今,多数系统都是分布式的 《数据密集型应用系统设计》 mit 6.824 还是太多?\n如果花几年时间自学 9 门科目让人望而却步,我们建议你只专注于两本书:《深入理解计算机系统》和《数据密集型应用系统设计》。根据我们的经验,投入到这两本书的时间可以获得极高的效率,特别适合从事网络应用开发的自学工程师。这两本书也可以作为上面表格中其他科目的纲领。\n为什么要学习计算机科学 软件工程师分为两种:一种充分理解了计算机科学,从而有能力应对充满挑战的创造性工作;另一种仅仅凭着对一些高级工具的熟悉而勉强应付。\n这两种人都自称软件工程师,都能在职业生涯早期挣到差不多的工资。然而,随着时间流逝,第一种工程师不断成长,所做的事情将会起来真有意义且更为高薪,不论是有价值的商业工作、突破性的开源项目、技术上的领导力或者高质量的个人贡献。第一种工程师总是寻求深入学习计算机科学的方法,或是通过传统的方法学习,或是在职业生涯中永无止息地学习。\n第二种工程师通常浮于表面,只学习某些特定的工具和技术,而不研究其底层的基本原理,仅仅在技术潮流的风向改变时学习新的技能。如今,涌入计算机行业的人数激增,然而计算机专业的毕业生数量基本上未曾改变。第二种工程师的供过于求正在开始减少他们的工作机会,使他们无法涉足行业内更加有意义的工作。\n对你而言,不论正在努力成为第一种工程师,还是想让自己的职业生涯更加安全,学习计算机科学是唯一可靠的途径。\n分科目指引 编程 大多数计算机专业本科教学以 程序设计“导论” 作为开始。这类课程的最佳版本不仅能满足初学者的需要,还适用于那些在初学编程阶段遗漏了某些有益的概念和程序设计模式的人。\n对于这部分内容,我们的标准推荐是这部经典著作:《计算机程序的构造和解释》。在网络上,这本书既可供 免费阅读(英文版) ,也作为 mit 的免费视频课程 。不过尽管这些视频课程很不错,我们对于视频课程的推荐实际上是 brian harvey 开设的 sicp 课程 (即 berkeley 的 61a 课程)。比起 mit 的课程,它更加完善,更适用于初学者。\n中文翻译新增:\n关于 sicp 国内视频观看地址 mit 的免费视频课程(中英字幕) brian harvey 开设的 sicp 课程(中英字幕) scheme 学习的相关资源参见: https://github.com/deathking/learning-sicp 更多 mit 经典公开课(中英字幕):https://www.bilibili.com/video/av8515129/ 应该是伯克利 scheme 版本最后一次公开课(英文字幕):https://www.bilibili.com/video/av40460492/ 伯克利 2018 年春课程(英文字幕,有部分字幕缺失):https://www.bilibili.com/video/av20538548 伯克利 2019 年夏课程(中英字幕,中文机翻):https://www.bilibili.com/video/av82503560 自从 2016 年首次发布这份指南以来,最常被问的一个问题是,我们是否推荐 john denero 讲授的 cs 61a 课程,以及配套的书籍 《composing programs》 ,这本书“继承自 sicp 但使用 python 讲解”。我们认为 denero 的课程也很不错,有的学生可能更喜欢,但我们还是建议把 sicp、 scheme 和 brain harvey 的视频课程作为首选。\n为什么这么说呢?因为 sicp 是独一无二的,它可以 \u0026ndash; 至少很有可能 \u0026ndash; 改变你对计算机和编程的基本认识。不是每个人都有这样的经验,有的讨厌这本书,有的人看了前几页就放弃了,但潜在的回报让它值得一读。\n如果你觉得 sicp 过于难,试试 《composing programs》,如果还是不合适,那我们推荐《程序设计方法》(中文版,英文版);如果你觉得 sicp 过于简单,那我们推荐 《concepts, techniques, and models of computer programming》 ;如果读这些让你觉得没有收获,也许你应该先学习其他科目,一两年后再重新审视编程的理念。\n新版原文删除了对 《concepts, techniques, and models of computer programming》 一书的推荐,但这本书对各种编程模型有深入的见解,值得一读。所以译文中依然保留。 — 译者注\n最后,有一点要说明的是:本指南不适用于完全不懂编程的新手。我们假定你是一个没有计算机专业背景的程序员,希望填补一些知识空白。事实上, 我们把“编程”章节包括进来只是提醒你还有更多知识需要学习。对于那些从来没有学过编程,但又想学的人来说,这份 指南 更合适。\n计算机系统结构 计算机系统结构 \u0026ndash; 有时候又被称为“计算机系统”或者“计算机组成” \u0026ndash; 是了解软件底层的重要视角。根据我们的经验,这是自学的软件工程师最容易忽视的领域。\n我们最喜欢是入门书是 《深入理解计算机系统》 ,典型的 计算机体系结构导论课程 会涵盖本书的 1-6 章。\n我们喜爱《深入理解计算机系统》,因为它的实用性,并且站在程序员的视角。虽然计算机体系的内容比本书所涉及的内容多得多,但对于那些想了解计算机系统以求编写更快、更高效、更可靠的软件的人来说,这本书是很好的起点。\n对那些既想了解这个主题又想兼顾硬件和软件的知识的人来说,我们推荐 《计算机系统要素》,又名“从与非门到俄罗斯方块” (nand2tetris),这本书规模宏大,让读者对计算机内的所有部分如何协同工作有完全的认识。这本书的每一章节对应如何构建计算机整体系统中的一小部分,从用 hdl (硬件描述语言)写基本的逻辑门电路出发,途径 cpu 和汇编,最终抵达诸如俄罗斯方块这般规模的应用程序。\n我们推荐把此书的前六章读完,并完成对应的项目练习。这么做,你将更加深入地理解计算机体系结构和运行其上的软件之间的关系。\n这本书的前半部分(包括所有对应的项目)均可从 nand2tetris 的网站上 免费获得。同时,在 coursera 上,这是一门 视频课程 。\n为了追求简洁和紧凑,这本书牺牲了内容上的深度。尤其值得注意的是,流水线和存储层次结构是现代计算机体系结构中极其重要的两个概念,然而这本书对些几乎毫无涉及。\n当你掌握了 nand2tetris 的内容后,我们推荐要么回到《深入理解计算机系统》,或者考虑 patterson 和 hennessy 二人所著的 《计算机组成与设计》,一本优秀的经典著作。这本书中的不同章节重要程度不一,因此我们建议根据 berkeley 的 cs61c 课程 “计算机体系结构中的伟大思想”来着重阅读一些章节。这门课的笔记和实验在网络上可以免费获得,并且在 互联网档案 中有这门课程的过往资料。\n硬件是平台。 \u0026ndash; mike acton 在 cppcon 上的演说\n算法与数据结构 正如几十年来的共识,我们认为,计算机科学教育所赋予人们的最大能量在于对常见算法和数据结构的熟悉。此外,这也可以训练一个人对于各种问题的解决能力,有助于其他领域的学习。\n关于算法与数据结构,有成百上千的书可供使用,但是我们的最爱是 steven skiena 编写的 《算法设计手册》 。显而易见,他对此充满热爱,迫不及待地想要帮助其他人理解。在我们看来,这本书给人一种焕然一新的体验,完全不同于那些更加经常被推荐的书(比如 cormen,leiserson,rivest 和 stein,或者 sedgewick 的书,后两者充斥着过多的证明,不适合以解决问题为导向的学习)。\n如果你更喜欢视频课程, skiena 慷慨地提供了他的课程 。此外,tim roughgarden 的课程也很不错,在 stanford 的 mooc 平台 lagunita ,或者 coursera 上均可获得。skiena 和 roughgarden 的这两门课程没有优劣之分,选择何者取决于个人口味。\n至于练习,我们推荐学生在 leetcode 上解决问题,leetcode 上的问题往往有趣且带有良好的解法和讨论。此外,在竞争日益激烈的软件行业,这些问题可以帮助你评估自己应对技术面试中常见问题的能力。我们建议大约 100 道随机挑选的 leetcode 问题,作为学习的一部分。\n最后,我们强烈推荐 《怎样解题》 ,这本书极为优秀且独特,指导人们解决广义上的问题,因而一如其适用于数学,它适用于计算机科学。\n我可以广泛推荐的方法只有一个: 写之前先思考。 \u0026ndash; richard hamming\n数学知识 从某个角度说,计算机科学是应用数学的一个“发育过度”的分支。尽管许多软件工程师试图 \u0026ndash; 并且在不同程度上成功做到 \u0026ndash; 忽视这一点,我们鼓励你用学习来拥抱数学。如若成功,比起那些没有掌握数学的人,你将获得巨大的竞争优势。\n对于计算机科学,数学中最相关的领域是“离散数学”,其中的“离散”与“连续”相对立,大致上指的是应用数学中那些有趣的主题,而不是微积分之类的。由于定义比较含糊,试图掌握离散数学的全部内容是没有意义的。较为现实的学习目标是, 了解逻辑、排列组合、概率论、集合论、图论以及密码学相关的一些数论知识 。考虑到 线性代数 在计算机图形学和机器学习中的重要性,该领域同样值得学习。\n学习离散数学,我们建议从 lászló lovász 的课程笔记 开始。lovász 教授成功地让这些内容浅显易懂且符合直觉,因此,比起正式的教材,这更适合初学者。\n对于更加高阶的学习,我们推荐 《计算机科学中的数学》 ,mit 同名课程的课程笔记,篇幅与书籍相当(事实上,现已出版)。这门课程的视频同样可 免费获得 ,是我们所推荐的学习视频。\n对于线性代数,我们建议从 essence of linear algebra 系列视频开始,然后再学习 gilbert strang 的 《线性代数导论》 和 视频课程 。\n如果人们不相信数学是简单的,那么只能是因为他们没有意识到生活有多么复杂。 — john von neumann\n操作系统 《操作系统概念》 (“恐龙书”)和 《现代操作系统》 是操作系统领域的经典书籍,二者都因为写作风格和对学生不友好而招致了一些批评。\n《操作系统导论》 (operating systems: three easy pieces) 是一个不错的替代品,并且可在网上 [免费获得(英文版)](http://pages.cs.wisc.edu/~remzi/ostep/。我们格外喜欢这本书的结构,并且认为这本书的习题很值得一做。\n在读完《操作系统导论》后,我们鼓励你探索特定操作系统的设计。可以借助 “{os name} internals” 风格的书籍,比如 [lion\u0026rsquo;s commentary on unix](https://www.amazon.com/lions-commentary-unix-john/dp/1573980137/, [the design and implementation of the freebsd operating system](https://www.amazon.com/design-implementation-freebsd-operating-system/dp/0321968972/,以及 [mac os x internals](https://www.amazon.com/mac-os-internals-systems-approach/dp/0321278542/)。对于 linux ,我们推荐 robert love 的 《linux 内核设计与实现》。\n为了巩固对操作系统的理解,阅读小型系统内核的代码并且为其增加特性是一个很不错的方法。比如 xv6 ,由 mit 的一门课程所维护的从 unix v6 到 ansi c 和 x86 的移植,就是一个很棒的选择。《操作系统导论》有一个附录,记载了一些可能的 xv6 实验项目,其中关于潜在项目的很棒想法。\n计算机网络 鉴于有那么多关于网络服务端和客户端的软件工程,计算机网络是计算机科学中价值最为“立竿见影”的领域之一。我们的学生,系统性的学习了计算机网络,最终能够理解那些曾困扰他们多年的术语、概念和协议。\n在这一主题上,我们最爱的书籍是 《计算机网络:自顶向下方法》。书中的小项目和习题相当值得练习,尤其是其中的“wireshark labs”(这部分在 网上可以获得)。\n如果更喜欢视频课程,我们推荐 stanford 的 introduction to computer networking ,可在他们的 mooc 平台 lagunita 上免费观看。\n对于计算机网络的学习,做项目比完成小的习题更有益。一些可能的项目有: http 服务器,基于 udp 的聊天 app ,迷你 tcp 栈,代理,负载均衡器,或者分布式哈希表。\n你无法盯着水晶球预见未来,未来的互联网何去何从取决于社会。 \u0026ndash; bob kahn\n数据库 比起其他主题,自学数据库系统需要更多的付出。这是一个相对年轻的研究领域,并且出于很强的商业动机,研究者把想法藏在紧闭的门后。此外,许多原本有潜力写出优秀教材的作者反而选择了加入或创立公司。\n鉴于如上情况,我们鼓励自学者大体上抛弃教材,而是从 2015 年春季学期的 cs 186 课程 (joe hellerstein 在 berkeley 的数据库课程)开始,然后前往阅读论文。\n对于初学者,有一篇格外值得提及的论文:“architecture of a database system”。这篇论文提供了独特的对关系型数据库管理系统(rdbms)如何工作的高层次观点,是后续学习的实用梗概。\n《readings in database systems》,或者以 数据库“红书” 更为人知,是由 peter bailis,joe hellerstein 和 michael stonebraker 编纂的论文合集。对于那些想要在 cs 186 课程的水平更进一步的学习者,“红书”应当是下一步。\n如果你坚持一定要一本导论教材,那我们推荐 ramakrishnan 和 gehrke 所著的 《数据库管理系统:原理与设计》。如需更深一步,jim gray 的经典著作 《transaction processing: concepts and techniques》 值得一读,不过我们不建议把这本书当作首要资源。\n如果没有编写足够数量的代码,很难巩固数据库理论。cs 186 课程的学生给 spark 添加特性,倒是不错的项目,不过我们仅仅建议从零实现一个简单的关系型数据库管理系统。自然,它将不会有太多的特性,但是即便只实现典型的关系型数据库管理系统每个方面最基础的功能,也是相当有启发的。\n最后,数据模型往往是数据库中一个被忽视的、教学不充分的方面。关于这个主题,我们推荐的书籍是 data and reality: a timeless perspective on perceiving and managing information in our imprecise world。\n编程语言与编译器 多数程序员学习编程语言的知识,而多数计算机科学家学习编程语言相关的知识。这使得计算机科学家比起程序员拥有显著的优势,即便在编程领域!因为他们的知识可以推而广之:相较只学习过特定编程语言的人,他们可以更深入更快速地理解新的编程语言。\n我们推荐的入门书是 bob nystrom 所著的优秀的 crafting interpreters,可在网上免费获取。这本书条理清晰,富有趣味性,非常适合那些想要更好地理解语言和语言工具的人。我们建议你花时间读完整本书,并尝试任何一个感兴趣的“挑战”。\n另一本更为传统的推荐书籍是 《编译原理》,通常称为“龙书”。不幸的是,这本书不是为自学者而设计的,而是供教师从中挑选一些主题用于 1-2 学期的教学。\n如果你选择使用龙书进行自学,你需要从中甄选主题,而且最好是在导师的帮助下。我们建议依据某个视频课程来设定学习的结构,然后按需从龙书中获取深入的内容。我们推荐的在线课程是 alex aiken 在 mooc 平台 edx 所开设的。\n不要做一个只写样板代码的程序员。相反,给用户和其他程序员创造工具。从纺织工业和钢铁工业中学习历史教训:你想制造机器和工具,还是操作这些机器? — ras bodik 在他的编译器课程伊始\n分布式系统 随着计算机在数量上的增加,计算机同样开始 分散。尽管商业公司过去愿意购买越来越大的大型机,现在的典型情况是,甚至很小的应用程序都同时在多台机器上运行。思考这样做的利弊权衡,即是分布式系统的研究所在,也是越来越重要的一项技能。\n我们推荐的自学参考书是 martin kleppmann 的 《数据密集型应用系统设计》。与传统的教科书相比,它是一本为实践者设计的具有很高的可读性的书,并且保持了深度和严谨性。\n对于那些偏爱传统教材,或者希望可以从网上免费获取的人,我们推荐的教材是 maarten van steen 和 andrew tanenbaum 所著的 《分布式系统原理与范型》(中文第二版,英文第三版)。\n对于喜欢视频课程的人,mit 的 6.824 是一门很好的在线视频课程,由 robert morris 教授的研究生课程,在这里可以看到课程安排。\n不管选择怎样的教材或者其他辅助资料,学习分布式系统必然要求阅读论文。这里 有一个不错的论文清单,而且我们强烈建议你出席你当地的 papers we love (仅限美国)。\nfaq 常见问题解答 这份指引的目标受众是? 我们面向自学的软件工程师、培训班学生、“早熟的”高中生或者想要通过自学补充正式教育的大学生。关于何时开启这段自学旅程,完全取决于个人,不过多数人在有一定的职业经历后深入学习计算机科学理论会获益匪浅。比如,我们注意到,如果学生在工作中曾经使用过数据库,他们会 喜爱学习数据库系统课程;如果学生从事过一两个 web 项目,他们会喜爱学习计算机网络。\n人工智能/计算机图形学/xx 主题怎么样? 我们试图把计算机科学主题清单限制到那些我们认为 每一个软件工程师 都应该了解的内容,不限于专业或行业。拥有了这些基础,你将能更加轻松地挑选教材或论文,然而无需指引地学习核心概念。在这里,我们给出一些其他常见主题的自学起点:\n人工智能:通过观看视频并完成 pacman 项目来学习 berkeley 的 ai 课程。至于教材,使用 russell 和 norvig 编写的 《人工智能:一种现代方法》。 机器学习:学习吴恩达在 coursera 上的课程。耐心学习,先确保理解了基础概念再奔向类如深度学习的诱人新主题。 计算机图形学:学习 berkeley cs 184 课程 的材料,使用 《计算机图形学:原理及实践》 作为教材。 一定要严格遵守推荐的学习次序吗? 事实上,所有主题之间都有一定程度的重叠,彼此循环引用。以离散数学和算法的关系为例:先学习数学可以帮助你更深入地分析和理解算法,然而先学习算法可以为学习离散数学提供更大的动力和应用背景。理想情况下,你将在你的职业生涯多次重温二者。\n因此,我们所推荐的次序主要是为了帮助你起步……如果你出于某种强烈的原因而倾向以不同的顺序学习,那也没有关系,勇敢开始吧!不过在我们看来,最重要的“先决条件”是:先学计算机体系结构再学操作系统或数据库,先学计算机网络和操作系统再学分布式系统。\n和其他指引比起来,这份指引? aka. open source society、freecodecamp curricula\noss 指引 涵盖太多主题,在许多主题中推荐劣质资源,没有就特定课程哪些方面有价值提供原因或指引。我们努力对这份指引中的课程加以限制,仅仅包括那些你作为软件工程师 确实需要了解的,不论你的专业方向,并且对每门课程为何必要做出了解释以帮助你理解。\nfreecodecamp 主要关注编程,而不是计算机科学。至于你为什么要学习计算机科学,参见上文。如果你是个新手,我们建议先学 freecodecamp 的课程,一两年后再回归本指南。\nxx 编程语言怎么样? 学习一门特定的编程语言和学习计算机科学的一个领域 完全不在一个维度 —— 相比之下,学习语言容易且缺乏价值。如果你已经了解了一些语言,我们强烈建议遵照我们的指引,然后在学习的空当中习得语言,或者暂且不管以后再说。如果你已经把编程学得不错了(比如学完了 《计算机程序的构造和解释》),尤其是如果你学习过编译器,那么面对一门新的语言,你只需要花一个周末稍多的时间即可基本掌握,之后你可以在工作中学习相关的类库/工具/生态。\nxx 流行技术怎么样? 没有任何一种技术的重要程度可以达到学习其使用足以成为计算机科学教学的核心部分。不过,你对学习那门技术充满热情,这很不错。诀窍是先从特定的技术回退到基本的领域或概念,判断这门流行技术在技术的宏观大局中位于何处,然后才深入学习这门技术。\n计算机科学的核心是什么?\r为什么你们还在推荐 sicp? 先尝试读一下,有些人觉得 sicp 让人神魂颠倒,这在其他书很少见。如果你不喜欢,你可以尝试其他的东西,也许以后再回到 sicp。\n为什么你们还在推荐龙书? 龙书依旧是内容最为完整的编译器单本书籍。由于过分强调一些如今不够时新的主题的细节,比如解析,这本书招致了恶评。然而事实上,这本书从未打算供人一页一页的学习,而仅仅是为了给教师准备一门课程提供足够的材料。类似地,自学者可以从书中量身按需挑选主题,或者最好依照公开课授课教师在课程大纲中的建议。\n如何便宜获取教材? 我们所建议的许多教材在网上都可以免费获得,这多亏了作者们的慷慨。对于那些不免费的书籍,我们建议购买旧版本的二手书籍。广而言之,如果一本教材有多个版本,旧版本大概率是完全足够使用的。即便新版本的价格是旧版本的 10 倍,新版本也绝不可能比旧版本好 10 倍!\n中文翻译新增: 事实上,比起美国,在国内购买技术书籍可以说是相当“廉价”了。如果仍旧寻求更加便宜的购买渠道,可以参考这篇 v2ex 上的 讨论帖子,其中提到了一些不错的购买渠道。\n这份指引是谁写的? 这份指引由 bradfield school of computer science (旧金山)的两位教员: ozan onay 和 myles byrne 编写,并由 oz 于 2020 年更新。这份指引基于我们对数千名自学成才的工程师和培训班学生教授计算机科学基础的经验。感谢我们所有学生对自学资源的持续反馈。\n只要有足够的时间和动力,我们非常有信心,你可以自学完以上所有课程。如果你喜欢一个集中式、结构化、由教师指导的课程,你可能对我们的 计算机科学强化班 感兴趣。我们 不建议 你去攻读硕士学位。\n这份指引是谁翻译的? 这份指引的中文翻译是 社区共同贡献的成果,我们欢迎任何反馈和改进!\n","date":"2021-07-28","permalink":"https://loveminimal.github.io/posts/teach-yourself-computer-science/","summary":"\u003cp\u003ei.e. 自学计算机科学\u003c/p\u003e\n\u003cp\u003e🔔 转载 \u003ca href=\"https://teachyourselfcs.com/\"\u003e英文原文\u003c/a\u003e、\u003ca href=\"https://github.com/keithnull/TeachYourselfCS-CN/blob/master/TeachYourselfCS-CN.md\"\u003e中文译本\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e无意中看到这篇文章,感觉挺不错的,转载一下,方便摘录!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"teach yourself computer science"},{"content":" 了解浏览器,它已经成为主流的信息传递方式,并将持续壮大!\n了解它的内核、构成及动作方式。\n\u0026gt; 蒂姆·伯纳斯·李 (1955.06.08 - )\n2017 年,他因 “发明万维网、第一个浏览器和使万维网得以扩展的基本协议和算法” 而获得 2016 年度的图灵奖。\n世界上第一个网站 http://info.cern.ch/ ,于 1991.8.6 是上网,它解释了万维网是什么,如何使用网页浏览器和如何建立一个网页服务器等等。\n深入理解现代浏览器 1 本章节来源于 mariko kosaka (小坂真子) 2018 年 9 月在 google 开发者网站上发表的 “inside look at modern web browser” 系列文章。\n她的网站: https://kosamari.com/ 她的 twitter: https://twitter.com/kosamari 原文链接:\nhttps://developers.google.com/web/updates/2018/09/inside-browser-part1 https://developers.google.com/web/updates/2018/09/inside-browser-part2 https://developers.google.com/web/updates/2018/09/inside-browser-part3 https://developers.google.com/web/updates/2018/09/inside-browser-part4 本章节分为以下 4 个部分:\n架构:以 chrome 为例,介绍现代浏览器的实现架构; 导航:从输入 url 到获取到 html 响应称为导航; 渲染:浏览器解析 html 、下载外部资源、计算样式并把网页绘制到屏幕上; 交互:用户输入事件的处理与优化。 老外写文章是真的长……配图很仔细!\r准备 一些基础性概念……\n_1.cpu \u0026amp; gpu\ncpu(central processing unit)和 gpu(graphics processing unit)作为计算机中最重要的两个计算单元直接决定了计算性能。\ncpu 是计算机的大脑,负责处理各种不同的任务。gpu 最初被用于处理图像,更擅长利用多核心同时处理单一的任务。\n_2. 计算机架构\n如图,可以把计算机自下而上分成三层:硬件、操作系统和应用。有了操作系统的存在,上层运行的应用可以使用操作系统提供的能力使用硬件资源而不会直接访问硬件资源。\n_3. 进程与线程\n一个进程(process)是应用正在运行的程序,而线程(thread)是进程中更小的部分。当应用被启动时,进程就会被创建出来,程序可以创建线程来帮助其工作,操作系统会为进程分配私有的内存空间以供使用。当关闭程序时,这段私有的内存也会被释放。其实,还有比线程更小的存在 \u0026ndash; 协程, async/await 就是基于协程实现的。\n_4. 进程间通信(ipc)\n一个进程可以让操作系统开户别一个进程处理不同的任务。当两进程需要通信时,可以使用 ipc(inter process communication)。\n多数程序被设计成使用 ipc 来进程进程间的通信,好处在于一个进程给另一个进程发消息而没有回应时,并不影响当前的进程继续工作。\n架构 web 浏览器架构,可以实现为一个进程包含多个线程,也可以实现为很多进程包含少数线程通过 ipc 通信。如何实现浏览器,并没有统一的标准。chrome 最新的架构:最上层是浏览器进程,负责承担各项工作的其他进程,比如实用程序进程、渲染器进程、gpu 进程、插件进程等,如下图所示:\n渲染器进程对应新开的标签页,每新开一个标签页,就会创建一个新的渲染器进程。不仅如此,chrome 还会尽量给每个站点新开一个渲染器进程,包括 iframe 中的站点,以实现站点隔离。\n下面详细了解一下每个进程的作用,可以参考下图。\n浏览器进程 :控制浏览器这个应用的 chrome (主框架)部分,包括地址栏、书签、前进/后退按钮等,同时也会处理浏览器不可见的高权限任务,如发送网络请求、访问文件; 渲染器进程 :负责在标签页中显示网站及处理事件; 插件进程 : 控制网站用到的所有插件; gpu 进程 :在独立的进程中处理 gpu 任务。之所以放到独立的进程,是因为 gpu 要处理来自多个应用的请求,但要在同一个界面上绘制图形。 当然,还有其他进程,比如扩展进程、实用程序进程。要知道你的 chrome 当前打开了多少个进程,点击右上角的按钮,选择“更多工具”,再选择“任务管理器”。\nchrome 的多进程架构有哪些优点呢?\n最简单的情况下,可以想像一个标签页就是一个渲染器进程,比如 3 个标签页就是 3 个渲染器进程。这时候,如果有一个渲染器崩溃了,只要把它关掉即可,不会影响其他标签页。如果所有标签页都运行在一个进程中,那只要有一个标签页卡住,所有标签页都会卡住。\n除此之外,多进程架构还有助于安全和隔离。因为操作系统有限制进程特权的机制,浏览器可以借此限制某些进程的能力。比如,chrome 会限制处理任意用户输入的渲染器进程,不让它任意访问文件。\n由于进程都有自己私有的内存空间,因此每个进程可能都会保存某个公共基础设施(比如 chrome 的 javascript 引擎 v8)的多个副本。这会导致内存占用增多。为了节省内存,chrome 会限制自己可以打开的进程数量,限制的条件取决于设备内存和 cpu 配置,达到限制条件后,chrome 会用一个进程处理同一个站点的多个标签页。\nchrome 架构进化的目标是将整个浏览器程序的不同部分服务化,便于分割或合并。基本思路是在调配设备中,每个服务独立开里程,保证稳定;在低配设备中,多个服务合并为一个进程,节约资源。同样的思路也应用到了 android 上。\n重点说一说 站点隔离 。站点隔离是新近引入 chrome 的一个里程碑式特性,即每个跨站点 iframe 都运行一个独立的渲染器进程。即便像前面说的那样,每个标签页单开一个渲染器进程,但允许跨站点的 iframe 运行在同一个渲染器进程中并共享内存空间,那安全攻击仍然有可能绕开 同源策略 ,而且有人发现在现代 cpu 中,进程有可能读取任意内存。\n进程隔离是隔离站点、确保上网安全最有效的方式。chrome 67 桌面版默认采用站点隔离。站点隔离是多年工程化努力的结果,它并非多开几个渲染器进程那么简单。比如,不同的 iframe 运行在不同进程中,开发工具在后台仍然要做到无缝切换,而且即便简单地 ctrl+f 查找也会涉及在不同进程中搜索。\n导航 导航涉及浏览器进程与线程间为显示网页而通信。一切从用户在浏览器中输入一个 url 开始,输入 url 之后,浏览器会通过互联网获取数据并显示网页。从请求网页到浏览器准备渲染网页的过程,叫做 _导航 。\n如前所述,标签页外面的一切都由浏览器进程处理。浏览器进程中包含如下线程:\nui 线程:负责绘制浏览器的按钮和地址栏; 网络线程:负责处理网络请求并从互联网接收数据; 存储线程:负责访问文件和存储数据。 下面我们来逐步看一看导航的几个步骤。\n1. 处理输入 ui 线程会判断用户输入的是查询字符器还是 url(因为 chrome 地址样同时也是搜索框)。\n2. 开始导航 如果输入的是 url ,ui 线程会通知网络线程发起网络调用,获取网站内容。此时标签页左端显示旋转图标,网络线程进行 dns 查询、建立 tls 连接(对于 https)。网络线程可能收到服务器的重定向头部,如 http 301 ,此时网络线程会跟 ui 线程沟通,告诉它服务器需求重定向,然后,再发起对另一个 url 的请求。\n3. 读取响应 服务器返回的响应体到来之后,网络线程会检查接收到的前几个字节。响应的 content-type 头部应该包含数据类型,如果没有这个字段,则需要 mime 类型嗅探 。看看 chrome 源码 中的注释就知道这一块有多难搞……\n如果响应是 html 文件,那下一步就是把数据交给渲染进程。但如果是一个 zip 文件或其他文件,就意味着是一个下载请求,需要把数据传给下载管理器。\n此时也是 “安全浏览” 检查环节。如果域名和响应数据匹配已知的恶意网站,网络线程会显示警告页。此外,corb(cross origin read blocking)检查也会执行,以确保敏感的跨站点数据不会发送给渲染器进程。\n4. 联系渲染器进程 所有查检完毕,网络线程确认浏览器可以导航到用户请求的网站,于是会通知 ui 线程数据已经准备好了,ui 线程会联系渲染器进程渲染网页。\n由于网络请求可能要共几百毫秒才能拿到响应,这里还会应用一个优化策略。第二步 ui 线程要求网络线程发送请求后,已经知道可能要导航到哪个网站了。因此在发送网络请求的同时,ui 线程会提前联系或并行启动一个渲染进程。这样在网络线程收到数据后,就已经有渲染器进程原地待命了。如果发生了重定向,这个待命进程可能用不上,而是换作其他进程去处理。\n= ‘流’行策略\n5. 提交导航 数据和渲染器进程都有了,就可以通过 ipc 从浏览器进程向渲染器进程提交导航,渲染器进程也会同时接收到不间断的 html 数据流。当浏览器进程收到渲染器进程的确认消息后,导航完成,文档加载阶段开始。\n此时,地址栏会更新,安全指示图标和网站设置 ui 也会反映新页面的信息。当前标签页的会话历史会更新,后退/前进按钮起作用。为便于标签页/会话在关闭标签页或窗口后恢复,会话历史会写入磁盘。\n6. 初始加载完成 提交导航之后,渲染器进程将负责加载资源和渲染页面,而在“完成”渲染后(在所有 iframe 中的 onload 事件触发且执行完成后),渲染器进程会通过 ipc 给浏览器进程发送一个消息。此时,ui 线程停止标签页上的旋转图标。\n初始加载完成后,客户端 javascript 仍然可能加载额外资源并重新渲染页面。\n如果此时用户在地址又输入了其他 url 呢?浏览器进程还会重复上述步骤,导航到新站点。不过在些之前,需要确认已渲染的网站是否关注 beforeunload 事件。因为标签页中的一切,包括 javascript 代码都由渲染器进程处理,所以浏览器进程必须与当前的渲染器进程确认后再导航到新站点。\n如果导航请求来自当前渲染器进程(用户点击了链接或 javascript 运行了 window.location = \u0026quot;https://newsite.com\u0026quot; ),渲染器进程首先会检查 beforeunload 处理程序。然后,它会走一遍与浏览器进程触发导航同样的过程,唯一的区别在于导航请求是由渲染器进程提交给浏览器进程的。\n导航到不同的网站时,会有一个新的独立渲染器进程负责处理新导航,而老的渲染器进程要负责处理 unload 之类的事件。更多细节可以参考 “页面生命周期 api” 。\n另外,导航阶段还可能涉及 service worker ,即网页应用中的网络代理服务,开发者可以通过它控制什么缓存在本地,何时从网络获取新数据。 service worker 说到底也是需要渲染器进程运行的 javascript 代码。 如果网站注册了 service worker ,那么导航请求到来时,网络线程会根据 url 将其匹配出来,此时 ui 线程就会联系一个渲染器进程来执行 service worker 的代码:可能只要从本地缓存读取数据,也可能需要发送网络请求。\n如果 service worker 最终决定从网络请求数据,浏览器进程与渲染器进程间的这种往返通信会导致延迟。因此,这里会有一个“导航预加载”的优化,即在 service worker 启动同时预先加载资源,加载请求通过 http 头部与服务器沟通,服务器决定是否完全更新内容。\n渲染 _渲染 是渲染器进程内部的工作,涉及 web 性能的诸多方面。标签页中的一切都由渲染器进程负责处理,其中主线程负责运行大多数客户端 javascript 代码,少量代码可能由工作线程处理(如果用到了 web worker 或 service worker)、合成器(compositor)线程和栅格化(raster)线程负责高效、平滑地渲染页面。\n渲染器进程的核心任务是把 html、css 和 javascript 转换成用户可以交互的网页接下来,我们从整体上过一遍渲染器进程处理 web 内容的各个阶段。\n1. 解析 html _1. 构建 dom\n渲染器进程收到导航的提交消息后,开始接收 html,其主线程开始解析文本字符器(html),并将它转换为 dom(document object model,文档对象模型)。\ndom 是浏览器内部对页面的表示,也是 javascript 与之交互的数据结构和 api 。\n如何将 html 解析为 dom 由 html 标准定义。html 标准要求浏览器兼容错误的 html 写法,因此浏览器会“忍气吞声”,绝不报错。详情可以看看 “解析器错误处理及怪异情形简介” 。\n_2. 加载子资源\n网站都会用到图片、css 和 javascript 等外部资源。浏览器需要从缓存或网络加载这些文件,主线程可以在解析并构建 dom 过程中发现一个加载一个,但这样效率太低。为此,chrome 会在解析同时并发运行“预加载扫描器”,当发现 html 文档中有 \u0026lt;img\u0026gt;或 \u0026lt;link\u0026gt; 时,预加载扫描器会将请求提交给浏览器进程中的网络线程。\n_3.javascript 可能阻塞解析\n如果 html 解析器碰到 \u0026lt;script\u0026gt; 标签,会暂停解析 html 文档并加载、解析和执行 javascript 代码。因为 javascript 有可能通过 document.write() 修改文档,进而改变 dom 结构(html 标准的“解析模型”有一张图可以一目了然: https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model)。 所以 html 解析器必须停下来执行 javascript ,然后再恢复解析 html 。至于执行 javascript 的细节,大家可以关注 v8 团队相关的分享: https://mathiasbynens.be/notes/shapes-ics 。\n提示浏览器你要加载资源。\n2. 计算样式 光有 dom 还不行,因为并不知道页面应该长啥样。所以接下来,主线程要解析 css 并计算每个 dom 节点的样式。这个过程就是根据 css 选择符,确定每个元素要应用什么样式。在 chrome 开发工具“计算的模式”中可以看每个元素计算后的样式。\n就算网页没有提供任何 css ,每个 dom 节点仍然会有计算的样式。这是因为浏览器有一个默认的样式表,chrome 默认的样式在这里: https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/resources/html.css 。\n3. 布局 到这一步,渲染器进程就知道了文档的结构,也知道了每个节点的样式,但基于这些信息仍然不足以渲染页面。比如,你通过电话跟朋友说:“画一个红色的大圆形,还有一个蓝色的小方形”,你的朋友仍然不知道该画成什么样。\n_布局 就是要找到元素间的几何位置关系。主线程会遍历 dom 元素及其计算样式,然后构造一棵布局树,这棵树的每个节点将带有坐标和大小信息。布局树与 dom 树的结构类似,但只包含页面中可见元素的信息。如果元素被应用了 display: none ,则布局树中不会包含它( visibility: hidden 的元素会包含在内)。类似的,通过伪类 p::before {content: \u0026quot;hi!\u0026quot;} 添加的内容会包含在布局树中,但 dom 树中却没有。\n确定页面的布局要考虑很多因素,并不简单。比如,字体大小、文本换行都会影响段落的形状,进而影响后续段落的布局。css 可以让元素浮动到一边、隐藏溢出边界的内容、改变文本显示方向。可想而知,布局阶段的任务是非常艰巨的。\n4. 绘制 有了 dom、样式和布局,仍然不足以渲染页面。还要解决先画什么后画什么,即绘制顺序的问题。比如, z-index 影响元素叠放,如果有这个属性,那简单地按元素在 html 中出现的顺序绘制就会出错。\n因此,在这一步,主线程会遍历布局树并创建绘制记录。绘制记录是对绘制过程的注解,比如“先画背景,然后画文本,最后画矩形”。如果你用过 \u0026lt;canvas\u0026gt; ,应该更容易理解这一点。\n渲染是一个流水线作业(pipeline):前一道工序的输出就是下一道工序的输入。这意味着如果布局树有变化,则相应的绘制记录也要重新生成。\n如果元素有动画,浏览器就需要每帧运行一次渲染流水线。目前显示器的刷新率为每秒 60 次(60fps),也就是说每秒 60 帧,动画会显得很流畅。如果中间缺了帧,那页面就会“闪眼睛”。\n即便渲染操作的频率能跟上屏幕刷新率,但由于计算发生在主线程上,而主线程可能因为运行 javascript 被阻塞,此时动画会因为阻塞被卡住。\n此时,可以使用 requestanimationframe() 将涉及动画的 javascript 操作分块并调度到每一帧的开始去运行。对于耗时的不必操作 dom 的 javascript 操作,可以考虑 web worker ,避免阻塞主线程。\n5. 合成 知道了文档结构、每个元素的样式、页面的几何关系,以及绘制顺序,接下来就该绘制页面了。具体怎么绘制呢?把上述信息转换为屏幕上的像素叫做 _栅格化 。\n最简单的方式,可能就是把页面在当前视口中的部分先转换为像素,然后随着用户滚动页面,再移动栅格化的画框(frame),填补缺失的部分。chrome 最早的版本就是这样干的。\n但现代浏览器会使用一个更高级的步骤叫合成。什么是合成? _合成(composite) 是将页面不同部分先分层并分别栅格化,然后再通过独立的合成器线程合成页面。这样当用户滚动页面时,因为层都已经栅格化,所以浏览器唯一要做的就是合成一个新的帧。而动画也可以用这样的方式实现:先移动层,再合成帧。\n怎么分层?为了确定哪个元素应该在哪一层,主线程会遍历布局树并创建分层树(这一部分在开发工具的“性能”面板中叫“update layer tree”)。如果页面某些部分应该独立一层(如滑入的菜单),那你可以在 css 中给它加上 will-change 属性来提醒浏览器。\n分层并不是越多越好,合成过多的层有可能还不如每帧都对页面中的一小部分执行一次栅格化更快。\n创建了分层树,确定了绘制顺序,主线程就会把这些信息提交给合成器线程。合成器线程接下来负责将每一层转换为像素 \u0026ndash; 栅格化。一层有可能跟页面一样大,此时合成器线程会将它切成小片(tile),再把每一片发给栅格化线程。栅格化线程将每一小片转换为像素后将它们保存在 gpu 的内存中。\n合成器线程会安排栅格化线程优先转换视口(及附近)的小片,而构成一层的小片也会转换为不同分辨率的版本,以便在用户缩放时使用。\n所有小片都栅格化以后,合成器线程会收集叫做“绘制方块”(draw quad)的小片信息,以创建合成器帧。\n绘制方块:包含小片的内存地址、页面位置等合成页面相关的信息; 合成器帧:由从多绘制方块拼成的页面中的一帧。 创建好的合成器帧会通过 ipc 提交给浏览器进程。与此同时,为更新浏览器界面,ui 线程可能还会添加另一个合成器帧;或者因为有扩展,其他渲染器也可能添加额外的合成器帧。所有这些合成器帧都会发送给 gpu,以便最终显示在屏幕上。如果发生滚动事件,合成器线程会再创建新的合成器帧并发送给 gpu 。\n使用合成的好处是不用牵扯主线程,合成器线程不用等待样式计算或 javascript 执行,这也是为什么“只需合成的动画”被认为性能最佳的原因。因为如果布局和绘制需要再次计算,那还得用到主线程。\n交互 最后,我们看一看合成器如何处理用户交互。说到用户交互,有人可能只会想到在文本框里打字或点击鼠标。实际上,从浏览器的角度看,交互意味着来自用户的任何输入:鼠标滚动转动、触摸屏幕、鼠标悬停,这些都是交互。\n当用户交互比如触摸事件发生时,浏览器进程首先接收该手势。但是,浏览器进程仅仅知道手势发生在哪里,因为标签页中的内容是渲染器进程处理。因此,浏览器进程会把事件类型(如 touchstart )及其坐标发送给渲染器进程,渲染器进程会处理这个事件,即根据事件目标来运行注册的监听程序。\n具体来说,输入事件是由渲染器进程中合成器线程处理的。如前所述,如果页面上没有注册事件监听程序,那合成器线程可以完全独立于主线程生成新的合成器帧。但是如果页面上注册了事件监听程序呢?此时合成器线程怎么知道是否有事件要处理?\n这就涉及一个概念,叫“非快速滚动区”(non-fast scrollable region)。我们知道,运行 javascript 是主线程的活儿。在页面合成后,合成器线程给附加了事件处理程序的页面区域打上 “non-fast scrollable region” 的记号。有了这个记号,合成器线程就可以在该区域发生事件时把事件发送给主线程。\n如果事件发生在这个区域外,那合成器线程会继续合成新帧而不会等待主线程。\n提到注册事件,有一个常见的问题要注意。很多人喜欢使用事件委托来注册处理程序,这是利用事件冒泡原理,把事件注册到最外层元素上,然后再根据事件目标是否执行任务。\n一个事件处理程序就可以面向多个元素,这种高效的写法因此很流行。然而,从浏览器的角度看,这样会导致整个页面被标记为“非快速滚动区”。这也就意味着,即便事件发生在那些不需要处理的元素上,合成器线程也要每次都跟主线程沟通,并等待它的回应。于是,合成器线程平滑滚动的优点就被抵销了。\n为缓冲使用事件委托带来的副作用,可以在注册事件时传入 passive: true 。这个选项会提醒浏览器,你仍然希望主线程处理事件,但与此同时合成器线程也可以继续合成新的帧。\ndocument.body.addeventlistener( \u0026#39;touchstart\u0026#39;, (evt) =\u0026gt; { // ... }, { passive: true } ); 此外,检查事件是否可以取消也是一个优化策略。假设页面中有一个盒子,你想限制盒子中的内容只能水平滚动。使用 passive: true 可以让页面平滑滚动,但为了限制滚动方向而调用 preventdefault 则不会避免垂直滚动,此时可以检查 evt.cancelable 。\ndocument.body.addeventlistener( \u0026#39;pointermove\u0026#39;, (evt) =\u0026gt; { if (evt.cancelable) { evt.preventdefault(); // 阻止原生滚动 // ... } }, { passive: true } ); 当然,也可以使用 css 规则,如 touch-action 完全避免使用事件处理程序。\n#area { touch-action: pan-x; } 合成器线程把事件发送给主线程以后,要做的第一件事就是通过测试(hit test)找到事件目标,命中测试就是根据渲染进程生成的绘制记录数据和事件坐标找到下方的元素。\n另外,事件还有一个触发频率的问题。通常的触屏设备每秒会产生 60~120 次触碰事件,而鼠标每秒会产生约 100 次事件。换句话说,输入事件具有比每秒刷新 60 次的屏幕更高的保真度。\n如果像 touchmove 这种连续性事件,以每秒 120 次的频率发送到主线程,相比更慢的屏幕刷新率而言,就会导致过多的命中测试和 javascript 执行。\n为把主线程过多的调用降至最少,chrome 会合并(coalesce)连续触发的事件(如 wheel、mousewheel、mousemove、pointermove、touchmove ),并将它们延迟到时恰好在下一次 requestanimationframe 之前派发。\n对于其他离散触发的事件,像 keydown、keyup、mouseup、mousedown、touchstart 和 touchend 会立即派发。\n合并后的事件在多数情况下足以保证不错的用户体验。但是,在一些特殊应用场景下,比如需要基于 touchmove 事件的坐标生成轨迹的绘图应用,合并事件就会导致丢失一些坐标,影响所绘线条的平滑度。\n此时,可以使用指针事件的 =getcoalescedevents= 方法,取得被合并事件的信息:\nwindow.addeventlistener(\u0026#39;pointermove\u0026#39;, (evt) =\u0026gt; { const events = evt.getcoalescedevents(); for (let evt of events) { const x = evt.pagex; const y = evt.pagey; // 使用 x 和 y 坐标画线 // ... } }); 这是个小小的结尾,相信不少前端开发者早已知道给 \u0026lt;script\u0026gt; 标签添加 defer、async 属性的作用。通过阅读本文,你应该也知道了为什么在注册事件监听器时最好传入 passive: true 选项,知道了 css 的 will-change 属性让浏览器做出不同的决策。事实上,不止上面这些,看完看懂篇文章,你甚至也会对其他关于浏览器性能优化的细节感到豁然开朗,从而对更多关于网页性能的话题会产生兴起。而这正是深入理解现代浏览器的重要意义和价值所在,因为它为我们打开了一扇大门。\n总结 至此,我们已经可能通过从用户在浏览器地址栏中的一次输入到页面图像的显示了解浏览器是如何工作的,小结如下:\n浏览器进程做为最重要的进程负责大多数页签外部的工作,包括地址栏显示、网络请求、页签状态管理等; 不同的渲染进程负责不同的站点渲染工作,渲染进程间彼此独立; 渲染进程在渲染页面的过程中会通过浏览器进程获取站点资源,只有安全的资源才会被渲染进程接收到; 渲染里程中主线程负责除了图像生成图像生成外绝大多数工作,如何减少主线程上代码的运行是交互性能的关键; 渲染进程中的合成线程和栅格线程负责图像生成,利用分层技术可以优化图像生成的效率; 当用户与页面发生交互时,事件的传播途径从浏览器进程到渲染进程的合成线程再根据事件监听的区域决定是否要传递给渲染进程的主线程处理。 ","date":"2021-07-21","permalink":"https://loveminimal.github.io/posts/browser/","summary":"\u003cblockquote\u003e\n\u003cp\u003e了解浏览器,它已经成为主流的信息传递方式,并将持续壮大!\u003cbr\u003e\n了解它的内核、构成及动作方式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cimg src=\"/posts/browser/imgs/tim.jpg\" width=\"300\" alt=\"蒂姆·伯纳斯·李\" /\u003e\r\n\u003cp\u003e\u003ccode\u003e\u0026gt; 蒂姆·伯纳斯·李 (1955.06.08 - )\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e2017 年,他因 “发明万维网、第一个浏览器和使万维网得以扩展的基本协议和算法” 而获得 2016 年度的图灵奖。\u003c/p\u003e","title":"浏览器"},{"content":" 不可或缺的工具!!!\n本文旨在记录个人使用过程中遇到的相关 git 命令,非教程式的,详细学习请参阅 《pro git》 的中文文档。\n入门篇 安装设置 去 git 官网 下载对应版本的 git 安装即可,此处不再赘述。\n安装完成后,需要设置当前用户的名字和 email 地址:\ngit config --global user.name \u0026#34;your name\u0026#34; git config --global user.email \u0026#34;email@example.com\u0026#34; 你总是可以通过以下方式获取帮助:\ngit help \u0026lt;verb\u0026gt; git \u0026lt;verb\u0026gt; -h # --help 最常用,如 git config --help man git-\u0026lt;verb\u0026gt; 基本操作 git init # 初始化仓库 # …… git add . # 添加工作区所有变更到暂存区 git commit -m \u0026#34;some commit log.\u0026#34; # 提交缓存区内变更到本地仓库 进阶篇 远程仓库 我们就以 站点仓库 为例。\n本地仓库和远程仓库之间的传输是通过 ssh 加密的,在使用远程仓库之前,我们需要先创建 ssh key 。\nssh-keygen -t rsa -c \u0026#34;youremail@example.com\u0026#34; 在用户主目录下,会生成 .ssh 目录,包含 id_rsa (私钥)和 id_rsa.pub (公钥)等文件。\n……\n# 列出远程仓库(名称及地址) git remote -v # --verbose # 添加远程仓库 git remote add \u0026lt;name\u0026gt; \u0026lt;url\u0026gt; # 移除远程仓库 git remote remove \u0026lt;name\u0026gt; # 重命名远程仓库分支 git remote rename \u0026lt;old\u0026gt; \u0026lt;new\u0026gt; 关于远程仓库的其他操作,会分散在后续章节中,不在此处单独列出。\n分支 # 列出分支 git branch git branch -l # --list 列出本地分支 git branch -a # --all 列出远程、本地分支 # 创建分支 git checkout -b \u0026lt;branch\u0026gt; # 切换分支 git checkout \u0026lt;branch\u0026gt; # 删除分支 git branch -d \u0026lt;branch\u0026gt; # --delete 删除合并完全的分支 git branch -d \u0026lt;branch\u0026gt; # 强制删除分支(包含未合并完全) # 删除远程分支 git branch -d \u0026lt;remote-repo\u0026gt; \u0026lt;remote-branch\u0026gt; # --delete 删除远程仓库的指定分支 git push \u0026lt;remote-repo\u0026gt; :\u0026lt;remote-branch\u0026gt; # or 推送一个空分支到远程分支,其实就相当于删除远程分支 git push \u0026lt;remote-repo\u0026gt; --delete \u0026lt;remote-brach\u0026gt; # # 重命名分支 git branch -m \u0026lt;branch\u0026gt; # --move # 合并分支 git merge \u0026lt;other-branch\u0026gt; \u0026lt;current-branch\u0026gt; 我们从一个远程仓库拉取分支,默认会在本地创建 master 分支,并关联到远程仓库的主分支。\n# 拉取远程仓库并关联到自定义本地分支 git clone -b \u0026lt;local-branch\u0026gt; \u0026lt;remote-repo-url\u0026gt; # 摘取远程仓库指定分支并关联自定义本地分支 git remote add origin \u0026lt;remote-repo-url\u0026gt; git fetch origin \u0026lt;remote-branch\u0026gt; git checkout -b \u0026lt;local-branch\u0026gt; origin/\u0026lt;remote-branch\u0026gt; # !关键 git pull origin \u0026lt;remote-branch\u0026gt; 强制推送本地分支到远程仓库分支(两者分支名称不同时),如下:\n# git remote add origin https://github.com/loveminimal/loveminimal.github.io.git # git push -f origin master:main # 本地分支 master ,远程分支 main git push -f origin master # 本地分支与远程分支名称相同,皆为 master !!永远不要试图一蹴而就,在使用的过程中慢慢补充完善即可!\rgit 设置本地与远程分支关联:\n# 查看本地与远程分支关联的情况 git branch -vv # 设置本地与远程分支关联 git branch --set-upstream-to=origin/\u0026lt;remote-branch\u0026gt; \u0026lt;local-branch\u0026gt; git submodule 子模块 参考 https://zhuanlan.zhihu.com/p/87053283\n果然,可以模块的地方,最终都会模块化。\r假定我们有两个项目: project-main (主项目)和 project-sub-1 (子模块项目)。\n我们可以使用 git submodule add \u0026lt;submodule_url\u0026gt; [\u0026lt;local_dir_name\u0026gt;] 命令在项目中创建一个子模块。\n*其中, \u0026lt;local_dir_name\u0026gt; 可选。\n上述命令执行之后,项目仓库会多出两个文件: .gitmodules (子模块的相关信息)和 project-sub-1 (子模块当前版本的版本号信息)。\n如何获取 submodule 呢?\n为了方便,我们不妨称主模块为 main ,子模块为 sub 。\n对于主项目 main 使用普通的 clone 操作并不会拉取到子模块 sub 中的实际代码(它是空的)。如果希望子模块代码也获取到,可以使用以下两种方式:\n在克隆 main 的时候带上参数 --recurse-submodules ,如此会递归地将项目中所有子模块的代码拉取;或, 在 main 中执行 git submodule init \u0026amp; git submodule update ,会根据主项目的配置信息,拉取更新子模块中的代码(or git submodule update --init --recursive)。 子模块内容更新了如何操作?\n对于子模块 sub 而言,它并不知道引用自己的主项目的存在,其自身是一个完整的 git 仓库,按照正常的 git 代码管理规范操作即可。\n主模块的处理,目前就记住,操作了 sub 后,再去操作 main 即可。\n删除子模块\n使用 git submodule deinit 命令卸载一个子模块。这个命令如果添加上参数 --force ,则子模块工作区内即使有本地的修改,也会被移除。\n其他 搭建 git 服务器并启用 hooks 许多教程使用的都是 root 账户,其实我们使用哪个账户都可以,比如,这里我们就用当前用户 jack 来搭建 git 服务。\n首先,把客户机的公钥 id_rsa.pub 文件,导入到 /home/jack/.ssh/authorized_keys 文件里(若没有,新建该文件即可),一行一个。\n然后,选定一个目录作为 git 仓库,比如 ~/.repo/site.git ,在 .repo 目录下,输入:\ngit init --bare site.git git 就会创建一个裸仓库,它没有工作区,纯粹是为了共享。\n最后,在客户机上,就可以通过 git clone 命令克隆远程仓库了,如:\n# jack - 服务器主机用户名 # ovirgo.com - 服务器绑定的域名(可以是 ip) git clone jack@ovirgo.com:/home/jack/.repo/site.git 什么是 hooks 呢?\n执行 cd ~/.repo/site.git/hooks \u0026amp;\u0026amp; ls ,你会发现许多 hooks 文件示例,用于该仓库去响应客户机的某些指令时的一些操作,比如,我们在客户机向该仓库推送时,自动克隆部署该仓库。\n在 hooks 目录下,执行以下操作:\n# 新建 post-receive touch post-receive vim post-receive 编辑 post-receive 这个钩子:\n#!/bin/sh cd /home/jack rm -rf blog git clone .repo/site.git blog 是的,它是一个 shell 脚本,如此而已。\n综上,我们完成了服务器端的 git 搭建及自动化部署。\n","date":"2021-07-21","permalink":"https://loveminimal.github.io/posts/git/","summary":"\u003cblockquote\u003e\n\u003cp\u003e不可或缺的工具!!!\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文旨在记录个人使用过程中遇到的相关 Git 命令,非教程式的,详细学习请参阅 \u003ca href=\"https://git-scm.com/book/zh/v2\"\u003e《Pro Git》 的中文文档\u003c/a\u003e。\u003c/p\u003e","title":"git"},{"content":"🔔 参考 中文技术文档的写作规范 ,及个人写作习惯。\n简介 本文旨在学习和思考如何形成良好统一的文档写作习惯,原则是做到简洁、易懂,便于检索。\n文档结构 目录/文件 必备/类型 说明 简介 introduction 是/目录 提供对产品和文档本身的总体的、扼要的说明 快速上手 getting started 否/文件 如何最快速地使用产品 入门篇 basics 是/目录 又称“使用篇”,提供初级的使用教程 环境准备 - prerequisite 是/文件 软件使用需要满足的前置条件 安装 - installation 否/文件 软件的安装方法 设置 - configuration 是/文件 软件的设置 进阶篇 advanced 否/目录 又称“开发篇”,提供中高级的开发教程 api reference 否/目录、文件 软件 api 的逐一介绍 faq 否/文件 常见问题解答 附录 appendix 否/目录 不属于教程本身,但对阅读教程有帮助的内容 名词解释 - glossary 否/文件 最佳实践 - recipes 否/文件 故障处理 - troubleshooting 否/文件 版本说明 - changelog 否/文件 反馈方式 - feedback 否/文件 \u0026gt; 文档结构\n*两个真实范例,可参考 redux 手册 和 atom 手册 。\n","date":"2021-07-19","permalink":"https://loveminimal.github.io/posts/zh-tech-doc/","summary":"\u003cp\u003e🔔 参考 \u003ca href=\"https://www.ruanyifeng.com/blog/2016/10/document_style_guide.html\"\u003e中文技术文档的写作规范\u003c/a\u003e ,及个人写作习惯。\u003c/p\u003e","title":"中文技术文档"},{"content":"🔔 参考 https://www.cnblogs.com/wjw1014/p/13564175.html\n简介 浏览器的默认滚动条往往都不怎么好看…… 让我们对它来做一些调整吧!\n假如我们页面的页面是这样的:\n\u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;滚动条样式\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;div__scroll-bar\u0026#34;\u0026gt;...\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;...\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 修改某个元素的滚动条 /* 设置一个元素的宽高(以使其内容滚动) */ #div__scroll-bar { width: 300px; height: 200px; border: 1px solid red; overflow: scroll; } /* 滚动条整体部分(width 纵向滚动条 height 横向滚动条) */ #div__scroll-bar::-webkit-scrollbar { width: 10px; height: 10px; } /* 滚动条的轨道(里面装有 thumb) */ #div__scroll-bar::-webkit-scrollbar-track { background-color: #afa; } /* 内层轨道,滚动条中间部分(除去两侧用于微调的 button 和交汇区) */ #div__scroll-bar::-webkit-scrollbar-track-piece { background-color: #f00; } /* 滚动条里面的小方块 */ #div__scroll-bar::-webkit-scrollbar-thumb { background-color: pink; border-radius: 10%; -webkit-box-shadow: inset 0 0 5px #880d0d; } #div__scroll-bar::-webkit-scrollbar-thumb:hover { background: #333; } /* 滚动条的轨道的两端按钮,允许通过点击微调小方块的位置 */ #div__scroll-bar::-webkit-scrollbar-button { background-color: rgb(22, 182, 27); /* display: none; */ } /* 边角,即两个滚动条的交汇处 */ #div__scroll-bar::-webkit-scrollbar-corner { background: #179a16; } 修改浏览器默认滚动条样式 /* 整个滚动条 */ ::-webkit-scrollbar { width: 5px; height: 5px; } /* 滚动条有滑块的轨道部分 */ ::-webkit-scrollbar-track-piece { background-color: transparent; border-radius: 5px; } /* 滚动条滑块(竖向:vertical 横向:horizontal) */ ::-webkit-scrollbar-thumb { cursor: pointer; background-color:#bbb; border-radius: 5px; } /* 滚动条滑块hover */ ::-webkit-scrollbar-thumb:hover { background-color: #999; } /* 同时有垂直和水平滚动条时交汇的部分 */ ::-webkit-scrollbar-corner { display: block; /* 修复交汇时出现的白块 */ } 总结 参数 说明 ::-webkit-scrollbar 滚动条整体部分 ::-webkit-scrollbar-track 滚动条的轨道 ::-webkit-scrollbar-thumb 滚动条里面的小方块 ::-webkit-scrollbar-button 滚动条的轨道的两端微调按钮 ::-webkit-scrollbar-track-piece 内层轨道,滚动条中间部分 ::-webkit-scrollbar-corner 边角,即两个滚动条的交汇处 ::-webkit-resizer 两个滚动条的交汇处上用于通过拖动调整元素大小的小控件 ","date":"2021-03-09","permalink":"https://loveminimal.github.io/posts/scroll-bar/","summary":"\u003cp\u003e🔔 参考 \u003ca href=\"https://www.cnblogs.com/wjw1014/p/13564175.html\"\u003ehttps://www.cnblogs.com/wjw1014/p/13564175.html\u003c/a\u003e\u003c/p\u003e","title":"scroll bar"},{"content":" 我并不喜欢王垠这个人,甚至有些反感,但他的某些博文我很喜欢,也因此而受益,hmm\u0026hellip;\n*tip. 非原文摘录,各别语句进行了删减和改动,建议看原文(点击章节标题)。\nblog essays 什么是“对用户友好” 什么是“对用户友好”\nany intelligent fool can make things bigger, more complex, and more violent. it takes a touch of genius - and a lot of courage - to\u0026gt; move in the opposite direction.\n\u0026ndash; albert einstein\n任何聪明的傻瓜都能把事情做得更大、更复杂、更暴力,而在相反的方向上前进则需要一点天才和很大的勇气。\n“对用户不友好”的背后,其实是程序设计的不合理使得它们 缺少抽象 ,而不是用户的问题。\n这点确实,比如,我就总是搞不清楚冰箱中的致冷程度数字、洗衣机面板上的各种设置开关……\r如何对用户更加友好呢? 统一、抽象!\ngtf - great teacher friedman gtf - great teacher friedman\n程序语言的研究者们往往追逐一些“新概念”,却未能想到很多这些新概念早在几十年前就被 friedman 想到了。\n知识的深度是无止境的。\nfriedman 研究一个东西的时候总是全身心的投入,执着的热爱。\n在 friedman 的课上,我利用它们(如 closure 、cps 等概念)来完成有实际意义的目标,才真正的体会到这些概念的内涵和价值。\n一个例子就是课程进入到没几个星期的时候,我们开始 写解释器 来执行简单的 scheme 程序。然后我们把这个解释器 进行 cps 变换 ,引入全局变量作为 “寄存器” (register),把 cps 产生的 continuation 转换成 数据结构(也就是堆栈) 。最后我们得到的是一个 抽象机 (abstract machine),而这在本质上相当于一个真实机器里的中央处理器(cpu)或者虚拟机(比如 jvm)。所以我们其实从无到有,“发明”了 cpu!从这里,我才真正的理解到寄存器,堆栈等的本质,以及我们为什么需要它们。我才真正的明白了,冯诺依曼体系构架为什么要设计成这个样子。后来他让我们去看一篇他的好朋友 olivier danvy 的论文,讲述如何从各种不同的解释器经过 cps 变换得出不同种类的抽象机模型。这是我第一次感觉到程序语言的理论对于现实世界的巨大威力,也让我理解到, 机器并不是计算的本质 。机器可以用任何可行的技术实现,比如集成电路,激光,分子,dna…… 但是无论用什么作为机器的材料, 我们所要表达的语义,也就是计算的本质,却是不变的。\n当然, 重新发明 东西并不会给我带来论文发表,但是它却给我带来了更重要的东西,这就是 独立的思考能力 。一旦一个东西被你“想”出来,而不是从别人那里 “学”过来,那么你就知道这个想法是 如何产生 的。这比起直接学会这个想法要有用很多,因为你知道这里面 所有的细节和犯过的错误 。而最重要的,其实是由此得到的 直觉 。如果直接去看别人的书或者论文,你就很难得到这种直觉,因为一般人写论文都会把直觉埋藏在一堆符号公式之下,让你看不到背后的真实想法。如果得到了直觉,下一次遇到类似的问题,你就有可能很快的利用已有的直觉来解决新的问题。\n什么是语义学 什么是语义学\n一个程序的“语义” 通常是由另一个程序(“解释器”)决定的 。 程序只是一个数据结构 ,通常表示为语法树(abstract syntax tree)或者指令序列 。这个数据结构本身其实没有意义,是解释器让它产生了意义,对同一个程序可以有不同的解释。\n解释器接受一个“程序”(program),输出一个“值”(value)。这个所谓的“值”可以具有非常广泛的含义。它可能是一个整数,一个字符串,也有可能是更加奇妙的东西。\ncpu 其实也是一个解释器,它的输入是以二进制表示的机器指令,输出是一些电信号。人脑也是一个解释器,它的输入是图像或者声音,输出是神经元之间产生的“概念”。\n所以“语义学”,基本上就是研究各种解释器。\n解释器的原理其实很简单,但是结构非常精巧微妙,如果你从复杂的语言入手,恐怕永远也学不会。最好的起步方式是写一个基本的 lambda calculus 的解释器。lambda calculus 只有三种元素,却可以表达所有程序语言的复杂结构。\n专门讲语义的书很少,现在推荐一本我觉得深入浅出的:《programming languages and lambda calculi》。只需要看完前半部分(part i 和 ii,100 来页)就可以了。这书好在什么地方呢?它是从非常简单的布尔表达式(而不是 lambda calculus)开始讲解 什么是递归定义,什么是解释,什么是 church-rosser,什么是上下文 (evaluation context) 。在让你理解了这种简单语言的语义,有了足够的信心之后,才告诉你更多的东西。比如 lambda calculus 和 cek,secd 等抽象机 (abstract machine)。理解了这些概念之后,你就会发现所有的程序语言都可以比较容易的理解了。\n怎样写一个解释器 怎样写一个解释器\n待细读……\n解密设计模式 解密设计模式\n有些人问我,你说学习操作系统的最好办法是学习程序设计。那我们是不是应该学习一些“设计模式”(design patterns)?\n总的来说,如果光从字面上讲,程序里确实是有一些“模式”可以发掘的。因为你总是可以借鉴以前的经验,用来构造新的程序。你可以把这种经验叫做“模式”。\n可是自从《设计模式》(通常叫做 gof,“gang of four”,“四人帮”)这本书在 1994 年发表以来,“设计模式”这个词有了新的,扭曲的含义。它变成了一种教条,带来了公司里程序的严重复杂化以及效率低下。\n教条主义不能有!当然,gof 还是值得读的!\r照搬模式东拼西凑,而不能抓住事物的本质,没有“灵感”,其实是设计不出好东西的。\npeter norvig 在 1998 年就做了一个演讲,指出在“动态语言”里面,gof 的 20 几个模式,其中绝大部分都“透明”了。也就是说,你根本感觉不到它们的存在。\n谈 linux, windows 和 mac 谈 linux, windows 和 mac\n这段时间受到很多人的来信。他们看了我很早以前写的推崇 linux 的文章,想知道如何“抛弃 windows,学习 linux”。天知道他们在哪里找到那么老的文章,真是好事不出门…… 我觉得我有责任消除我以前的文章对人的误导,洗清我这个“linux 狂热分子”的恶名。\nhmm... 我也受这篇文章影响过……\r学习操作系统最好的办法是 学会(真正的)程序设计思想 ,而不是去“学习”各种古怪的工具。所有操作系统,数据库,internet,以至于 web 的设计思想(和缺陷),几乎都能用程序语言的思想简单的解释。\n一个好的工具,应该只有少数几条需要记忆的规则,就像象棋一样。\n这里其实是一个悖论,工具实现的其实是心中所想,而如果你要进行精细化的操作,就很难仅用很简单的规则就可以。\r有些人鄙视图形界面,鄙视 ide,鄙视含有垃圾回收的语言(比如 java),鄙视一切“容易”的东西。他们却不知道,把自己沉浸在别人设计的繁复的规则中,是始终无法成为大师的。\n容易的东西不一定是坏的,而困难的东西也不一定是好的。\n学习计算机(或者任何其它工具),应该“只选对的,不选难的”。记忆一堆的命令,乌七八糟的工具用法,最后脑子里什么也不会留下。 学习“原理性”的东西,才是永远不会过时的。\n谈语法 谈语法\n使用和研究过这么多程序语言之后,我觉得几乎不包含多余功能的语言,只有一个: scheme。\n我觉得 scheme (lisp) 的基于“s 表达式”(s-expression)的语法,是世界上最完美的设计。为什么我喜欢这样一个“全是括号,前缀表达式”的语言呢?这是出于对语言结构本质的考虑。\n其实,我觉得语法是完全不应该存在的东西。即使存在,也应该非常的简单。\n语法 其实只是对 语言的本质结构,“抽象语法树”(abstract syntax tree,ast) 的一种编码。一个良好的编码,应该极度简单,不引起歧义,而且应该容易解码。在程序语言里,这个“解码”的过程叫做“语法分析”(parse)。\n为什么我们却又需要语法呢?\n因为受到现有工具(操作系统,文本编辑器)的限制,到目前为止,几乎所有语言的程序都是用字符串的形式存放在文件里的。为了 让字符串能够表示“树”这种结构 ,人们才给程序语言设计了“语法”这种东西。\n但是人们喜欢耍小聪明,在有了基本的语法之后,他们开始在这上面大做文章,使得简单的问题变得复杂……\n语法糖?但有时候真的便捷易用啊……\r最老的是 fortran 的程序,最早的时候都是用打孔机打在卡片上的,所以它其实是几乎没有语法可言的。\nlisp (scheme 的前身)是世界上第二老的程序语言。\r自己想一下,如果要表达一颗“树”,最简单的编码方式是什么?就是用括号把每个节点的“数据”和“子节点”都括起来放在一起。lisp 的设计者们就是这样想的。他们把这种完全用括号括起来的表达式,叫做“s 表达式”(s 代表 “symbolic”)。这貌似很“粗糙”的设计,甚至根本谈不上“设计”。奇怪的是,在用过一段时间之后,他们发现自己已经爱上了这个东西,再也不想设计更加复杂的语法。于是 s 表达式就沿用至今。\n首先,把所有的结构都用括号括起来,轻松地避免了别的语言里面可能发生的“歧义”。程序员不再需要记忆任何“运算符优先级”。\n其次,把“操作符”全都放在表达式的最前面,使得基本算术操作和函数调用,在语法上发生 完美的统一 ,而且使得程序员可以使用几乎任何符号作为函数名。\n拓展阅读“前缀表达式”和“中缀表达式”相关知识。\r在其他的语言里,函数调用看起来像这个样子: f(1) ,而算术操作看起来是这样: 1+2 。在 lisp 里面,函数调用看起来是这样 (f 1) ,而算术操作看起来也是这样 (+ 1 2) 。你发现有什么共同点吗?那就是 f 和 + 在位置上的对应。实际上,加法在本质也是一个函数。这样做的好处,不但是突出了加法的这一本质,而且它让人可以用跟定义函数一模一样的方式,来定义“运算符”!这比起 c++ 的“运算符重载”强大很多,却又极其简单。\nlisp 的很多其它的设计,比如“垃圾回收”,后来被很多现代语言(比如 java)所借鉴。可是人们遗漏了一个很重要的东西:lisp 的语法,其实才是世界上最好的语法。\n我也感觉 s-expression 很好用!\r程序语言的常见设计错误 - \u0026gt; 片面追求短小 程序语言的常见设计错误 - \u0026gt; 片面追求短小\n我的程序的“短小”是建立在 语义明确,概念清晰 的基础上的。在此基础上,我力求去掉冗余的,绕弯子的,混淆的代码,让程序更加直接,更加高效的表达我心中设想的“模型”。这是一种在概念级别的优化,而程序的短小精悍只是它的一种“表象”。\n我的这种短小往往是在 语义和逻辑层面 的,而不是在语法上死抠几行代码。我绝不会为了程序显得短小而让它变得难以理解或者容易出错。\n文中举了两个小例子,这里只做简要概括。\r1.自增减操作\n从理论上讲, 自增减操作本身就是错误的设计 。因为它们把对变量的“读”和“写”两种根本不同的操作,毫无原则的合并在一起。这种对读写操作的混淆不清,带来了非常难以发现的错误。相反,一种等价的,“笨”一点的写法, i = i + 1 ,不但更易理解,而且在逻辑上更加清晰。\n有些人很在乎 i++ 与 ++i 的区别,去追究 i++ 与 ++i 谁的效率更高。这些其实都是徒劳的。比如,i++ 与 ++i 的效率差别,其实来自于早期 c 编译器的愚蠢。\n以下是正确讲解 i++ 的办法!!!它曾是多少人的痛……\r因为 i++ 需要在增加之后返回 i 原来的值 ,所以它其实被编译为:\n(tmp = i, i = i + 1, tmp) 但是在 =for (int i = 0; i \u0026lt; max; i++)= 中,其实你并不需要在 =i++= 之后得到它自增前的值。所以有人说,在这里应该用 =++i= 而不是 =i++= ,否则你就会浪费一次对中间变量 =tmp= 的赋值。\n而其实呢,一个良好设计的编译器应该在两种情况下都生成相同的代码。\n# 在 i++ 的情况,代码其实先被转化为\rfor (int i = 0; i \u0026lt; max; (tmp = i, i = i + 1, tmp))\r# ↓↓↓\r# 由于 tmp 这个临时变量从来没被用过,\r# 所以它会被编译器的“dead code elimination”消去,\r# 编译器最后实际上得到了\rfor (int i = 0; i \u0026lt; max; i = i + 1) 所以,“精通”这些细微的问题,并不能让你成为一个好的程序员。很多人所认为的高明的技巧,经常都是因为早期系统设计的缺陷所致。一旦这些系统被改进,这些技巧就没什么用处了。\n真正正确的做法其实是:完全不使用自增减操作,因为它们本来就是错误的设计。\n2.赋值语句返回值\n在几乎所有像 c,c++,java 的语言里,赋值语句都可以被作为值。\ny = 0 不应该具有一个值。它的作用应该是“赋值”这种“动作”,而不应该具有任何“值”。即使牵强一点硬说它有值,它的值也应该是 void 。这样一来 x = y = 0 和 if (y = 0) 就会因为“类型不匹配”而被编译器拒绝接受,从而避免了可能出现的错误。\nif (y == 0) 写成 if (y = 0) ,hmm……\r“解决问题”与“消灭问题” “解决问题”与“消灭问题”\n如果你仔细观察就会发现,很多“难题”,其实是“人造”出来的,而不是“必然”的。它们的存在,往往是由于一些早期的“设计错误”。\n如果我们转换一下思路,或者改变一下“设计”,很多问题就可以不解自消。这就是我所谓的“消灭问题”的能力。\n所以,在解决问题之前,我们应该先问自己三个问题:\n这问题是否真的“存在”? 如果解决了这个问题,会给我和他人在合理的时间之内带来什么实际的好处? 这问题是否可以在简单的改变某些“设计”或者“思路”之后,不复存在? 本质上是认真思考一下问题产生的原因!\rlisp 已死,lisp 万岁! lisp 已死,lisp 万岁!\n1.lisp 的优点:\nlisp 的语法是世界上最精炼,最美观,也是语法分析起来最高效的语法。这是 lisp 独一无二的,其他语言都没有的优点。有些人喜欢设计看起来很炫的语法,其实都是自找麻烦。为什么这么说呢,请参考这篇《谈语法》 。\nlisp 是第一个可以 在程序的任何位置定义函数 ,并且可以 把函数作为值传递 的语言。这样的设计使得它的表达能力非常强大。这种理念被 python,javascript,ruby 等语言所借鉴。\nlisp 有世界上最强大的宏系统(macro system)。这种宏系统的表达力几乎达到了理论所允许的极限。如果你只见过 c 语言的“宏”,那我可以告诉你它是完全没法跟 lisp 的宏系统相提并论的。\nlisp 是世界上第一个使用垃圾回收( garbage collection)的语言。这种超前的理念,后来被 java,c# 等语言借鉴。\n想不到吧,现代语言的很多优点,其实都是来自于 lisp — 世界上第二古老的程序语言。所以有人才会说,每一种现代语言都在朝着 lisp 的方向“进化”。\n如果你相信了这话,也许就会疑惑,为什么 lisp 今天没有成为主流?为什么 lisp machine 会被 unix 打败?其实除了商业原因之外,还有技术上的问题。\n2.dynamic scoping\n早期的 lisp 其实普遍存在一个非常严重的问题:它使用 dynamic scoping 。\n所谓 dynamic scoping 就是说,如果你的函数定义里面有 “自由变量” ,那么这个自由变量的值,会随着函数的“调用位置”的不同而发生变化。\n(setq f (let ((x 1)) (lambda (y) (* x y)))) 这里的 x 对于函数 (lambda (y) (* x y)) 来说就是个“自由变量”(free variable),因为它不是它的参数。\n……\ntips: 详细论证过程就参考原文……\n话说回来,为什么早期的 lisp 会使用 dynamic scoping 呢?\n原来,emacs lisp 直接把函数定义处的 s 表达式 '(lambda (y) (* x y)) 作为了函数的“值”!\n如果你在 emacs 里面显示 f 的值,它会打印出:\n\u0026#39;(lambda (y) (* x y)) 这说明 f 的值其实是一个 s 表达式,而不是像 scheme 一样的“闭包”(closure)。\n简单倒是简单,麻烦事接着就来了。调用 f 的时候,比如 (funcall f 2) ,y 的值当然来自参数 2,可是 x 的值是多少呢?答案是:不知道!不知道怎么办?到“外层环境”去找呗,看到哪个就用哪个,看不到就报错。所以你就看到了之前出现的现象,函数的行为随着一个完全无关的变量而变化。如果你单独调用 (funcall f 2) 就会因为找不到 x 的值而出错。\n那么正确的实现函数的做法是什么呢?是制造“闭包”(closure)!这也就是 scheme,common lisp 以及 python,c# 的做法。\n在函数定义被解释或者编译的时候,当时的自由变量(比如 x)的值,会跟函数的代码绑在一起,被放进一种叫做“闭包”的结构里。比如上面的函数,就可以表示成这个样子: (closure ‘(lambda (y) (* x y)) ‘((x . 1))) 。\n在这里我用 (closure …) 表示一个“结构”(就像 c 语言的 struct)。它的第一个部分,是这个函数的定义。第二个部分是 ‘((x . 1)) ,它是一个 “环境” ,其实就是一个从变量到值的映射(map)。利用这个映射,我们记住函数定义处的那个 x 的值,而不是在调用的时候才去瞎找。\n3.lexical scoping\n与 dynamic scoping 相对的就是“lexical scoping”。我刚才告诉你的闭包,就是 lexical scoping 的实现方法。\n你也许发现了,lisp 其实不是一种语言,而是很多种语言。这些被人叫做“lisp 家族”的语言,其实共同点只是它们的“语法”:它们都是基于 s 表达式。如果你因此对它们同样赞美的话,那么你赞美的其实只是 s 表达式,而不是这些语言本身。\n因为 一个语言的本质应该是由它的语义决定的,而跟语法没有很大关系。 你甚至可以给同一种语言设计多种不同的语法,而不改变这语言的本质。\nchez scheme 的传说 chez scheme 的传说\n在我看来,早期 lisp 编译器出现的主要问题,其实在于对编译的本质的理解,以及编译器与解释器的根本区别。\n解释器之所以大部分时候比编译器慢,是因为解释器“问太多的问题”。 每当看到一个构造,解释器就会问:“这是一个整数吗?”“这是一个字符串吗?”“这是一个函数吗?”…… 然后根据问题的结果进行不同的处理。这些问题,在编译器的理论里面叫做 “解释开销” (interpretive overhead)。\n编译的本质,其实就是在程序运行之前进行“静态分析”,试图一劳永逸的回答这些问题。\n早期的 lisp 编译器,以及现在的很多 scheme 编译器出现的问题其实在于,它们并没有干净的消除这些问题,甚至根本没有消除这些问题。\n编译的过程,就是将输入程序经过一系列的变换之后,转化为机器代码。\n什么是“脚本语言” 什么是“脚本语言”\n其实“脚本语言”与“非脚本语言”并没有语义上,或者执行方式上的区别。它们的区别只在于它们设计的初衷:脚本语言的设计,往往是作为一种临时的“补丁”。相反,“非脚本”的通用程序语言,往往由经过严格训练的专家甚至一个小组的专家设计,它们从一开头就考虑到了“通用性”,以及在大型工程中的可靠性和可扩展性。\n“脚本”这个概念是如何产生的?\n使用 unix 系统的人都会敲入一些命令,而命令貌似都是“一次性”或者“可抛弃”的。然而不久,人们就发现这些命令其实并不是那么的“一次性”,自己其实一直在重复的敲入类似的命令,所以有人就发明了“脚本”这东西。它的 设计初衷是“批量式”的执行命令 ,你在一个文件里把命令都写进去,然后执行这个文件。可是不久人们就发现,这些命令行其实可以用更加聪明的方法构造,比如定义一些变量,或者根据系统类型的不同执行不同的命令。于是,人们为这脚本语言加入了变量,条件语句,数组,等等构造。“脚本语言”就这样产生了。\nscheme 编程环境的设置 scheme 编程环境的设置\n学习 sheme 的时候可以参考下……\r关于语言的思考 关于语言的思考\n怎么说呢,我觉得每个程序员的生命中都至少应该有几个月在静心学习 haskell。学会 haskell 就像吃几天素食一样。每天吃素食显然会缺乏全面的营养,但是每天都吃荤的话,你恐怕就永远意识不到身体里的毒素有多严重。\n我今天想说其实就是,没有任何一种语言值得你用毕生的精力去“精通”它。“精通”其实代表着“脑残”——你成为了一个高效的机器,而不是一个有自己头脑的人。你必须对每种语言都带有一定的怀疑态度,而不是完全的拥抱它。 每个人都应该学习多种语言 ,这样才不至于让自己的思想受到单一语言的约束,而没法接受新的,更加先进的思想。这就像每个人都应该学会至少一门外语一样,否则你就深陷于自己民族的思维方式。有时候这种民族传统的思想会让你深陷无须有的痛苦却无法自拔。\n原因与证明 原因与证明\n一个东西具有如此的性质,并不是因为你证明了它。这性质是它天生就有的,不管你是否能证明它。\n了大部分的教育过分的重视了“证明”,却忽略了比证明更重要的东西——“原因”。\n原因往往比证明来得更加简单,更加深刻,但却更难发现。 对于一个事实往往有多种多样的证明,然而导致这个事实的原因却往往只有一个。如果你只知道证明却不知道原因,那你往往就被囚禁于别人制造的理论里面,无法自拔。你能证明一个事物具有某种特性,然而你却没有能力改变它。你无法对它加入新的,好的特性,也无法去掉一个不好的特性。你也无法发明新的理论。有能力发明新的事物和理论的人,他们往往不仅知道“证明”,而且知道“原因”。\n古人说的“知其然”与“知其所以然”的区别,也就是同样的道理吧。\n丘奇和图灵 丘奇和图灵\n丘奇代表了“逻辑”和“语言”,而图灵代表着“物理”和“机器”。完全投靠丘奇,或者完全投靠图灵,貌似都是错误的做法。\n据我的经验,丘奇的理论让很多事情变得简单,而图灵的机器却过度的复杂。丘奇所发明的 lambda calculus 以及后续的工作,是几乎一切程序语言的理论基础。\n图灵机永远的停留在了理论的领域,绝大多数被用在“计算理论”(theory of computation)中。\n计算理论其实包括两个主要概念: “可计算性理论”(computability) 和 “复杂度理论”(complexity) 。\n这两个概念在通常的计算理论书籍(比如 sipser 的经典教材)里,都是用图灵机来叙述的。其实几乎所有计算理论的原理,都可以用 lambda calculus ,或者程序语言和解释器的原理来描述。\n所谓“通用图灵机”(universal turing machine),其实就是一个 可以解释自己的解释器 ,叫做“元解释器”(meta-circular interpreter)。\n然而我的“元解释器”却是基于 lambda calculus 的,所以我后来发现了一种方法,可以完全的用 lambda calculus 来解释计算理论里面几乎所有的定理。\n在我的头脑里面并存着丘奇和图灵的影子。我觉得丘奇的 lambda calculus 是比图灵机简单而强大的描述工具,然而我却又感染到了图灵对于“物理”和“机器”的执着。我觉得逻辑学家们对 lambda calculus 的解释过于复杂,而通过把它理解为物理的“电路元件”,让我对 lambda calculus 做出了更加简单的解释,把它与“现实世界”联系在了一起。\n所以到最后,丘奇和图灵这两种看似矛盾的思想,在我的脑海里得到了和谐的统一。这些精髓的思想帮助我解决了许多的问题。\n我和权威的故事 我和权威的故事\ndonald knuth\n有一句话说得好:“跟真正的大师学习,而不是跟他们的徒弟。”如果你真的要学一个算法,就应该直接去读那算法的发明者的论文,而不是转述过来的“二手知识”。二手的知识往往把发明者原来的动机和思路都给去掉了,只留下苍白无味,没有什么启发意义的“最后结果”。\n我跟 knuth 的最后一次“联系”是在我就要离开清华的时候。我从 email 告诉他我觉得中国的研究环境太浮躁了,不是做学问的好地方,想求点建议。结果他回纸信说:“可我为什么看到中国学者做出那么多杰出的研究?计算机科学不是每个人都可以做的。如果你试了这么久还不行,那说明你注定不是干这行的料。”还好,我从来没有相信他的这段话,我下定了决心要证明这是错的。多年的努力还真没有白费,今天我可以放心的说,knuth 你错了,因为我已经在你引以为豪的多个方面超过了你。\nhmm……\runix\n所谓的“unix 哲学”,也就是进程间通信主要依靠无结构字符串,造成了一大批过度复杂,毛病众多的工具和语言的产生: awk,sed,perl,……\nlisp 程序员早就明白这个道理,所以他们尽一切可能避免使用字符串。他们设计了 s 表达式,用于结构化的传输数据。实际上 s 表达式不是“设计”出来的,它是每个人都应该首先想到的,最简单的可以 表示树结构 的编码方法。lisp 的设计原则里面有一条就是:do not encode。它的意思是,尽量不要把有用的数据编码放进字符串。\ngo 语言\n……\ncornell\n……\n图灵奖\n说到这里应该有人会问这个问题,我是不是也属于那种没找到导师走投无路的人。答案是,对的,我确实没有在 cornell 找到可以做我导师的人。\n……\n再见了,权威们\n几经颠簸的求学生涯,让我获得了异常强大的力量。我的力量不仅来自于老师们的教诲,而且在于我自己不懈的追求,因为机会只亲睐有准备的头脑。\n王垠指出了现存的许多问题和弊端,也提出了许多改进和设想,然而只到现在也没有一件可用的…… 我倒觉得他应该听 knuth 的,如他现在这样,开班教学也不错。\r程序语言与…… 程序语言与……\n程序语言的设计类似于其它很多东西的设计,有些微妙的地方只有用过更好的设计的人才能明白。\n……\n程序语言与减肥\n我的方法就是一句话:让每天吃进去的热量比消耗的少一些,但是不至于难受,另外适当运动来增加热量的消耗。很显然嘛,根据热力学定律,每天消耗的能量比摄入的多,多出来的部分只能通过分解你身上的物质(脂肪)来产生。\n程序员的心理疾病 程序员的心理疾病\n1.无自知之明\n由于程序员的工作最近几年比较容易找,工资还不错,所以很多程序员往往只看到自己的肚脐眼,看不到自己在整个社会里的位置其实并不是那么的关键和重要。很多程序员除了自己会的那点东西,几乎对其它领域和事情完全不感兴趣,看不起其他人……\n这一点确实需要警醒!!!\r2.垃圾当宝贝\n按照 dijkstra 的说法,“软件工程”是穷途末路的领域,因为它的目标是: 如果 我不会写 程序的话,怎么样才 能写出 程序?\n为了达到这个愚蠢的目的,很多人开始兜售各种像减肥药一样的东西。面向对象方法,软件“重用”,设计模式,关系式数据库,nosql,大数据…… 没完没了。\n不赞同!\r3.宗教斗争\n为什么有人说在软件行业里需要不停地“学习”,因为不断地有人为了制造新的理念而制造新的理念。\n……\n4. 以语言取人\n很多程序员都以自己会用最近流行的一些新语言为豪,以为有了它们自己就成了更好的程序员。他们看不到,用新的语言并不能让他们成为更好的程序员。其实最厉害的程序员无论用什么语言都能写出很好的代码。在他们的头脑里其实只有一种很简单的语言,他们首先用这种语言把 问题建模 出来,然后根据实际需要“翻译”成最后的代码。这种在头脑里的建模过程的价值,是很难用他最后用语言的优劣来衡量的。\n……\n一个对 dijkstra 的采访视频 一个对 dijkstra 的采访视频\n(可以访问 youtube 或者从源地址下载 mpeg1,300m)\n现在看来,任何一个语言里面没有递归函数都是不可思议的事情,然而在 1950-60 年代的时候,居然很少有人知道它有什么用!所以你就发现,所谓的“主流”和“大多数人”一直都是比较愚蠢的。现在,同样的故事发生在 lambda 身上。多年以后,没有 lambda 的语言将是不可接受的。\n在这里只摘录他提到的几个要点:\n软件测试可以确定软件里有 bug,但却不可能用来确定它们没有 bug。\n程序的优雅性不是可以或缺的奢侈品,而是决定成功还是失败的一个要素。优雅并不是一个美学的问题,也不是一个时尚品味的问题,优雅能够被翻译成可行的技术。牛津字典对 elegant 的解释是: pleasingly ingenious and simple。如果你的程序真的优雅,那么它就会容易管理。第一是因为它比其它的方案都要短,第二是因为它的组件都可以被换成另外的方案而不会影响其它的部分。很奇怪的是,最优雅的程序往往也是最高效的。\n为什么这么少的人追求优雅?这就是现实。如果说优雅也有缺点的话,那就是 你需要艰巨的工作才能得到它,需要良好的教育才能欣赏它 。\n当没有计算机的时候,编程不是问题。当有了比较弱的计算机时,编程成了中等程度的问题。现在我们有了巨大的计算机,编程就成了巨大的问题。\n我最开头编程的日子跟现在很不一样,因为我是给一个还没有造出来的计算机写程序。造那台机器的人还没有完工,我在同样的时间给它做程序,所以没有办法测试我的代码。于是我发现自己做的东西必须要能放进自己的脑子里。\n我的母亲是一个优秀的数学家。有一次我问她几何难不难,她说一点也不难,只要你用“心”来理解所有的公式。如果你需要超过 5 行公式,那么你就走错路了。\n学术腐败是历史的必然 学术腐败是历史的必然\n学术腐败是历史的必然,是人类历史的发展趋势和技术进步的结果。\n为什么这么说呢?\n首先想想在资本主义社会里人靠什么过活?钱 一般人怎么得到钱?工作 谁是人最大的工作竞争对手?机器,电脑,互联网,机器人…… 自己的工作被机器取代了怎么办?寻找机器干不了的工作! 什么是机器仍然干不了,而且不久的将来也干不了的工作?搞研究! 搞研究是为了什么?制造更高效更智能的机器! 然后你就明白了,这是一个让人类越来越痛苦的怪圈。\n关系式模型的实质 关系式模型的实质\n……\n谈创新 谈创新\n有人告诉我,我所说的很多事情只是在已有的事物上面挑出毛病来,那不能引起真正的“创新”。\n什么是创新?创新真的那么重要吗,它的意义何在?\n世界上并不缺少创新,而是创新过剩了!大量的所谓“创新”,让人们的生活变得纷繁复杂,导致他们需要记住更多事物的用法,而无法专注于利用已有的设施,最大限度的享受生活的乐趣。\n最缺乏创造力的人,往往是最爱标榜创新的。\n创新往往也是与良好的设计理念背道而驰的。一个好的设计,总是力求减少“新”的感觉,而着重于让整个设计浑然一体,天衣无缝,用起来顺手。最好的设计就是让设计的目标消失掉,或者感觉不到它的存在。\n这里王垠有些偷换概念了……\r……\n美国和中国\n在这里提到美国的优秀设计,并不是说我更喜欢美国。每次提到这些,总有朋友感觉不平,仿佛觉得我是“美帝的走狗”一样。 我其实对任何国家都没有特别的感情和归属感,我的感情只针对个人,而不是国家。实际上,我认为国家这种东西是不必要存在的。 美国人对我显然没有很多中国人对我好,然而 技术和设计是没有国界的 ,好的东西不学就等于永远落后。很多中国人喜欢用所谓的“民族自豪感”来代替理性的思考,看不到自己的问题。中国为什么到现在还属于第三世界国家,恐怕就有这里面的原因。没有用心,就不能提高。中国的经济发展了,国家的总资产可以说已经很多了,然而有很多东西不是钱就可以买来的,它需要用心设计。看,我在美国受了这么多的苦和委屈才学会了这些,如果你们不理解消化,那多可惜啊。\n你的朋友说的是对的!!!都 2021 了,还持有这种思想(“技术和设计是没有国界的”)的人,在我看来不是坏透了,就是个傻b ……\r一味的试图创新而不仔细思考,是人们的生活由于各种“新事物”而变得复杂的重要原因。\n只有你能从已有的东西里面看到实质的问题,你才有可能达到天衣无缝的设计。设计不需要全新的,它必须最大限度的让人可以方便的生活,而不需要记忆很多不必要的指令。否则如果你不吸取历史的教训,做出所谓“全新”的设计,那么你很有可能不是解决了问题,而是制造了问题。我觉得有一句话说得好,忘记历史就是毁灭未来。\n所谓“人为错误” 所谓“人为错误”\n在我看来,整个软件行业基本就是建立在一堆堆的设计失误之上。做程序员如此困难和辛苦,大部分原因就是因为软件系统里面积累了大量前人的设计失误,所以我们需要做大量的工作来弥补或者绕过。\n然而一般程序员都没有意识到这里面的设计错误,知道了也不敢指出来,他们反而喜欢显示自己死记硬背得住这些稀奇古怪的规则。这就导致了软件行业的“皇帝的新装现象”——没有人敢说工具的设计有毛病,因为如果你说出来,别人就会认为你在抱怨,那你不是经验不足,就是能力不行。\n我体会很深的一个例子就是 git 版本控制工具。有人很把这种东西当回事,引以为豪记得住如何用一些稀奇古怪的 git 命令(比如 git rebase, git submodule 之类)。好像自己知道了这些就真的是某种专家一样,每当遇到不会用这些命令的人,都在心底默默地鄙视他们。 作为一个比 git 的作者还要高明的程序员,我却发现自己永远无法记住那些命令 。在我看来,这些命令晦涩难懂,很有可能是因为没设计好造成的。因为如果一个东西设计好了,以我的能力是不可能不理解的。可是 linus torvalds 的名气之大,威望之高,有谁敢说:“我就是不会用你设计的破玩意儿!你把我怎么着?\nhmm…… 如果你高明到让自己并不高明,是不是说明你并不如自己想象的那么高明……\r怎样尊重一个程序员 怎样尊重一个程序员\n1.认识和承认技术领域的历史遗留糟粕\n很多不尊重人现象的起源,都是因为某些人偏执的相信某种技术就是世界上最好的,每个人都必须知道这些东西,否则他就不是一个合格的程序员。\n如果你对计算机科学理解到一定程度,就会发现我们其实仍然生活在计算机的石器时代。特别是软件系统,建立在一堆历史遗留的糟糕设计之上。\n各种蹩脚脑残的操作系统(比如 unix,linux),程序语言(比如 c++,javascript,php,go),数据库,编辑器,版本控制工具,…… 时常困扰着我们,这就是为什么你需要那么多的所谓“经验”和“知识”。\n2.分清精髓知识和表面知识,不要太拿经验当回事\n在任何领域,都只有少数知识是精髓的,另外大部分都是表面的,肤浅的,是从精髓知识衍生出来的。\n精髓知识和表面知识都是有用的,然而它们的分量和重要性却是不一样的。所以必须区分精髓知识和表面知识,不能混为一谈,对待它们的态度应该是不一样的。由于表面知识基本是死的,而且很容易从精髓知识推导衍生出来。我们不应该因为自己知道很多表面知识,就自以为比掌握了精髓知识的人还要强。不应该因为别人不知道某些表面知识,就以为自己高人一等。\n……\n编程的宗派 编程的宗派\n总是有人喜欢争论这类问题,到底是“函数式编程”(fp)好,还是“面向对象编程”(oop)好……\n1.面向对象编程(object-oriented programming)\n如果你看透了表面现象就会发现,其实“面向对象编程”本身没有引入很多新东西。\n所谓“面向对象语言”,就是经典的“过程式语言”(比如 pascal),加上一点抽象能力。所谓“类”和“对象”,基本是过程式语言里面的记录(record,或者叫结构,structure),它 本质其实是一个从名字到数据的“映射表”(map) 。\n你可以用名字从这个表里面提取相应的数据。\n所谓“对象思想”(区别于“面向对象”),实际上就是对这种数据访问方式的进一步抽象。\n“对象思想”的价值,它让你可以通过“间接”(indirection,或者叫做 “抽象” )来 改变 =point.x= 和 =point.y= 的语义,从而让使用者的代码 完全不用修改 。虽然你的实际数据结构里面 可能没有 x 和 y 这两个成员,但由于 =.x= 和 =.y= 可以被重新定义 ,所以你可以通过改变 .x 和 .y 的定义来“模拟”它们。在你使用 =point.x= 和 =point.y= 的时候,系统内部其实在运行两片代码(所谓 getter),它们的作用是从 r 和 angle 计算出 x 和 y 的值。这样你的代码就感觉 x 和 y 是实际存在的成员一样,而 其实它们是被临时算出来的 。\n对象思想的价值也就到此为止了。你见过的所谓“面向对象思想”,几乎无一例外可以从这个想法推广出来。\n“对象思想”作为数据访问的方式,是有一定好处的。然而“面向对象”(多了“面向”两个字),就是把这种本来良好的思想东拉西扯,牵强附会,发挥过了头。\n很多面向对象语言号称“所有东西都是对象”(everything is an object), 把所有函数都放进所谓对象里面,叫做“方法”(method),把普通的函数叫做“静态方法”(static method) 。\n实际上呢,就像我之前的例子,只有极少需要抽象的时候,你需要使用内嵌于对象之内,跟数据紧密结合的“方法”。其他的时候,你其实只是想表达数据之间的变换操作,这些完全可以用普通的函数表达,而且这样做更加简单和直接。\n这种把所有函数放进方法的做法是本末倒置的,因为函数并不属于对象。 绝大部分函数是独立于对象的,它们不能被叫做“方法”。强制把所有函数放进它们本来不属于的对象里面,把它们全都作为“方法”,导致了面向对象代码逻辑过度复杂。\n面向对象语言不仅有自身的根本性错误,而且由于面向对象语言的设计者们常常是半路出家,没有受到过严格的语言理论和设计训练却又自命不凡,所以经常搞出另外一些奇葩的东西。比如在 javascript 里面,每个函数同时又可以作为构造函数(constructor),所以每个函数里面都隐含了一个 this 变量,你嵌套多层对象和函数的时候就发现没法访问外层的 this,非得“bind”一下。python 的变量定义和赋值不分,所以你需要访问全局变量的时候得用 global 关键字,后来又发现如果要访问“中间层”的变量,没有办法了,所以又加了个 nonlocal 关键字……\n有些人问我为什么有些语言设计成那个样子,我只能说,很多语言设计者其实根本不知道自己在干什么。\n2.函数式编程(functional programming)\n有人盲目的相信函数式编程能够奇迹般的解决并发计算的难题,而看不到实质存在的,独立于语言的问题。\n函数式编程当然提供了它自己的价值。函数式编程相对于面向对象最大的价值,莫过于对于函数的正确理解。\n在函数式语言里面,函数是“一类公民”(first-class)。它们可以像 1, 2, “hello”,true,对象…… 之类的“值”一样,在任意位置诞生,通过变量,参数和数据结构传递到其它地方,可以在任何位置被调用。这些是很多过程式语言和面向对象语言做不到的事情。\n很多所谓“面向对象设计模式”(design pattern),都是因为面向对象语言没有 first-class function,所以导致了 每个函数必须被包在一个对象里面才能传递到其它地方 。\n函数式编程的另一个贡献,是它们的类型系统。\n函数式语言对于类型的思维,往往非常的严密。函数式语言的类型系统,往往比面向对象语言来得严密和简单很多,它们可以帮助你对程序进行严密的逻辑推理。然而类型系统一是把双刃剑,如果你对它看得太重,它反而会带来不必要的复杂性和过度工程。\n3.符号必须简单的对世界建模\n在我的心目中其实只有一个概念,它叫做“编程”(programming),它不带有任何附加的限定词(比如“函数式”或者“面向对象”)。我研究的领域称叫做“programming languages”,它研究的内容不局限于某一个语言,也不局限于某一类语言,而是所有的语言。在我的眼里, 所有的语言都不过是各个特性的组合 。所以最近出现的所谓“新语言”,其实不大可能再有什么真正意义上的创新。我不喜欢说“发明一个程序语言”,不喜欢使用“发明”这个词,因为不管你怎么设计一个语言,所有的特性几乎都早已存在于现有的语言里面了。我更喜欢使用“设计”这个词,因为虽然一个语言没有任何新的特性,它却有可能在细节上更加优雅。\n编程最重要的事情,其实是让写出来的符号,能够简单地对实际或者想象出来的“世界”进行建模。\n一个程序员最重要的能力,是直觉地看见符号和现实物体之间的对应关系。不管看起来多么酷的语言或者范式,如果必须绕着弯子才能表达程序员心目中的模型,那么它就不是一个很好的语言或者范式。\n关于建模的另外一个问题是,你心里想的模型,并不一定是最好的,也不一定非得设计成那个样子。\n有些人心里没有一个清晰简单的模型,觉得某些语言“好用”,就因为它们能够对他那种扭曲纷繁的模型进行建模。所以你就跟这种人说不清楚,为什么这个语言不好,因为显然这个语言对他是有用的!\n所谓软件工程 所谓软件工程\n这是一个比较有趣的话题,后续可以了解一下。\r有人把软件工程领域的本质总结为:“how to program if you cannot?”(如果你不会编程,那么你如何编程?)我觉得这句话说得很好,因为我发现软件工程这整个领域,基本就是吹牛扯淡卖“减肥药”的。软件行业的大部分莫名其妙的愚昧行为,很多是由所谓“软件工程专家”发明的。\n打破软件工程幻觉的一个办法,就是实地去看看“专家”们用自己的方法论做出了什么好东西。你会惊奇的发现,这些提出各种新名词的所谓“专家”,几乎都是从不知道什么旮旯里冒出来的民科。他们跟真正的计算机科学家或者高明的程序员没有任何关系,也没有做出过什么有技术含量的东西,他们根本没有资格对别人编程的方式做出指导。这些人做出来少数有点用的东西(比如 junit),其实非常容易,以至于每个初学编程的人都应该做得出来。一个程序员见识需要低到什么程度,才会在乎这种人说的话?\n可世界上就是有这样划算的行当,虽然写不出好的代码,对计算的理解非常肤浅,却可以通过嘴里说说,得到评价别人“代码质量”的权力,占据软件公司的管理层位置。久而久之,别人还以为他们是什么泰斗。你仔细看过提出 design pattern 的“四人帮”(gof),做出过什么有实质价值的东西吗?提出“dry principle”的作者,做出过什么吗?再看看 agile,pair programming,tdd 的提出者?他们其实不懂很多编程,写出文章和书来也是极其肤浅。\ndry 原则的误区 dry 原则的误区\n简言之,dry(don\u0026rsquo;t repeat yourself)原则鼓励对代码进行抽象,但是鼓励得过了头\n1.抽象与可读性的矛盾\n代码的“抽象”和它的“可读性”(直观性),其实是一对矛盾的关系。适度的抽象和避免重复是有好处的,它甚至可以提高代码的可读性,然而如果你尽“一切可能”从代码里提取模板,甚至把一些微不足道的“共同点”也提出来进行“共享”,它就开始有害了。\n这是因为, 模板并不直接显示在“调用”它们的位置 。提取出模板,往往会使得阅读代码时不能一目了然。如果由此带来的直观性损失超过了模板所带来的好处时,你就应该考虑避免抽象了。\n2.抽象的时机问题\n抽象的思想,关键在于“发现两个东西是一样的”。然而很多时候,你开头觉得两个东西是一回事,结果最后发现,它们其实只是肤浅的相似,而本质完全不同。 防止过早抽象 的方法其实很简单,它的名字叫做“等待”。\n谈程序的正确性 谈程序的正确性\n100% 可靠的代码,这是多么完美的理想!然而它并不存在!!!\n1.衡量程序最重要的标准\n许多人其实不明白一个重要的道理: 你得先写出程序,才能开始谈它的正确性 。看一个程序好不好,最重要的标准,是看它能否有效地解决问题,而不是它是否正确。如果你的程序没有解决问题,或者解决了错误的问题,或者虽然解决问题但却非常难用,那么这程序再怎么正确,再怎么可靠,都不是好的程序。\n正确不等于简单,不等于优雅,不等于高效。一个不简单,不优雅,效率低的程序,就算你费尽周折证明了它的正确,它仍然不会很好的工作。\n2.如何提高程序的正确性\n话说回来,虽然程序的正确性相对于解决问题,处于相对次要的地位,然而它确实是不可忽视的。\n如果你深入研究过程序的逻辑推导就会知道,测试和形式化证明的能力都是非常有限的。\n那么提高程序正确性最有效的方法是什么呢?在我看来,最有效的方法莫过于对代码反复琢磨推敲,让它变得简单,直观,直到你一眼就可以看得出它不可能有问题。\n具体如何做呢?\r对 parser 的误解 对 parser 的误解\n1. 什么是 parser\n所谓 parser,一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。\n最常见的 parser,是把程序文本转换成编译器内部的一种叫做“抽象语法树”(ast)的数据结构。也有简单一些的 parser,用于处理 csv,json,xml 之类的格式。\n之所以需要做这种从字符串到数据结构的转换,是因为编译器是无法直接操作“1+2”这样的字符串的。实际上, 代码的本质根本就不是字符串 ,它本来就是一个具有复杂拓扑的数据结构,就像电路一样。“1+2”这个 字符串只是对这种数据结构的一种“编码” ,就像 zip 或者 jpeg 只是对它们压缩的数据的编码一样。\n这种编码可以方便你把代码存到磁盘上,方便你用文本编辑器来修改它们,然而你必须知道,文本并不是代码本身。 所以从磁盘读取了文本之后,你必须先“解码”,才能方便地操作代码的数据结构。\n对于程序语言,这种解码的动作就叫做 parsing ,用于解码的那段代码就叫做 parser 。\n2.parser 在编译器中的地位\n那么貌似这样说来,parser 是编译器里面很关键的一个部分了?显然,parser 是必不可少的,然而它并不像很多人想象的那么重要。parser 的重要性和技术难度,被很多人严重的夸大了。\n我喜欢把 parser 称为“万里长征的第 0 步”,因为等你 parse 完毕得到了 ast,真正的编译技术才算开始。\n一个编译器包含许多的步骤:语义分析,类型检查/推导,代码优化,机器代码生成,…… 这每个步骤都是在对某种中间数据结构(比如 ast )进行分析或者转化,它们完全不需要知道代码的字符串形式。也就是说,一旦代码通过了 parser,在后面的编译过程里,你就可以完全忘记 parser 的存在。\nparser 虽然必不可少,然而它比起编译器里面最重要的过程,是处于一种辅助性的地位。\nast 数据结构才是程序本身,而程序的文本只是这种数据结构的一种编码形式。\n3.parser 技术发展的误区\n很多人盲目地设计复杂的语法,然后用越来越复杂的 parser 技术去 parse 它们,这就是 parser 技术仍然在发展的原因。\n制造复杂难懂的语法,没有什么真正的好处。不但给程序员的学习造成了不必要的困难,让代码难以理解,而且也给 parser 的作者带来了严重的挑战。\n4.编译原理课程的误导\n一般大学里上编译原理课,都是捧着一本大部头的“龙书”或者“虎书”,花掉一个学期 1/3 甚至 2/3 的时间来学写 parser。由于 parser 占据了大量时间,以至于很多真正精华的内容都被一笔带过:语义分析,代码优化,类型推导,静态检查,机器代码生成,…… 以至于很多人上完了编译原理课程,记忆中只留下写 parser 的痛苦回忆。\n我从来就不认为自己是“编译器”专业的,我认为自己是“pl 专业”。编译器领域照本宣科成分更多一些,pl 专业更加注重本质的东西。\n如果你想真的深入理解编译理论,最好是从 pl 课程的读物,比如 eopl 开始。\n我可以说 pl 这个领域,真的和编译器的领域很不一样。请不要指望编译器的作者(比如 llvm 的作者)能够设计出好的语言,因为他们可能根本不理解很多语言设计的东西,他们只是会实现某些别人设计的语言。可是反过来,理解了 pl 的理论, 编译器的东西只不过是把一种语言转换成另外一种语言(机器语言)而已 。工程的细枝末节很麻烦,可是当你掌握了精髓的原理,那些都容易摸索出来。\n5.我写 parser 的心得和秘诀\n很多人都觉得写 parser 很难,一方面是由于语言设计的错误思想导致了复杂的语法,另外一方面是由于人们对于 parser 构造过程的思维误区。很多人不理解 parser 的本质和真正的用途,所以他们总是试图让 parser 干一些它们本来不应该干的事情,或者对 parser 有一些不切实际的标准。当然,他们就会觉得 parser 非常难写,非常容易出错。\n……\n所以你看到了,parser 并不是编译器,它甚至不属于编译里很重要的东西。\nparser 的研究其实是在解决一些根本不存在或者人为制造的问题。复杂的语法导致了复杂的 parser 技术,它们仍然在给计算机世界带来不必要的困扰和麻烦。对 parser 写法的很多误解,过度工程和过早优化,造成了很多人错误的高估写 parser 的难度。\n图灵的光环 图灵的光环\n王垠的博文写的真长,一定读了不少的书,并且作了验证(对错暂不论),就这一点也是非常值得学习的。\r编程的智慧 编程的智慧\n编程是一种创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的 练习和领悟 ,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。\n1.反复推敲代码* 有些人喜欢炫耀自己写了多少多少万行的代码,仿佛代码的数量是衡量编程水平的标准。然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。\n就像文学作品一样,代码是不可能一蹴而就的。灵感似乎总是零零星星,陆陆续续到来的。\n所以如果反复提炼代码已经不再有进展,那么你可以暂时把它放下。过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。这样反反复复很多次之后,你就积累起了灵感和智慧,从而能够在遇到新问题的时候直接朝正确,或者接近正确的方向前进。\n2.写优雅的代码* 人们都讨厌“面条代码”(spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。\n那么优雅的代码一般是什么形状的呢?\n如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。\n优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。\n3.写模块化的代码* 有些人吵着闹着要让程序“模块化”,其实并不理解什么叫做“模块”。肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。\n真正的模块化,并不是文本意义上的,而是逻辑意义上的。\n一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。\n想要达到很好的模块化,你需要做到以下几点:\n1) 避免写太长的函数\n如果发现函数太大了,就应该把它拆分成几个更小的。\n2) 制造小的工具函数\n如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。\n3) 每个函数只做一件简单的事情\n有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。这种“复用”其实是有害的。\n如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。\n如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。\n具体示例,可以参考原文,确实简洁明了!\r4) 避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数\n这个确实,使用全局变量,就不得不考虑该变量是不是在别处作了改变。\r依赖全局的数据,函数不再有明确的输入和输出,依赖于当前的上下文。全局的数据还有可能被其他代码改变,代码变得难以理解,难以确保正确性。\n4.写可读的代码* 有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。\n实际上,真正优雅可读的代码,是几乎不需要注释的。\n如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。\n有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。\n如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。\n所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:\n1) 使用有意义的函数和变量名字\n如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如:\n// put elephant1 into fridge2\rput(elephant1, fridge2); 2) 局部变量应该尽量接近使用它的地方\n这种短距离,可以加强读者对于这里的“计算顺序”的理解。读者就就清楚的知道,这个变量并不是保存了什么可变的值,而且它算出来之后就没变过。\n如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。\n3) 局部变量名字应该简短\n因为它们处于局部,再加上第 2 点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思。\n4) 不要重用局部变量\n我认为这是逻辑意义上的局部,比如 a 逻辑嵌套了 b 和 c ,a 中定义了一个变量 x ,在 b 和 c 中改变后,在 a 内后续使用该变量的值,则“不算重用”。\r不过仍然需要注意,变量定义和最终使用距离不要太远,否则,就应该考虑其他方式。\n5) 把复杂的逻辑提取出去,做成“帮助函数”\n有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。\n举一个例子:\n... // put elephant1 into fridge2 opendoor(fridge2); if (\u0008elephant1.\u0008alive()) { ... } else { ... } closedoor(fridge2); ... 如果你把这片代码提出去定义成一个函数:\nvoid put(elephant elephant, fridge fridge) { opendoor(fridge); if (\u0008elephant.alive()) { ... } else { ... } closedoor(fridge); } 这样原来的代码就可以改成:\n... put(elephant1, fridge2); ... 更加清晰,而且注释也没必要了。\n6) 把复杂的表达式提取出去,做成中间变量\npizza pizza = makepizza(crust(salt(), butter()), topping(onion(), tomato(), sausage())); // ↓↓↓ crust crust = crust(salt(), butter()); topping topping = topping(onion(), tomato(), sausage()); pizza pizza = makepizza(crust, topping); 有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。\n7) 在合理的地方换行\n是的,现在用自动化的格式工具,确实没有手动让语义更易读。\r5.写简单的代码* 程序语言都喜欢标新立异,提供这样那样的“特性”,然而有些特性其实并不是什么好东西。很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。\n并不是语言提供什么,你就一定要把它用上的。实际上你只需要其中很小的一部分功能,就能写出优秀的代码。我一向反对“充分利用”程序语言里的所有特性。\n实际上,我心目中有一套最好的构造。不管语言提供了多么“神奇”的,“新”的特性,我基本都只用经过千锤百炼,我觉得值得信赖的那一套。\n现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。\n1) 避免使用自增减表达式( =i++,++i,i\u0026ndash;,\u0026ndash;i= )\n这种自增减操作表达式其实是历史遗留的 设计失误 。\n它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。\n其实这两个表达式完全可以分解成两步,把读和写分开:一步更新 i 的值,另外一步使用 i 的值。\nfoo(i++); // ↓↓↓ let t = i; i += 1; foo(t); // ------- foo(++i); // ↓↓↓ i += 1; foo(i); 不难看出, =i++= 其实是使用更新前的值,而 =++1= 是使用更新后的值。\n拆开之后的代码,含义完全一致,却清晰很多。到底更新是在取值之前还是之后,一目了然。\n自增减表达式只有在两种情况下才可以安全的使用。一种是在 for 循环的 update 部分,比如 =for(int i = 0; i \u0026lt; 5; i++)= 。另一种情况是写成单独的一行,比如 =i++;= 。这两种情况是完全没有歧义的。\n你需要避免其它的情况,比如用在复杂的表达式里面,比如 =foo(i++),foo(++i) + foo(i),……= 没有人应该知道,或者去追究这些是什么意思。\n2) 永远不要省略花括号\n3) 合理使用括号,不要盲目依赖操作符优先级\n4) 避免使用 =continue= 和 =break=\n循环语句(for,while)里面出现 =return= 是没问题的,然而如果你使用了 =continue= 或者 =break= ,就会让循环的逻辑和终止条件变得复杂,难以确保正确。\n出现 continue 或者 break 的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要 continue 或者 break 的。如果你的循环里出现了 continue 或者 break ,你就应该考虑改写这个循环。\n改写循环的办法有多种:\n如果出现了 continue ,你往往只需要把 continue 的条件反向,就可以消除 continue ; 如果出现了 break ,你往往可以把 break 的条件,合并到循环头部的终止条件里,从而去掉 break ; 有时候你可以把 break 替换成 return,从而去掉 break ; 如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后 continue 或者 break 就可以去掉了。 详细示例见原文。\r6.写直观的代码* 我写代码有一条重要的原则:如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。\n比如,人们往往容易滥用了逻辑操作 =\u0026amp;\u0026amp;= 和 =||= 的短路特性。这两个操作符可能不执行右边的表达式,原因是为了机器的执行效率,而不是为了给人提供这种“巧妙”的用法。这两个操作符的本意,只是作为逻辑操作,它们并不是拿来给你代替 if 语句的。\n……\n7.写无懈可击的代码* 在之前一节里,我提到了自己写的代码里面很少出现只有一个分支的 if 语句。我写出的 if 语句,大部分都有两个分支。使用这种方式,其实是为了无懈可击的处理所有可能出现的情况,避免漏掉 corner case。所以我的代码很多看起来是这个样子:\n// 推荐 ✔ if (...) { if (...) { ... return false; } else { return true; } } else if (...) { ... return false; } else { return true; } 缺了 else 分支的 if 语句,控制流自动“掉下去”,到达最后的 return true 。这种写法看似更加简洁,避免了重复,然而却很容易出现疏忽和漏洞。\n// 不推荐 ✘ if (...) { if (...) { ... return false; } } else if (...) { ... return false; } return true; 嵌套的 if 语句省略了一些 else,依靠语句的“控制流”来处理 else 的情况,是很难正确的分析和推理的。如果你的 if 条件里使用了 \u0026amp;\u0026amp; 和 || 之类的逻辑运算,就更难看出是否涵盖了所有的情况。\n由于疏忽而漏掉的分支,全都会自动“掉下去”,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。\n另外一种省略 else 分支的情况是这样:\n// 不推荐 ✘ string s = \u0026#34;\u0026#34;; if (x \u0026lt; 5) { s = \u0026#34;ok\u0026#34;; } 写这段代码的人,脑子里喜欢使用一种“缺省值”的做法。s 缺省为 null ,如果 x\u0026lt;5 ,那么把它改变(mutate)成“ok”。这种写法的缺点是,当 x\u0026lt;5 不成立的时候,你需要往上面看,才能知道 s 的值是什么。这还是你运气好的时候,因为 s 就在上面不远。很多人写这种代码的时候,s 的初始值离判断语句有一定的距离,中间还有可能插入一些其它的逻辑和赋值操作。\n// 推荐 ✔ string s; if (x \u0026lt; 5) { s = \u0026#34;ok\u0026#34;; } else { s = \u0026#34;\u0026#34;; } // 这个情况比较简单,你还可以把它写成这样 // (对于更加复杂的情况,我建议还是写成 if语句为好) string s = x \u0026lt; 5 ? \u0026#34;ok\u0026#34; : \u0026#34;\u0026#34;; 8.正确处理错误* 使用有两个分支的 if 语句,只是我的代码可以达到无懈可击的其中一个原因。这样写 if 语句的思路,其实包含了使代码可靠的一种 通用思想:穷举所有的情况 ,不漏掉任何一个。\n……\n关于异常,这里先不谈……\r9.正确处理 null 指针* ……\n10.防止过度工程* 过度工程即将出现的一个重要信号,就是当你过度的思考“将来”,考虑一些还没有发生的事情,还没有出现的需求。另外一种过度工程的来源,是过度的关心“代码重用”。过度地关心“测试”,也会引起过度工程。\n根据这些,我总结出来的防止过度工程的原则如下:\n先把眼前的问题解决掉,解决好,再考虑将来的扩展问题; 先写出可用的代码,反复推敲,再考虑是否需要重用的问题; 先写出可用,简单,明显没有 bug 的代码,再考虑测试的问题。 真知灼见!!!好好思考下,值得反复阅读和学习!\r给 java 说句公道话 给 java 说句公道话\njava 超越了所有咒骂它的“动态语言”!\njava 的“继承人”没能超越它!\njava 没有特别讨厌的地方。\n编程使用什么工具是重要的,然而工具终究不如自己的技术重要。很多人花了太多时间,折腾各种新的语言,希望它们会奇迹一般的改善代码质量,结果最后什么都没做出来。选择语言最重要的条件,应该是“够好用”就可以,因为项目的成功最终是靠人,而不是靠语言。既然 java 没有特别大的问题,不会让你没法做好项目,为什么要去试一些不靠谱的新语言呢?\n我为什么不再做 pl 人 我为什么不再做 pl 人\npl 看似计算机科学最精髓的部分,事实确实也是这样的。没有任何一个其它领域,可以让你对程序的本质形成如此深入的领悟。\n……\n测试的道理 测试的道理\n在长期的程序语言研究和实际工作中,我摸索出了一些关于测试的道理。\n在我心目中,代码本身的地位大大的高于测试。我不忽视测试,但我不会本末倒置,过分强调测试,我并不推崇测试驱动开发(tdd)。\n看,别人的经验也不是空中楼阁,是自己一步步领悟出来的。\rps.我不怎么写测试……\n现在我就把这些自己领悟到的关于测试的道理总结一下,其中有一些是鲜为人知或者被误解的。\n不要以为你处处显示出“重视代码质量”的态度,就能提高代码质量; 真正的编程高手不会被测试捆住手脚; 在程序和算法定型之前,不要写测试; 不要为了写测试而改变本来清晰的编程方式; 不要测试“实现细节”,因为那等同于把代码写两遍; 并不是每修复一个 bug 都需要写测试; 避免使用 mock,特别是多层的 mock; 不要过分重视“测试自动化”,人工测试也是测试; 避免写太长,太耗时的测试; 一个测试只测试一个方面,避免重复测试; 避免通过比较字符串来进行测试; 认知“测试能帮助后来人”的误区。 ……\n经验和洞察力 经验和洞察力\n很多人很在乎“经验”,比如号称自己在某领域有 30 年的经验,会用这样那样的技术。我觉得经验是有价值的,我也有经验,各个领域的都有点。然而我并不把经验放在很重要的位置,因为我拥有大部分人都缺乏而且忽视的一种东西:洞察力(insight)。\n什么是洞察力?洞察力就是透过现象看到本质的能力。\n一切的东西走到最后,都会成为一个哲学问题。\r其实,经验和洞察力并不是矛盾的,王垠想表达的是他得到了“道”,所以可以很快的掌握“术”。\n如何掌握所有的程序语言 如何掌握所有的程序语言\n重视语言特性,而不是语言 任何一种“语言”,都是各种“语言特性”的组合。\n举一些语言特性的例子:\n- 变量定义\r- 算术运算\r- for 循环语句,while 循环语句\r- 函数定义,函数调用\r- 递归\r- 静态类型系统\r- 类型推导\r- lambda 函数\r- 面向对象\r- 垃圾回收\r- 指针算术\r- goto 语句\r- …… 对于初学者来说,其实没必要纠结到底要先学哪一种语言,再学哪一种。\n初学者往往不理解, 每一种语言里面必然有一套“通用”的特性 。比如变量,函数,整数和浮点数运算,等等。这些是每个通用程序语言里面都必须有的,一个都不能少。你只要通过“某种语言”学会了这些特性,掌握这些特性的根本概念,就能随时把这些知识应用到任何其它语言。你为此投入的时间基本不会浪费。所以初学者纠结要“先学哪种语言”,这种时间花的很不值得,还不如随便挑一个语言,跳进去。\n如果你不能用一种语言里面的基本特性写出好的代码,那你换成另外一种语言也无济于事。你会写出一样差的代码。\n很多初学者不了解,一个高明的程序员如果开始用一种新的程序语言,他往往不是去看这个语言的大部头手册或者书籍,而是先有一个需要解决的问题。手头有了问题,他可以用两分钟浏览一下这语言的手册,看看这语言大概长什么样。然后,他直接拿起一段例子代码来开始修改捣鼓,想法把这代码改成自己正想解决的问题。在这个简短的过程中,他很快的掌握了这个语言,并用它表达出心里的想法。\n在这个过程中,随着需求的出现,他可能会问这样的问题:\n这个语言的“变量定义”是什么语法,需要“声明类型”吗,还是可以用“类型推导”? 它的“类型”是什么语法?是否支持“泛型”?泛型的 “variance” 如何表达? 这个语言的“函数”是什么语法,“函数调用”是什么语法,可否使用“缺省参数”? …… 注意到了吗?上面每一个引号里面的内容,都是一种语言特性(或者叫概念)。这些概念可以存在于任何的语言里面,虽然语法可能不一样,它们的本质都是一样的。\n这些实际问题都是随着写实际的代码,解决手头的问题,自然而然带出来的,而不是一开头就抱着语言手册看得仔仔细细。\n掌握了语言特性的人都知道,自己需要的特性,在任何语言里面一定有对应的表达方式。 如果没有直接的方式表达,那么一定有某种“绕过方式”。如果有直接的表达方式,那么它只是语法稍微有所不同而已。所以,他是带着问题找特性,就像查字典一样,而不是被淹没于大部头的手册里面,昏昏欲睡一个月才开始写代码。\n掌握了通用的语言特性,剩下的就只剩某些语言“特有”的特性了。\n研究语言的人都知道,要设计出新的,好的,无害的特性,是非常困难的。所以一般说来,一种好的语言,它所特有的新特性,终究不会超过一两种。如果有个语言号称自己有超过 5 种新特性,那你就得小心了,因为它们带来的和可能不是优势,而是灾难!\n最好的语言研究者,往往不是某种语言的设计者,而是某种 关键语言特性的设计者 (或者支持者)。\n合理的入门语言 所以初学者要想事半功倍,就应该从 一种“合理”的,没有明显严重问题的语言 出发, 掌握最关键的语言特性,然后由此把这些概念应用到其它语言 。哪些是合理的入门语言呢?我个人觉得这些语言都可以用来入门:\n- scheme\r- c\r- java\r- python\r- javascript 那么相比之下,我不推荐用哪些语言入门呢?\n- shell\r- powershell\r- awk\r- perl\r- php\r- basic\r- go\r- rust 掌握关键语言特性,忽略次要特性 为了达到我之前提到的融会贯通,一通百通的效果,初学者应该专注于语言里面最关键的特性,而不是被次要的特性分心。\n……\n自己动手实现语言特性 在基本学会了各种语言特性,能用它们来写代码之后,下一步的进阶就是去实现它们。只有实现了各种语言特性,你才能完全地拥有它们,成为它们的主人。否则你就只是它们的使用者,你会被语言的设计者牵着鼻子走。\n有个大师说得好, 完全理解一种语言最好的方法就是自己动手实现它,也就是自己写一个解释器来实现它的语义 。但我觉得这句话应该稍微修改一下: 完全理解一种“语言特性”最好的方法就是自己亲自实现它 。\n注意我在这里把“语言”改为了“语言特性”。你并不需要实现整个语言来达到这个目的,因为我们最终使用的是语言特性。 只要你自己实现了一种语言特性,你就能理解这个特性在任何语言里的实现方式和用法。\n举个例子,学习 sicp 的时候,大家都会亲自用 scheme 实现一个面向对象系统。用 scheme 实现的面向对象系统,跟 java,c++,python 之类的语言语法相去甚远,然而它却能帮助你理解任何这些 oop 语言里面的“面向对象”这一概念,它甚至能帮助你理解各种面向对象实现的差异。\n这种效果是你直接学习 oop 语言得不到的,因为在学习 java,c++,python 之类语言的时候,你只是一个用户,而用 scheme 自己动手实现了 oo 系统之后,你成为了一个创造者。\n类似的特性还包括类型推导,类型检查,惰性求值,如此等等。我实现过几乎所有的语言特性,所以任何语言在我的面前,都是可以被任意拆卸组装的玩具,而不再是凌驾于我之上的神圣。\n总结 写了这么多,重要的话重复三遍: 语言特性,语言特性,语言特性,语言特性! 不管是初学者还是资深程序员,应该专注于语言特性,而不是纠结于整个的“语言品牌”。只有这样才能达到融会贯通,拿起任何语言几乎立即就会用,并且写出高质量的代码。\n解谜计算机科学 解谜计算机科学\n解谜英语语法 解谜英语语法\n学习的智慧 学习的智慧\n1.死知识,活知识\n大部分人从学校,从书籍,从文献学知识,结果学到一堆“死知识”。要检验知识是不是死的,很简单。如果你遇到前所未见的问题,却不能把这些知识运用出来解决问题,那么这些知识就很可能是死的。\n实践出真知!\r死知识可能来源于真正聪明的人,但普通人往往是间接得到它。从知识的创造者到你之间,经过了多次的转手倒卖。就算你直接跟知识的鼻祖学习都不容易得到真传,普通人还得经过多次转手。每一次转手都损失里面的信息含量,增加“噪音”,甚至完全被误传。所以到你这里的时候,里面的“信噪比”就很低了。这就是为什么你学了东西,到时候却没法用出来。\n追根溯源之后,你会发现这知识最初的创造者经过了成百上千的错误。\n没有这些失败的经验,你就少了所谓“思路”,那你是不大可能从一个知识发展出新的知识的。\n死知识是脆弱的。面对现实的问题,死知识的拥有者往往不知所措,他们的内心充满了恐惧。\n世界上最重大的发现,往往产生于对非常基础的问题的思考。\n活知识必须靠自己创造出来,要经过许许多多的失败。如果没有经过失败,是不可能得到活知识的。\n2.知识的来源\n知识的来源最好是自己的头脑,但也不尽然。有些东西成本太高,没条件做实验就没法得到,所以还是得先获取现成的死知识。\n有些人说到“学习”,总是喜欢认认真真上课,抄笔记,看书。有些人喜欢勾书,把书上整整齐齐画满了横杠。兢兢业业不辞辛苦,最后却发现没学会什么。\n为什么会这样呢?\n首先因为他们没有理智的选择知识的来源。其次,他们不明白如何有效的“提取”知识。这第一点属于“品位”问题,第二点则属于“方法”问题。\n很多人没有意识到,对于同一个问题有很多不同的书,不同的作者对于问题的见解深度是不一样的。如果一个主题你看得头大,最好的办法是放下这书,去寻找对同一主题的更简单的解释。这些东西可以来源于网络,也可以来自其它书籍,也可以来自身边的人。\n同时保留多个这样的资源,你就可以对任何主题采用同样的“广度优先”搜索,获得深入理解的机会就会增加。\n3.英语的重要性\n不是我崇洋媚外,可是实话说,这几年中文内容虽然改进了很多,可是很多方向上的专业程度还是比英文的低很多,很多不准确甚至根本就是错的。\n我不排斥看中文内容,但我建议不要片面的只看中文内容。事无巨细都应该同时参考英文信息,多方面分析之后再做决定。生活的决策如此,专业知识的学习当然也一样。对于同一个知识点,看到中文的时候你最好搜索它的英文,对比各种资料,这样你就更容易得到准确的信息。\ntalk is not cheap talk is not cheap\n“苦干,用代码说话,忽视想法”,是很多程序员的误区。\n人的思想不一定需要代码来证明,甚至很多的想法无法简单的用代码表示,只有靠人的头脑才能想得清楚。思想是首要的,代码只是对思想的一种实现。\n我们先得要有思想(算法),才可能有代码。\n代码不能代替思想交流和讨论。代码不能清晰的表达一个人的想法,也不能显示一个人的思维深度。\n代码是死的,它是对已有问题的解决方案。而你想要知道的是这个人在面对新的问题的时候,他会怎样去解决它。所以你必须知道这个人的思维方式,看清楚他是否真的知道他声称“精通”的那些东西。\n凡事皆有度,物极必反。\r我不是编译器专家 我不是编译器专家\n我不是编译器专家,而且我看不起编译器这个领域。我一般不会居高临下看低其它人,然而对于认识肤浅却又自视很高的人,我确实会表示出藐视的态度。现在我的态度是针对编译器这整个领域。真的,我看这些人不顺眼很多年了。\n“哈哈,不要误会,我不是针对某个人,我是说在座的各位都是垃圾……”\r就最后研究的领域,我是一个编程语言(pl)研究者,从更广的角度来看,我是一个计算机科学家。\nit 业人士经常混淆编程语言(pl)和编译器两个领域,而其实 pl 和编译器是很不一样的。真懂 pl 的人去做编译器也会比较顺手,而编译器专业的却不一定懂 pl。为什么呢?因为 pl 研究涵盖了计算最本质的原理,它不但能解释语言的语义,而且能解释处理器的构架和工作原理 。当然它也能解释编译器是怎么回事,因为编译器只不过是把一种语言的语义,利用另外一种语言表达出来,也就是翻译一下。pl 研究所用的编程范式和技巧,很多可以用到编译器的构造中去,但却比编译器的范畴广阔很多。\n深入研究过 pl 的人,能从本质上看明白编译器里在做什么。所以编译器算是 pl 思想的一种应用,然而 pl 的应用却远远不止做编译器。\n实际上做编译器是很无聊的工作,大部分时候只是把别人设计的语言,翻译成另外的人设计的硬件指令。所以编译器领域处于编程语言(pl)和计算机体系构架(computer architecture)两个领域的夹缝中,上面的语言不能改,下面的指令也不能改,并没有很大的创造空间。\n我的事业计划 我的事业计划\n为了建立起最高水准的,真正的教育机构,我的初期计划是做一个顾问或者导师。\n在计划中的课程内容可能包括:\n- 计算机科学入门\r- 掌握所有的编程语言\r- c++,java,python,javascript,haskell\r- 编程的智慧——如何写出优雅的代码\r- 算法\r- 编程语言理论\r- 操作系统\r- 计算机体系构架\r- 编译器设计和实现\r- 函数式编程\r- 逻辑式编程\r- 机器学习(深度学习,计算机视觉等)\r- …… 每一个课程,我都会试图用最简单直观的方式来讲解。\n如何阅读别人的代码 如何阅读别人的代码\n比起阅读代码,我更喜欢别人给我讲解他们的代码,用简单的语言或者图形来解释他们的思想。有了思想,我自然知道如何把它变成代码,而且是优雅的代码。很多人的代码我不会去看,但如果他们给我讲,我是可以接受的。\n如果有同事请我帮他改进代码,我不会拿起代码埋头就看,因为我知道看代码往往是事倍功半,甚至完全没用。我会让他们先在白板上给我解释那些代码是什么意思。我的同事们都发现,把我讲明白是需要费一番工夫的。因为我的要求非常高,只要有一点不明白,我就会让他们重新讲。还得画图,我会让他们反复改进画出来的图,直到我能一眼看明白为止。如果图形是 3d 的,我会让他们给我压缩成 2d 的,理解了之后再推广到 3d。\n我无法理解复杂的,高维度的概念,他们必须把它给我变得很简单。\n所以跟我讲代码可能需要费很多时间,但这是值得的。我明白了之后,往往能挖出其他人都难以看清楚的要点。给我讲解事情,也能提升他们自己的思维和语言能力,帮助他们简化思想。很多时候我根本没看代码,通过给我讲解,后来他们自己就把代码给简化了。节省了我的脑力和视力,他们也得到了提高。\n我最近一次看别人的代码是在 intel,我们改了 pytorch 的代码。那不是一次愉悦的经历,因为虽然很多人觉得 pytorch 好用,它内部的代码却是晦涩而难以理解的。\npytorch 之类的深度学习框架,本质上是某种简单编程语言的解释器,只不过这些语言写出来的函数可以求导而已。\n很多人都不知道,有一天我用不到一百行 scheme 代码就写出了一个「深度学习框架」,它其实是一个小的编程语言。虽然没有性能可言,没有 gpu 加速,功能也不完善,但它抓住了 pytorch 等大型框架的本质——用这个语言写出来的函数能自动求导。这种洞察力才是最关键的东西,只要抓住了关键,细节都可以在需要的时候琢磨出来。几十行代码反复琢磨,往往能帮助你看透上百万行的项目里隐藏的秘密。\n很多人以为看大型项目可以提升自己,而没有看到大型项目不过是几十行核心代码的扩展,很多部分是低水平重复。几十行平庸甚至晦涩的代码,重复一万次,就成了几十万行。看那些低水平重复的部分,是得不到什么提升的。\n造就我今天的编程能力和洞察力的,不是几百万行的大型项目,而是小到几行,几十行之短的练习。\n不要小看了这些短小的代码,它们就是编程最精髓的东西。反反复复琢磨这些短小的代码,不断改进和提炼里面的结构,磨砺自己的思维。逐渐的,你的认识水平就超越了这些几百万行,让人头痛的项目。\n所以我如何阅读别人的代码呢?don’t。如果有条件,我就让代码的作者给我讲,而不是去阅读它。如果作者不合作,而我真的要使用那个项目的代码,我才会去折腾它。那么如何折腾别人的代码呢?我有另外一套办法。\n英语学习的一些经验 英语学习的一些经验\n对智商的怀疑 对智商的怀疑\n计算机科学入门班报名 计算机科学入门班报名\n1.为什么重视“零基础”教育\n有些人可能不大明白我为什么喜欢讲“零基础”课程。一方面,真正好的教育应该是能让完全无基础的人顺利掌握的。就像爱因斯坦说:“如果你不能给一个六岁小孩解释清楚,那你并不真的懂。” 所以“零基础”的学生能够检验我是否达到了这个“真懂”的目标。\n实际上, 我的很多深刻理解,都是通过反复琢磨非常基础的概念获得的 ,而不是通过很“高级”,很复杂的概念。我最常用的“心理模型”,其实跟初学者第一节课学的内容差不多。\n在我心里并没有“初学者”和“资深者”的差别。我发现很多工作了几十年的工程师,很多连最基本的概念都是一知半解的,这也许就是为什么他们在工作中无法找准正确的方向,经常瞎撞。\n2.课程内容\n课程计划涵盖计算机科学的主要思想,大概会包括以下内容:\n- 基础语言构造,包含最常用几种语言的主要特性。\r- 递归思想,递归数据结构的处理。\r- 基本数据结构,少量基础算法。\r- 函数式编程基本思想。\r- 抽象的思维方式。\r- 基础的解释器原理。 如果从书籍的覆盖面来看,我试图包括以下书籍的精华内容:\n- sicp(前 4 章)\r- the little schemer\r- a little java, a few patterns 3.你将受到的训练\n掌握系统化的思维方法,严密的推理技巧; 写出简单,优雅,容易理解,可靠的代码; 从无到有,不依赖于任何语言的特性,解决各种计算问题的思路。 新丑陋的中国人 新丑陋的中国人\n我一定是年龄大了,比以前成熟点了。但看到这个题目,我还是忍不住想说: “去你妈的!你才丑陋,你全家都丑陋,操!!!”\r计算机科学基础班(第三期)报名 计算机科学基础班(第三期)报名\n……\n课程大纲,下面简要说一下课程的内容:\n教学语言\n课程目前使用 javascript 作为教学语言,但并不是教 javascript 语言本身,不会使用 javascript 特有的任何功能。课程教的思想不依赖于 javascript 的任何特性,它可以应用于任何语言,课程可以在任何时候换成任何语言。学生从零开始,学会的是计算机科学最核心的思想,从无到有创造出各种重要的概念,直到最后实现出自己的编程语言和类型系统。\n课程强度\n课程的设计是一个逐渐加大难度,比较辛苦,却很安全的山路,它通往很高的山峰。要参加课程,请做好付出努力的准备。在两个月的时间里,你每天需要至少一个小时来做练习,有的练习需要好几个小时才能做对。跟其他的计算机教学不同,学生不会因为缺少基础而放弃,不会误入歧途,也不会掉进陷阱出不来。学生需要付出很多的时间和努力,但没有努力是白费的。\n第一课:函数\n跟一般课程不同,我不从所谓“hello world”程序开始,也不会叫学生做一些好像有趣而其实无聊的小游戏。\n一开头我就讲最核心的内容:函数。\n关于函数只有很少几个知识点,但它们却是一切的核心。只知道很少的知识点的时候,对它们进行反复的练习,让头脑能够自如地对它们进行思考和变换,这是教学的要点。我为每个知识点设计了恰当的练习。\n第一课的练习每个都很小,只需要一两行代码,却蕴含了深刻的原理。练习逐渐加大难度,直至超过博士课程的水平。我把术语都改头换面,要求学生不上网搜索相关内容,为的是他们的思维不受任何已有信息的干扰,独立做出这些练习。练习自成系统,一环扣一环。后面的练习需要从前面的练习获得的灵感,却不需要其它基础。有趣的是,经过正确的引导,好些学生把最难的练习都做出来了,完全零基础的学生也能做出绝大部分,这是我在世界名校的学生里都没有看到过的。具体的内容因为不剧透的原因,我就不继续说了。\n第二课:递归\n递归可以说是计算机科学(或数学)最重要的概念。\n我从最简单的递归函数开始,引导理解递归的本质,掌握对递归进行系统化思考的思路。递归是一个很多人自以为理解了的概念,而其实很多人都被错误的教学方式误导了。很多人提到递归,只能想起“汉诺塔”或者“八皇后”问题,却不能拿来解决实际问题。很多编程书籍片面强调递归的“缺点”,教学生如何“消除递归”,却看不到问题的真正所在——某些语言(比如 c 语言)早期的函数调用实现是错误而效率低下的,以至于学生被教导要避免递归。由于对于递归从来没有掌握清晰的思路,在将来的工作中一旦遇到复杂点的递归函数就觉得深不可测。\n第三课:链表\n从零开始,学生不依赖于任何语言的特性,实现最基本的数据结构。\n第一个数据结构就是链表,学生会在练习中实现许多操作链表的函数。这些函数经过了精心挑选安排,很多是函数式编程语言的基本函数,但通过独立把它们写出来,学生掌握的是递归的系统化思路。这使得他们能自如地对这类数据结构进行思考,解决新的递归问题。\n与一般的数据结构课程不同,这个课程实现的大部分都是「函数式数据结构」,它们具有一些特别的,有用的性质。因为它们逻辑结构清晰,比起普通数据结构书籍会更容易理解。与 haskell 社区的教学方式不同,我不会宗教式的强调纯函数的优点,而是客观地让学生领会到其中的优点,并且发现它们的弱点。学会了这些结构,在将来也容易推广到非函数式的结构,把两种看似不同的风格有机地结合在一起。\n第四课:树结构\n从链表逐渐推广出更复杂的数据结构——树。\n在后来的内容中,会常常用到这种结构。树可能是计算机科学中最常用,最重要的数据结构了,所以理解树的各种操作是很重要的。我们的树也都是纯函数式的。\n第五课:计算器\n在熟悉了树的基本操作之后,实现一个比较高级的计算器,它可以计算任意嵌套的算术表达式。算术表达式是一种“语法树”,从这个练习学生会理解“表达式是一棵树”这样的原理。\n第六课:查找结构\n理解如何实现 =key-value= 查找结构,并且亲手实现两种重要的查找数据结构。我们的查找结构也都是函数式数据结构。这些结构会在后来的解释器里派上大的用场,对它们的理解会巩固加深。\n第七课:解释器\n利用之前打好的基础,亲手实现计算机科学中最重要,也是通常认为最难理解的概念——解释器。\n解释器是理解各种计算机科学概念的关键,比如编程语言,操作系统,数据库,网络协议,web 框架。计算机最核心的部件 cpu 其实就是一个解释器,所以解释器的认识能帮助你理解「计算机体系构架」,也就是计算机的“硬件”。你会发现这种硬件其实和软件差别不是很大。你可以认为解释器就是「计算」本身,所以它非常值得研究。对解释器的深入理解,也能帮助理解很多其它学科,比如自然语言,逻辑学。\n第八课:类型系统\n在解释器的基础上,学生会理解并实现一个相当高级的类型系统(type system)和类型检查器(typechecker)。\n这相当于实现一个类似 java 的静态类型语言,但比 java 在某些方面还要高级和灵活。我们的类型系统包含了对于类型最关键的要素,而不只是照本宣科地讲解某一种类型系统。当你对现有的语言里的类型系统不满意的时候,这些思路可以帮助你设计出自己的类型系统。学生会用动手的方式去理解静态类型系统的原理,其中的规则,却不含有任何公式。\n类型系统的规则和实现,一般只会在博士级别的研究中才会出现,可以写成一本厚书(比如 tapl 那样的),其中有各种神秘的逻辑公式。而我的学生从零开始,一节课就可以掌握这门技术的关键部分,实现出正确的类型系统,并且推导出正确的公式。有些类型规则是如此的微妙,以至于微软这么大的公司在 21 世纪做一个新的语言(typescript),仍然会在初期犯下类型专家们早已熟知的基本错误。上过这个课程的很多同学,可以说对这些基础原理的理解已经超过了 typescript 的设计者,但由于接受的方式如此自然,他们有一些人还没有意识到自己的强大。\n关于面向对象\n虽然课程不会专门讲“面向对象”的思想,但面向对象思想的本质(去掉糟粕)会从一开头就融入到练习里。上过课的同学到后来发现,虽然我从来没直接教过面向对象,而其实他们已经理解了面向对象的本质是什么。在将来的实践中,他们可以用这个思路去看破面向对象思想的本质,并且合理地应用它。\n奖励练习\n途中我会通过“奖励练习”的方式补充其它内容。比如第二期的课程途中,我临时设计了一个 parser 的练习,做完了其它练习的同学通过这个练习,理解了 parser 的原理,写出了一个简单但逻辑严密的 parser。奖励练习之所以叫“奖励”,因为并不是所有学生都能得到这个练习,只有那些付出了努力,在其他练习中做到融会贯通,学有余力的学生才会给这个练习。这样会鼓励学生更加努力地学习。\n一个朋友看了我的课程内容说,这不叫“基础班”,只能叫“大师班”。他不相信零基础的学生能跟上,但事实却是可行的。\n为什么不能即是“基础班”又是“大师班”呢?\n有句话说得好,大师只不过是把基础的东西理解得很透彻的人而已。\n我希望这个基础班能帮助人们获得本质的原理,帮助他们看透很多其它内容。所以上了“基础班”,可能在很长时间之内都不需要“高级班”了,因为他们已经获得了很强的自学能力,能够自己去探索未知的世界,攀登更高的山峰。\n","date":"2021-03-03","permalink":"https://loveminimal.github.io/posts/yinwang/","summary":"\u003cblockquote\u003e\n\u003cp\u003e我并不喜欢王垠这个人,甚至有些反感,但他的某些博文我很喜欢,也因此而受益,Hmm\u0026hellip;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e*Tip. 非原文摘录,各别语句进行了删减和改动,建议看原文(点击章节标题)。\u003c/p\u003e","title":"当然我在扯淡"},{"content":"i.e. modular programming\n每一个领域的内容,都要有先问一下 \u0026ndash; 是什么?为什么?做什么?怎么做?\n是什么?简述概念; 为什么?历史发展,出现的原因,优缺点; 做什么?主要应用场景; 怎么做?具体使用方法步骤。 简介 模块化编程 (模块化程序设计),是指在进行程序设计时将一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并在这些模块之间建立必要的联系,通过模块的互相协作完成整个功能的程序设计方法。\n= 大程序 → (功能)小程序模块( → 子过程) → 模块联系、协作\n随着程序规模的扩大,无论是项目本身(编写、扩展、测试、维护),还是经济上的考虑(效率、收支),分工协作都是不可避免的。\n在模块化的过程中可能会产生什么问题呢? 命名冲突,本质在于作用域。\n= 实际项目中,模块化开发几乎不会是一个人开发所有模块,那就很难避免命名冲突(小甲和小乙都使用了同一个变量名、函数名),项目越大,越难控制。*\n下面我们就以 javascript 为例来看一下其模块化的发展。\n= 为什么选用 javascript 呢?因为 javascript 最初的设计是没有模块化考虑的,而是随着其应用场景和范围的变化,逐渐通过各种方式实现了模块化,如此,我们可以看一下其模块化的全貌和历程。*\njavascript 的模块化 1 1991 年 8 月 6 日 (农历六月廿六),世界上第一个网页诞生。世界上的第一个网页是这样的 http://info.cern.ch/ ,只用来作为文本的展示和极其简单的交互。\n\u0026gt; 万维网的发明者,互联网之父蒂姆·伯纳斯·李\n检查一下网页源码的话,你会发现,它只是纯 html 文本,没有 css ,更没有 javascript 。\n= 哈坤·利于 1994 年在芝加哥的一次会议上第一次展示了 css 的建议;javascript 是 1995 年由 netscape 公司的 brendan eich,在网景导航者浏览器上首次设计实现而成。*\n随着网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等……开发者不得不使用软件工程的方法,管理网页的业务逻辑。javascript 的模块化编程,已经成为一个迫切的需求。\n前面我们已经知道,javascript 最初设计的时候就没有考虑到模块化,那么我们要怎么实现它的模块化呢?\n模块化编程是将一个大程序按照功能划分为若干小程序模块,那么,我们我们首先要解决的就是 划分模块 。\n原始划分 我们可以认为模块就是实现特定功能的一组方法(以及记录状态),只要把不同函数简单地放在一起,就算是一个模块。\n// 这就是一个模块 function m1() { /* ... */ } function m2() { /* ... */ } // ... 上面的代码中,函数 m1() 和 m2() ,组成一个模块,使用的时候,直接调用就行了。这样做有以下缺点:\n- 污染了全局变量,无法保证不与其他模块发生变量名冲突;\r- 模块成员之间看不出直接关系。 = 看,模块化其实就是相反的两个方面: ‘拆分’(隔离、解决冲突)和‘组合’(依赖、导出引入)。*\n对象划分 为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。\nvar module1 = new object({ _count: 0, m1: function () { /* ... */ }, m2: function () { /* ... */ }, }); 上边的函数 m1() 和 m2() ,都封装在 module1 对象里(现在它们有关系了,都属于一个对象嘛),使用的时候,就是调用这个对象的属性,如 module1.m1() 。但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写,如 module1._count = 5 。\n= 很显然,这样的封装是不安全的。*\n立即执行函数划分 如何不暴露私有成员的目的,自然是做到外界不能直接访问原始对象内部,只能访问我们想让他访问的对象。使用“立即执行函数(immediately-invoked function expression, iife)”,可以砍到不暴露私有成员的目的。\nvar module1 = (function () { var _count = 0; var m1 = function () { /* ... */ }; var m2 = function () { /* ... */ }; // 这个就是我们想让外界访问到的对象了 return { m1: m1, m2: m2, }; })(); 如此,在 { m1: m1, m2: m2 } 中根本没有 _count 变量,外界自然是访问不到的喽。\n至此,我们解决了 javascript 中如何模块化的问题 \u0026ndash; 使用 iife 来实现模块分离(避免命名冲突、封装不够等问题)。\n模块依赖 我们在前面已经讲述了 如何分离模块的问题 ,那么,另一个问题也就随之而来了 \u0026ndash; 假如在一个程序中,模块 a 调用了 b ,b 又调用了 c、d ,我们如何处理 模块之间的依赖关系 呢?\n什么是依赖?聚焦在模块本身无非就是两点:\n如何导入一个模块? 如何导出模块中你想暴露的? 我们来了解一下比较成熟的模块实现方式,如 commonjs、amd、cmd、umd 和 es module 等。后续的章节中,我们重点来认识一下 commonjs 和 es module 两种模块规范,其他在实际项目中已经应用不多。\ncommonjs2 在 node 环境中,一个 .js 文件就称之为一个模块(module),它们遵循 commonjs 模块规范。\n我们来看一个具体的例子,假如我们遵循 commonjs 规范编写 hello.js 文件,如下:\n// hello.js 文件就是名为 hello 的模块 var s = \u0026#39;hello\u0026#39;; function greet(name) { console.log(s + \u0026#39;, \u0026#39; + name + \u0026#39;!\u0026#39;); } // 导出模块 // 把函数 greet 作为模块的输出暴露出去 module.exports = greet; 在同一目录下,再来编写 main.js 文件,来调用 hello 模块的 greet 函数,如下:\n// 引入模块 // 使用 node 提供的 `require` 函数引入模块 `hello` var greet = require(\u0026#39;./hello\u0026#39;); var s = \u0026#39;michael\u0026#39;; greet(s); // → hello, michael! 综上可知,commonjs 规范,使用\nmodule.exports 暴露模块,如 module.exports = variable ,其中 variable 可以是任意对象、函数、数组等; require 函数导入模块,如 var ref = require('module_name) ,引入的对象具体是什么,取决于所引入模块输出的对象。 = ‘输出’ 决定 ‘引入’!*\ncommonjs 模块原理 我们知道,javascript 语言本身并没有一种模块机制来保证不同模块可以使用相同的变量名。\n那么,nodejs 是如何实现这一点的呢?\n_1. 模块隔离\n其实要实现“模块”这个功能,并不需要语法层面的支持,nodejs 也没有增加任何新的 javascript 语法。实现“模块”功能的奥妙就在于 javascript 是一种函数式编程语言, 它运行闭包!闭包!闭包! ,如我们上面说到是 iife 。我们只需要把一段 javascript 代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。\n= 看,我们使用函数的作用域作了隔离,如此,就避免了不同模块内部变量、函数名的命名冲突。*\n答案:内存机制决定的。看,内存才是最本质的。 下面,我们来看看 nodejs 具体为我们做了什么?\n我们编写的 hello.js 代码是这样的:\nvar name = \u0026#39;world\u0026#39;; console.log(\u0026#39;hello, \u0026#39; + name + \u0026#39;!\u0026#39;); nodejs 加载了 hello.js 之后,把代码包装了一下,变成了这样执行 ↓ :\n(function () { // 读取 hello.js 代码 var name = \u0026#39;world\u0026#39;; console.log(\u0026#39;hello, \u0026#39; + name + \u0026#39;!\u0026#39;); // hello.js 代码读取结束 })(); 如此, name 就变成了函数内部的局部变量,如果 nodejs 继续加载其他模块,这些模块中定义的 name 也互不干扰。\n_2. 模块输出\nnodejs 利用 javascript 的函数式编程特性 \u0026ndash; 闭包,轻松实现了模块的隔离。\n但是,模块的输出 module.exports 是怎么实现的呢?\n这个也容易,原来 nodejs 先准备了一个对象 module ,如下:\n// nodejs 准备一个 module 对象 var module = { id: \u0026#39;hello\u0026#39;, exports: {}, }; var load = function (module) { // 读取的 hello.js 代码 function greet(name) { console.log(\u0026#39;hello, \u0026#39; + name + \u0026#39;!\u0026#39;); } module.exports = greet; // hello.js 代码读取结束 return module.exports; }; var exported = load(module); // 保存 module save(module, exported); 可见,变量 module 是 nodejs 在加载 .js 文件之前准备的一个变量,并将其传入加载函数,我们在 hello.js 中可以直接使用变量 module 原因就在于它实际上是加载函数的一个参数。\n通过把参数 module 传递给 load() 函数, hello.js 就顺利地把一个变量传递给了 node 执行环境,node 会把 module 变量保存到某个地方。\n由于 node 保存了所有导入的 module ,当我们用 require() 获取模块时,node 找到对应的 module ,把这个 module 的 exports 属性返回,如此,另一个模块就拿到了模块的输出。\n= 通俗来说,就是 node 加载模块时,在其可访问的环境(作用域)内挂载了一个 module 对象,如此该模块输出的 ‘对象’(变量、函数、对象等) 就可以赋给 module 对象的 exports 属性(即 module.exports)。*\nmodule.exports vs exports 我们只需要知道, module.exports 是 nodejs 为我们准备的用于模块输入的唯一真正的对象, exports 不过是对 module.exports 的引用罢了。这也就决定了,如下使用方式的对错:\nmodule.exports = { desc: \u0026#39;一个引用类型对象\u0026#39; }; // ✔ module.exports.var1 = variable; // ✔ exports.var1 = variable; // ✔ 并有改变 `exports` 的引用 exports = { desc: \u0026#39;一个引用类型对象\u0026#39; }; // ✘ 已改变了 `exports` 的引用 // 你改变了 `exports` 的指向,它就不再指向 `module.exports` 了, // 自然也就不是模块的输出对象了, // 记住,模块的输出对象只有一个 *注:强烈建议只使用 module.exports 方式来输出模块变量!\nes module3 在 es6 之前,社区制定了一些模块加载方案,最主要的有 commonjs(node 服务器端) 和 amd (浏览器端)两种。\n有一个好消息,es6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 commonjs 和 amd 规范,成为浏览器和服务器通用的模块解决方案。\nes module vs commonjs es module 模块的设计思想是尽量的 静态化 ,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。commonjs 和 amd 模块,都只能在运行时确定这些东东。比如,commonjs 模块输出的就是 module.exports 对象,输入时必须查找对象属性。\n= 有了 es6 静态加载,使得静态分析成为可能,就能进一步拓宽 javascript 的语法,比如引入宏(macro)和类型系统(type system)。*\n❓ 想一想,为什么 commonjs 和 amd 只有在运行时才能确定模块的依赖关系及输入输出变量呢?\r✔️ 因为它们本质上都是“立即执行函数”(iife),不执行自然无法得到! let { stat, exists, readfile } = require(\u0026#39;fs\u0026#39;); // 等同于 let _fs = require(\u0026#39;fs\u0026#39;); // 引入了整个 `fs` 模块的整个输出对象 let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile; 上面代码的实质是整体加载 fs 模块(即加载 fs 模块的整个输出对象 module.exports ),生成一个对象 _fs ,然后再从这个对象上读取 3 个方法。\n这种加载称为 运行时加载 ,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。\n= 所谓“静态化”,没那么玄乎,不过就是在运行之前,编译阶段就可以确定的一些东东。*\n与 commonjs 模块输出的是 module.exports 对象不同的时,es module 模块输出的并不是对象,而是 通过 export 命令显式指定输出的代码,再通过 import 命令输入 。如下:\n// es module import { stat, exists, readfile } from \u0026#39;fs\u0026#39;; 其实质是从 fs 模块加载 3 个方法, 其他方法不加载 ,即它无法引用 es module 模块本身(因为它不是对象),这种加载称为 编译时加载 (或静态加载)。\nexport es module 模块功能主要由两个命令构成: export 和 import :\nexport 命令用于规定模块的对外接口; import 命令用于输入其他模块提供的功能。 export、import 命令可以出现在模块的任何位置,只要处于模块顶层就可以,因为处在代码块中,就没法做静态优化了。\n*注: export 命令显式指定输出的代码,对,是代码!代码!代码!\n= 其实,看一下基本的编译原理,就很容易理解这个地方了,export 输出的其实是一个动态外链。*\nexport var year = 1990; // ✔ // or var year = 1990; export { year }; // ✔ // 还可以用别名输出 export { year as yearalias1 }; // ✔ export { year as yearalias2 }; // ✔ 如上, export 导出的都是 var year = 1990 这行代码,而不是 year 这个变量。\n如果,你像在下面这样使用,就会报错了,如:\nexport 1 // ✘ var m = 1 export m // ✘ 想一下,为会什么会报错呢?因为它们 不是有意义的代码 呗! 1 只是一个字面量,引用它明显没有什么意义,同样 m 中存的也是 1 。\n同理,对于函数而言,也是一样的,如下:\nexport function f() {} // ✔ // or function f() {} export { f } // ✔ export { f as fn } // ✔ export f // ✘ export 语句输出的接口,与其对应的值是动态绑定关系。 即通过该接口,可以取到模块内部实时的值。这一点与 commonjs 规范完全不同,commonjs 模块输出的是值的缓存,不存在动态更新。\nexport var foo = \u0026#39;bar\u0026#39;; settimeout(() =\u0026gt; (foo = \u0026#39;baz\u0026#39;), 500); 上面代码输出变量 foo ,值为 bar ,500 毫秒后变成 baz 。\nimport 前面我们讲了输出 export ,这个章节我们来认识一下输入 import 。\nimport 命令接受一对大括号,里面指定要从其他模块导入的变量名,该变量名必须与被导入模块对象接口(输出的大括号是的变量啦)名称相同。其用法实例如下:\nimport { year } from \u0026#39;./profile.js\u0026#39;; // ✔ import { year as yearalias } from \u0026#39;./profile.js\u0026#39;; // ✔ import \u0026#39;loadsh\u0026#39;; // ✔ 只是执行 loadsh 模块,不输入任何值 除了指定加载某个输出值,还可以使用 整体加载 ,即用星号( * )指定一个对象,所有输出值都加载在这个对象上面。\nimport * as circle from \u0026#39;./circle\u0026#39;; circle.area(4); // 调用 circle 的 area() 方法计算面积 circle.circumference(14); // 调用 circle 的 circumference() 方法计算周长 export default 前面说过,使用 import 命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 export default 命令,为模块指定默认输出。\n*注: 使用 export default 输出的代码,用 import 命令引入时,不使用大括号。\n// export-default.js /// // 匿名函数 export default function() {} // ✔ // 具名函数 export default function foo() {} // ✔ // or function foo() {} export default foo // ✔ // import-default.js /// import baz from \u0026#39;./exort-default\u0026#39; // ✔ `baz` 可以是任何自定义的变量名 但是,对于使用 export 命令时,就不能省略大括号,如下:\n// export.js /// export function foo() {} // ✔ function foo() {} export { foo } // ✔ // import.js /// import { foo } from \u0026#39;./export.js\u0026#39; // ✔ import foo form \u0026#39;./export.js\u0026#39; // ✘ 本质上, export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。\n// export.js /// function add(x, y) { return x + y; } export { add as default }; // ✔ // 等同于 // export default add // ✔ // import.js /// import { default as foo } from \u0026#39;./export.js\u0026#39;; // ✔ // 等同于 // import foo from \u0026#39;./export.js\u0026#39; // ✔ 同样,因为 export default 命令其实只是输出一个叫做 default 的变量,所以它后面不能跟变量声明语句。\nexport var a = 1 // ✔ var a = 1 export default a // ✔ export default var a = 1 // ✘ export 与 import 的复合写法 如果在一个模块之中,先输入后输出同一个模块, import 语句可以与 export 语句写在一起。\nexport { foo, bar } from \u0026#39;my_module\u0026#39;; // 等价于 import { foo, bar } from \u0026#39;my_module\u0026#39;; export { foo, bar }; 上面代码中, export 和 import 语句可以结合在一起,写成一行。\n但需要注意的是,写成一行以后, foo 和 bar 实际上 并没有被导入当前模块,只是相当于对外转发 了这两个接口,导致当前模块不能直接使用 foo 和 bar 。\n模块的接口改名和整体输出,也可以采用这种写法。\n// 接口改名 export { foo as myfoo } from \u0026#39;my_module\u0026#39;; // 整体输出 export * from \u0026#39;my_module\u0026#39;; export * as ns from \u0026#34;mod\u0026#34;; // 默认接口的写法 export { default } from \u0026#39;foo\u0026#39;; // 具名接口改为默认接口的写法 export { es6 as default } from \u0026#39;./somemodule\u0026#39;; // 等同于 import { es6 } from \u0026#39;./somemodule\u0026#39;; export default es6; // 默认接口也可以改名为具名接口 export { default as es6 } from \u0026#39;./somemodule\u0026#39;; import() 前面介绍过, import 命令会被 javascript 引擎静态分析,先于模块内的其他语句执行。\n这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载。在语法上,条件加载就不可能实现。如果 import 命令要取代 node 的 require 方法,就形成了一个障碍。\n// 报错 if (x === 2) { import mymodule from \u0026#39;./mymodule\u0026#39;; // ✘ `import` 命令不支持动态加载 } es2020 提案引入了 import() 函数,支持动态加载模块。\nimport(specifier) // `specifier` 指定所要加载的模块的位置 import() 函数和 import 命令接受的参数相同,区别在于 import() 可以动态加载,且与所加载的模块并没有静态链接关系, 返回一个 promise 对象 。\nimport() 函数类似于 node 的 require 方法,区别在于 import() 是异步加载,而 require 是同步加载。\nif (condition) { import(\u0026#39;modulea\u0026#39;).then(/* ... */); // ✔ } else { import(\u0026#39;moduleb\u0026#39;).then(/* ... */); // ✔ } *注: import() 加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数。\nwebpack module4 在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of funtionality),并称之为模块。\n每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。\nnodejs 从一开始就支持模块化编程,而 web 浏览器对于模块化的支持却姗姗来迟。在 web 存在多种支持 javascript 模块化的工具,如 amd、cmd 等,这些工具各有优势和限制。\nwebpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。\n什么是 webpack 模块呢?\nwebpack 模块能够以各种方式表达模块之间的依赖关系,如:\nes2015 的 import 语句; commonjs 的 require() 语句; amd 的 define 和 require 语句; css/sass/less 文件中的 @import 语句; 样式 url(...) 或 html 文件 \u0026lt;img src=...\u0026gt; 中的图片链接。 webpack 是怎么做到这一点的呢? loader !\nwebpack 通过 loader 可以支持各种语言和预处理器编写模块。loader 描述了 webpack 如何处理非 javascript 的模块,并且在 bundle 中引入这些依赖。另外,webpack 社区已经为各种就行语言和语言处理器构建了 loader ,如: coffeescript、typescript、esnext(babel)、sass、less 等。\n*注:有关完整列表,请参考 loader 列表 或 自己编写 。\n","date":"2020-12-11","permalink":"https://loveminimal.github.io/posts/modular-programming/","summary":"\u003cp\u003ei.e. Modular Programming\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e每一个领域的内容,都要有先问一下 \u0026ndash; 是什么?为什么?做什么?怎么做?\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e是什么?简述概念;\u003c/li\u003e\n\u003cli\u003e为什么?历史发展,出现的原因,优缺点;\u003c/li\u003e\n\u003cli\u003e做什么?主要应用场景;\u003c/li\u003e\n\u003cli\u003e怎么做?具体使用方法步骤。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e","title":"模块化编程"},{"content":"before learning virtual dom, lets take a look at dom.\nwhat, exactly, is the dom?1 the document object model, or the \u0026ldquo;dom\u0026rdquo;, is an interface to web pages. it is essentially an api to the page, allowing programs to read an manipulate the page\u0026rsquo;s content, structure, and styles. let\u0026rsquo;w break this down.\nhow is a web page built? how a browser goes from a source html document to displaying a styled and interactive page in the viewport is called the \u0026ldquo;critical rendering path\u0026rdquo;. although this process can be broken down into several steps, as i cover in my article on understanding the critical rendering path, these steps can be roughy grouped into two stages.\nthe first stage involves the browser parsing the document to determine what will ultimately be rendered on the page, and the second stage involves the browser performing the render.\nthe result of the first stage is what is called a \u0026ldquo;render tree\u0026rdquo;.\nthe render tree is a representation of the html elements that will be rendered on the page and their related styles. in order to build this tree, the browser needs two things:\nthe cssom, a representation of the styles associated with elements; the dom, a representation of the elements. how is the dom created? and what does it look like?\nthe dom is an object-based representation of the source html document. it has some differences, as we will see below, but it is essentially an attempt to convert the structure and content of the html document into an object model that can be used by various programs.\nthe object structure of the dom is represented by what is called a \u0026ldquo;node tree\u0026rdquo;. it is so called because it can be thought of as a tree with a single parent stem that branches out into several child branches, each which may have leaves. in this case, the parent \u0026ldquo;stem\u0026rdquo; is the root == element, the child \u0026ldquo;branches\u0026rdquo; are the nested elements, and the \u0026ldquo;leaves\u0026rdquo; are the content within the elements.\nlet\u0026rsquo;s take this html document as an example:\n\u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;my first web page\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;how are you?\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; this document can be represented as the above node tree.\nwhat the dom is not in the example i gabe above, it seems like the dom is a 1-to-1 mapping of the source html document or what you see your devtools. however, as i mentioned, there are differences. in order to fully understand what the dom is, we need to look at what it is not.\n1.the dom is not your source html\nalthough the dom is created from the source html document, it is not always exactly the same. there are two instances in which the dom can be different from the source html.\n(1) when the html is not valid\nthe dom is an interface for valid html documents. during the process of creating the dom, the browser may correct some invalidities in the html code. like this:\n\u0026lt;!doctype html\u0026gt; \u0026lt;html\u0026gt; hello, world! \u0026lt;/html\u0026gt; the document is missing a \u0026lt;head\u0026gt; and \u0026lt;body\u0026gt; element, which is a requirement for valid html. if we look at the resulting dom tree, we will see that this has been corrected:\n(2) when the dom is modified by javascript\nbeside being an interface to viewing the content of an html document, the dom can alse modified, making it a living resource.\nwe can, for example, crate additional nodes to the dom using javascript.\nvar newparagraph = document.crateelement(\u0026#39;p\u0026#39;) var paragraphcontent = document.createtextnode(\u0026#39;i\\\u0026#39;m new!\u0026#39;) newparagraph.appendchild(paragraphcontent) document.body.appendchild(newparagraph) this will update the dom, but of course not our html document.\n2.the dom is not what you see in the browser (i.e., the render tree)\nwhat you see in the browser viewport is the render tree which, as i mentioned, is a combination of the dom and the cssom.\nwhat really separates the dom from the render tree, is that the latter only consists of what will eventually be painted on the screen.\nbecuase the render tree is only concerned with what is rendered, it excludes elements that are visually hidden. for example, elements that have =display: none= styles associated to them.\n\u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;hello, world!\u0026lt;/h1\u0026gt; \u0026lt;p style=\u0026#34;display: none;\u0026#34;\u0026gt;how are you?\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; the dom will include the \u0026lt;p\u0026gt; element:\nhowever, the render tree, and therefore what is seen in the viewport, will not include that element.\n3.the dom is not what is in devtools\nthis differentce is a bit more minuscule because the devtools element inspector provides the closest approximation to the dom that we have in the browser. however, the devtools inspector includes addtional information that isn\u0026rsquo;t in the dom.\nthe best example of this is css pseudo-elements. pseudo-elements created using the =::before= and =::after= selectors form part of the cssom and render tree, but are not technically part of the dom. this is because the dom is built from the source html document alone, not including the styles applied to the element.\ndespite the fact that pseudo-elements are not part of the dom, they are in our devtools element inspector.\nthis is why pseudo-elements cannot be targetted by javascript, because they are not part of the dom.\nrecap the dom is an interface to an html document. it is used by browsers as a first step towards determining what to render in the viewport, and by javascript programs to modify the content, structure, or styling of the page.\nalthough similar to other forms of the source html document, the dom is different in a number of ways:\nit is always valid html it is a living model that can be modifed by javascript it doesn\u0026rsquo;t include pseudo-elements (e.g. ::after) it does include hidden elements (e.g. with display: none) understanding the critical rendering path2 when a browser receives the html response for a page from the server, there are lot of steps to be taken before pixels are drawn on the screen. this sequence the browser needs to run through for the initial paint of the page is called the \u0026ldquo;critical rendering path\u0026rdquo;.\n*tip: we will use crp represent critical rendering path.\nknowledge of the crp is incredibly useful for understanding how a site\u0026rsquo;s performance can be improved. there are 6 stages to the crp:\n1. constructing the dom tree\r2. construting the cssom tree\r3. running javascript\r4. creating the render tree\r5. generating the layout\r6. painting 1.constructing the dom tree the dom (document object model) tree is an object representation of the fully parsed html page.\nstarting with the root element, \u0026lt;html\u0026gt;, nodes are created for each element/text on the page. elements nested within other elements are represented as child nodes and each node contains the full attributes for that element. for example, an \u0026lt;a\u0026gt; element will have the href attribute associated with it\u0026rsquo;s node.\ntake, for example, this sample document:\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;understanding the critical rendering path\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;header\u0026gt; \u0026lt;h1\u0026gt;understanding the critical rendering path\u0026lt;/h1\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;main\u0026gt; \u0026lt;h2\u0026gt;introduction\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;lorem ipsum dolor sit amet\u0026lt;/p\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;footer\u0026gt; \u0026lt;small\u0026gt;copyright 2017\u0026lt;/small\u0026gt; \u0026lt;/footer\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; this will create the following dom tree:\na good thing about html is that it can be executed in parts. the full document doesn\u0026rsquo;t have to be loaded for content to start appearing on the page. however, other resources, css and javascript, can block the render of the page.\n2.constructing the cssom tree the cssom (css object model) is an object representation of the styles associated with the dom. it is represented in a similar way to the dom, but with the associated styles for each node, whether they explicitly declared or implicitly inherited, included.\nin the =tyle.css file from the document mentioned above, we have the following styles:\nbody { font-size: 18px; } header { color: plum; } h1 { font-size: 28px; } main { color: firebrick; } h2 { font-size: 20px; } footer { display: none; } this will create the following cssom tree:\ncss is considered a \u0026ldquo;render blocking resource\u0026rdquo;. this means that the [[*4.creating the render tree][render tree (see below)]] cannot be constructed without first fully parsing the resource.\nunlike html, css cannot be used in parts because of its inherit cascading nature. styles defined later in the document can override and change styles that were previously defined. so, if we start using css styles defined earlier in the stylesheet before the entirety of the stylesheet has been parsed, we may get a situation where the wrong css is being applied.\nthis means that css must be fully parsed before we can move on to the next stage.\ncss files are only considered render blocking if they apply to the current device. the \u0026lt;link rel=\u0026quot;stylesheet\u0026quot;\u0026gt; tag can accept a media attribute, in which we can specify any media query which the styles within apply to. if, for example, we have a stylesheet with a media attribute of orientation: landscape, and we are viewing the page in portrait mode, that resource will not be considered render blocking.\ncss can also be \u0026ldquo;script blocking\u0026rdquo;. this is because javascript files must wait until the cssom has been constructed before it can run.\n3.running javascript javascript is considered a \u0026ldquo;parser blocking resource\u0026rdquo;. this means that the parsing of the html document itself is blocked by javascript.\nwhen the parser reaches a \u0026lt;script\u0026gt; tag, whether that be internal or external, it stops to fetch (if it is external) and run it. this why, if we have a javascript file that references elements within the document, it must be placed after the appearance of that document.\nto avoid javascript being parse blocking, it can be loaded asynchronously be applying the async attribute.\n\u0026lt;script async src=\u0026#34;script.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 4.creating the render tree the render tree is a combination of both the dom and cssom. it is a tree that represents what will be eventually rendered on the page. this means that it only captures the visible content and will not include, for example, elements that have been hidden with css using display: none.\nusing the example dom and cssom above, the following render tree will be created:\n5.generating the layout the layout is what determines what the size of the viewport is , which provides context for css styles that are dependent on it, e.g. percentage or viewport units. the viewport size is determined by meta viewport tag provided in the document head or, if no tag is provided, the default viewport width of 980px is used.\nfor example, the most common meta viewport value is to set the viewport size to correspond to the device width:\n\u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width,initial-scale=1\u0026#34;\u0026gt; if the user visits the webpage on a device with a width of, for example, 1000px, then sizes will be based on that unit. half the viewport will be 500px, 10vw will be 100px, and so on.\n6.painting finally, in the painting step, the visible content of the page can be converted to pixels to be displayed on the screen.\nhow much time the paint step takes depends on the size of the dom, as well as what styles are applied. some styles require more work to execute than others. for example, a complicated gradient background-image will require more time than a simple solid background colour.\nputting it all together to see the critical rendering path in process, we can inspect it in devtools. in chrome, it is under the timeline tab (in canary, and soon to be chrome stable, it\u0026rsquo;s renamed performance).\ntake for example, our sample html from above (width an added \u0026lt;script\u0026gt; tag):\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;understanding the critical rendering path\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;header\u0026gt; \u0026lt;h1\u0026gt;understanding the critical rendering path\u0026lt;/h1\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;main\u0026gt; \u0026lt;h2\u0026gt;introduction\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;lorem ipsum dolor sit amet\u0026lt;/p\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;footer\u0026gt; \u0026lt;small\u0026gt;copyright 2017\u0026lt;/small\u0026gt; \u0026lt;/footer\u0026gt; \u0026lt;script src=\u0026#34;main.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; if we look at the event log for the page load, this is what we get:\nsend request - get request sent for index.html parse html and send request - begin parsing of html and dom construction. send get request for style.css and main.js parse stylesheet - cssom created for style.css evaluate script - evaluate main.js layout - generate layout based on meta viewport tag in html paint - paint pixels on document based on this information, we can make decisions on how to optimize the critical rendering path. i will go into some of these techniques in later articles.\nunderstanding the virtual dom3 i\u0026rsquo;ve recently been writing about what exactly the dom and the shodow dom are and how they differ.\nto recap, the document object model is an object-based representation of an html document and an interface to mainpulating that object.\nthe shadow dom can be thought of as a \u0026ldquo;lite\u0026rdquo; version of the dom. it is also an object-based representation of html elements, but not of a full standalone document. instead, the shadow dom allows us to separate our dom into smaller, encapsulated bits that can be used across html documents.\nanother similar term you may have come across is the \u0026ldquo;virtual dom\u0026rdquo;.\nalthough the concep has been around for several years, it was made more popular by its use in the react framework. in this article, i will cover exactly what the virtual dom is, how it differs from the original dom, and how it it used.\nwhy do we need a virtual dom? to understand why the concept of the virtual dom arose, let\u0026rsquo;s revisit the original dom.\nas i mentioned, there are two parts to the dom:\nthe object-based representation of the html document, and the api to manipulating that object. for example, let\u0026rsquo;s take this simple html document with an unordered list and one list item.\n\u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;ul class=\u0026#34;list\u0026#34;\u0026gt; \u0026lt;li class=\u0026#34;list__item\u0026#34;\u0026gt;list item\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; this document can be represented as the following dom tree:\nlet\u0026rsquo;s say we want to modify the content of the first list item to \u0026ldquo;list item one\u0026rdquo; and also add a second list item. to do this, we will need use the dom apis to find the elements we want to update, create the new elements, add attributes and content, then finally update the dom elements themselves.\nconst listitemone = document.getelementsbyclassname(\u0026#39;list__item\u0026#39;)[0] listitemone.textcontent = \u0026#39;list item one\u0026#39; const list = document.getelementsbyclassname(\u0026#39;list\u0026#39;)[0] const listitemtwo = document.createelement(\u0026#39;li\u0026#39;) listitemtwo.classlist.add(\u0026#39;list__item\u0026#39;) listitemtwo.textcontent = \u0026#39;list item two\u0026#39; list.appendchild(listitemtwo) the dom wasn\u0026rsquo;t made for this\u0026hellip;\nwhen the first specification for the dom was released in 1998, we built and managed web pages in very differently. there was far less reliance on the dom apis to create and update the page content as frequently as we do today.\nsimple methods such as =document.getelementsbyclassname()= are fine to use on a small scale, but if we are updating muliple elements on a page every few seconds, it can start to become really expensive to constantly query and update the dom.\neven further, because of the way the apis are setup, it is usually simpler to perform more expensive operations where we update larger parts of the doucment than to find and update the specific elements. going back to our list example, it is in some ways easier to replace the entire unordered list with a new one than to modify the specific elements.\nconst list = document.getelementsbyclassname(\u0026#39;list\u0026#39;)[0] list.innerhtml = ` \u0026lt;li class=\u0026#34;list__item\u0026#34;\u0026gt;list item one\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;list__item\u0026#34;\u0026gt;list item two\u0026lt;/li\u0026gt; ` in this particular example, the performance difference between the methods is probably insignificant. however, as the size of the web page grows, it becomes more important to only select and update what is needed.\n\u0026hellip;but the virtual dom was!\nthe virtual dom was created to solve these problems of needing to frequently update the dom in a more performant way. unlike the dom or the shadow dom, the virtual dom isn\u0026rsquo;t an official specification, but rather a new method of interfacing with the dom.\na virtual dom can be thought of as a copy of the original dom. this copy can be frequently manipulated and upated, without using the dom apis. once all the updates have been made to the virtual dom, we can look at what specific changes need to be made to the original dom and make them in a targetted and optimised way.\nwhat does a virtual dom look like? the name \u0026ldquo;virtual dom\u0026rdquo; tends to add to the mystery of what the concept actually is. in fact, a virtual dom is just a regular javascript object.\nlet\u0026rsquo;s revisit the dom tree we created earlier:\nthis tree can also be represented as a javascript object.\nconst vdom = { tagname: \u0026#39;html\u0026#39;, children: [ { tagname: \u0026#39;head\u0026#39;}, { tagname: \u0026#39;body\u0026#39;, children: [ { tagname: \u0026#39;ul\u0026#39;, attributes: { class: \u0026#39;list\u0026#39; }, children: [ { tagname: \u0026#39;li\u0026#39;, attributes: { class: \u0026#39;list__item\u0026#39; }, textcontent: \u0026#39;list item\u0026#39; } // end li ] } // end ul ] } // end body ] } // end html we can think of this object as our virtual dom. like the original dom, it is an object-based representation of our html document. but since it is a plain javascript object, we can manipulate it freely and frequently without touching the actual dom until we need to.\ninstead of using one object for the entire object, it is more common to work with small sections of the virtual dom. for example, we may work on a list component, which would corespond to our unordered list element.\nconst list = { tabname: \u0026#39;ul\u0026#39;, attributes: { class: \u0026#39;list\u0026#39; }, children: [ { tagname: \u0026#39;li\u0026#39;, attributes: { class: \u0026#39;list__item\u0026#39; }, textcontent: \u0026#39;list item\u0026#39; } ] } under the hood of the virtual dom now that we\u0026rsquo;ve seen what a virtual dom looks like, how does it work to solve the performance and usability problems of the dom?\nas i mentioned, we can use the virtual dom to single out the specific changes that need to be made to the dom and make those specific updates alone. let\u0026rsquo;s go back to our unordered list example and make the same changes we did using the dom api.\nthe first thing we would do is make a copy of the virtual dom, containing the changes we want to make. since we don\u0026rsquo;t need to use the dom apis, we can actually just create a new object alltogether.\nconst copy = { tagname: \u0026#39;ul\u0026#39;, attributes: { class: \u0026#39;list\u0026#39; }, children: [ { tagname: \u0026#39;li\u0026#39;, attributes: { class: \u0026#39;list__item\u0026#39; }, textcontent: \u0026#39;list item one\u0026#39; }, { tagname: \u0026#39;li\u0026#39;, attributes: { class: \u0026#39;list__item\u0026#39; }, textcontent: \u0026#39;list item two\u0026#39; } ] } this copy is used to create what is called a \u0026ldquo;diff\u0026rdquo; between the original virtual dom, in this case the list , and the updated one. a diff could look something like this:\nconst diffs = [ { newnode: { /* new version of list item one */ }, oldnode: { /* original version of list item one */ }, index: /* index of element in parent\u0026#39;s list of child nodes */ }, { newnode: { /* list item two */ }, index: { /* */ } } ] this diff provides instructions for how to update the actual dom. once all the diffs are collected, we can batch changes to the dom, making only the updates that are needed.\nfor example we could loop through each diff and either add a new child or update an old one depending on what the diff specifies.\nconst domelement = document.getelementsbyclassname(\u0026#39;list\u0026#39;)[0] diffs.foreach((diff) =\u0026gt; { cosnt newelement = document.createelement(diff.newnode.tagname) /* add attributes ... */ if (diff.oldnode) { // if there is an old version, replace it with the new version domelement.replacechild(diff.newnode, diff.index) } else { // if no old version exists, create a new node domelement.appendchild(diffnode) } }) note that this is a really simplified and stripped-back version of how a virtual dom could work and there are lot of cases i didn\u0026rsquo;t cover here.\nthe virtual dom and frameworks it\u0026rsquo;s more common to work with the virtual dom via a framework, rather than interfacing with it directly as i showed in the example above.\nframeworks such as react and vue use the virtual dom concept to make more performant updates to the dom. for example, our list component can be written in react in the following way.\nimport react from \u0026#39;react\u0026#39; import reactdom from \u0026#39;react-dom\u0026#39; const list = react.createelement( \u0026#39;ul\u0026#39;, { classname: \u0026#39;list\u0026#39; }, react.createelement(\u0026#39;li\u0026#39;, { classname: \u0026#39;list__item\u0026#39; }, \u0026#39;list item\u0026#39;) ) reactdom.render(list, document.body) if we wanted to update our list, we could just rewrite the entire list template, and call =reactdom.render()= again, passing in the new list.\nconst newlist = react.createelement( \u0026#39;ul\u0026#39;, { classname: \u0026#39;list\u0026#39; }, react.createelement(\u0026#39;li\u0026#39;, { classname: \u0026#39;list__item\u0026#39; }, \u0026#39;list item one\u0026#39;), react.createelement(\u0026#39;li\u0026#39;, { classname: \u0026#39;list__item\u0026#39; }, \u0026#39;list item two\u0026#39;) ) settimeout(() =\u0026gt; reactdom.render(newlist, document.body), 5000) because react uses the virtual dom, even though we are re-rendering the entire template, only the parts that actually change are updated. if we look at our developer tools when the change happens, we will see the specific elements and the specific parts of the elements that change.\nthe dom vs the virtual dom to recap, the virtual dom is a tool that enables us to interface with dom elements in an easier and more performant way. it is a javascript object representation of the dom, which we can modify as frequently as we need to. changes made to this object are then collated, and modifications to the actual dom are targetted and made less often.\n","date":"2020-12-02","permalink":"https://loveminimal.github.io/posts/virtual-dom/","summary":"\u003cp\u003eBefore learning Virtual DOM, lets take a look at DOM.\u003c/p\u003e","title":"virtual dom"},{"content":"🔔 转载自 如何打造一款标准的 js-sdk\nsdk(software development kit,软件开发工具包),一般都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。简单讲, 面向开发者,针对特定领域的软件包 ,基于它,开发人员可以快速构建自己的应用 app 。\n*注:比较规范的 sdk 一般都会包含若干的 api 、开发工具集和说明文档。\n本文主要介绍如何基于 javascript 来开发的 sdk ,鉴于 js 语言本身的特性,基于它封装的 sdk 更多常见于 ui 组件库、统计分析、web 服务接口封装、前端稳定性和性能监控等场景。\n设计原则 如何设计 sdk ? 更多取决于其应用场景和最终用途。比如,实现一个给网页调用的 sdk 与用于服务端的 sdk 就有明显的差异,但综们之间存在着一些共通的原则:\n最小可用性原则,即用最少的代码,如无必要勿增实体; 最少依赖性原则,即最低限度的外部依赖,如无必要勿增依赖。 进一步阐述,即我们打造的 sdk 要符合以下的要求。\n1. 满足功能需求\nsdk 一般都是偏向于某个领域,所以,同时在设计和实现的时候明确职责和边界很重要,同时还应该足够精简,专注领域的业务。\n2. 足够稳定\n绝不能导致宿主应用崩溃(最基础、最严格的要求); 较好的性能,比如 sdk 体积应尽量小,运行速度尽量快; 可测试,保障每一次变更; 向后兼容,不轻易出现 breakchange 。 3. 少依赖,易扩展\n最小程度的第三方依赖,尽可能自行实现,确实无法避免则最小化引入; 插件化,最大限度支持扩展; hook 机制,满足个性化诉求。 如何实现 下面我们将通过剖析岳鹰前端监控 sdk 的设计过程,来看看上述的设计原则是如何应用到实际的开发过程中的。\n明职责,定边界 岳鹰前端监控 sdk 是前端稳定性和性能监控的 sdk,主要面向前端 h5 领域。因此,稍加分析即可得出以下结论:\n前端领域,稳定性方面主要的关注点: js 异常; 资源加载异常; api 请求异常; 白屏异常; 性能方面,核心的关注点: 白屏时间; 可交互时间(tti); 首屏时间; fp/fmp/fcp 等。 上述监控内容实际上都相对独立,因此我们可以把 ta 们横向划分为如下几大部分:\n明确了 sdk 的边界以及各部分的职责,结合前端监控的特性,我们可以开始设计 sdk 的整体框架了。\n筑框架,夯基础 1. 确定 sdk 的引用形式\n前端模块有多种表现形式:\n- es module\r- commonjs\r- amd/cmd/umd 而在引用方面大体分为:cdn 和 npm 两种分发方式,即无论我们实现的是哪种形式的模块,最终都是通过这两种方式提供给用户引用。\nimport wpkreporter from \u0026#39;wpkreporter\u0026#39; // commonjs const wpkreporter = require(\u0026#39;wpkreporter\u0026#39;) // amd requirejs 引用 require.config({ paths: { \u0026#39;wpk\u0026#39;: \u0026#39;https://g.alicdn.com/woodpeckerx/jssdk/wpkreporter.js\u0026#39;, } }) require([\u0026#39;wpk\u0026#39;, \u0026#39;test\u0026#39;], function(wpk) { // do your business }) 有点烦琐,但事实上今时今日的前端工程领域,已有很多利器可以帮助我们达到目的。比如, webpack 通过简单的配置就可以构建出一个 umd 的 bundule,可以自动适配所有形式的模块:\nmodule.exports = { output: { filename: \u0026#39;[name].js\u0026#39;, path: `${__dirname}/dist`, globalobject: \u0026#39;this\u0026#39;, library: \u0026#39;[name]\u0026#39;, librarytarget: \u0026#39;umd\u0026#39; } } 2. 确定 sdk 的版本管理机制\n现有较成熟的版本管理机制当属 语义化版本号 ,表现形式为 {主版本}.{次版本}.{补丁版本} ,简单易记好管理。\n一般重大的变更才会触发主版本号的更替,而且很可能新旧版本不兼容; 次版本主要对应新特性或者较大的调整,因此也有可能出现 breakchange ; 其他小的优化或 bugfix 就基本都是在补丁版本号体现。 所有 npm 模块都遵循语义化版本规范,因此结合第一点,我们可以将 sdk 初始化为一个 npm 模块,结合 webpack 的能力就可以实现基础的版本管理及模块构建。\n3. 确定 sdk 的基础接口\n接口是 sdk 和用户沟通的桥梁,每一个接口对应着一个独立的 sdk 功能,并且有明确的输入和输出。\n我们可以先来看看岳鹰前端监控 sdk 的核心接口有哪些?\nwpk.report(logdata) wpk.reportjserror(error) wpk.reportapierror(apidata) // 配置变更 wpk.setconfig(data) // sdk 诊断 wpk.diagnose() // 添加插件 wpk.addplugin(plugin) 总结一下接口的设计原则,大致如下:\n职责单一,一个接口只做一件事情; 命名简单清晰,参数尽量少但可扩展; 参数尽可能使用 object 封装; 做好参数校验和逻辑保护。 好的接口命名就是最好的注释,一看即明白其用处。\n领域分析,模块划分 确定边界的时候,我们已经清楚划分了 sdk 的几个关键部分:全局异常、api 异常、页面性能和白屏, 每一块都对应一个专业的领域 ,因此对应到 sdk 也是每一个独立的模块。\n实际上监控 sdk 通常也会内置对页面就是的监控,以方便用户对异常的影响面做出评估。除了这些核心的偏领域的模块,sdk 还需要更基础的与领域无关的模块,包括:\n- sdk 内核:\r- 构造方法\r- 插件机制\r- 与下游的交互\r- 上报队列机制\r- 不同环境和管理等等\r- 工具库 我们来看一下岳鹰前端监控 sdk 最后的整体模块划分:\nsdk 底层提供基础的能力,包括上面提到的内核、插件机制的实现、工具类库以及暴露给用户的基础 api 。\n可以看到,我们前面提到的所有模块都 以插件的形式存在 ,即各个领域的功能都各自松散的做实现,这样使得底层能力更具有通用性,同时扩展能力也更强,用户甚至也可以封装自己的插件。\nbiz 部分更多是对于不同宿主环境的多入口适配,当前支持浏览器 、weex 以及 nodejs 。\n测试覆盖,线上无忧 sdk 是一个基础服务,相对于前台业务而言可能更底层些。其影响面跟应用的范围是正比的关系,更多的用户意味着更大的责任。所以 sdk 的质量保障也是很重要的一个环节。 岳鹰前端监控 sdk 的质量保障策略很简单,只有两条:\n核心接口 100% 的单元测试覆盖率; 发布卡点:再小的版本发布也需要走集成测试回归。 事实上,除了核心接口,工具类库的所有功能我们都实现了 100% 的单元测试覆盖,我们采用的前端测试工具是轻量好用的 jest 。\ntest(\u0026#39;iserror: real error\u0026#39;, function () { var err = new error(\u0026#39;this is an error\u0026#39;) expect(util.iserror(err)).tobetruthy() }) 细节打磨,极致体验 快捷引入:\n极尽所能提高用户引用的效率; 一行代码,快速引入,享用监控全家桶功能。 \u0026lt;script\u0026gt; !(function(c,i,e,b){var h=i.createelement(\u0026#34;script\u0026#34;);var f=i.getelementsbytagname(\u0026#34;script\u0026#34;)[0];h.type=\u0026#34;text/javascript\u0026#34;;h.crossorigin=true;h.onload=function(){c[b]||(c[b]=new c.wpkreporter({bid:\u0026#34;dta_1_203933078\u0026#34;}));c[b].installall()};f.parentnode.insertbefore(h,f);h.src=e})(window,document,\u0026#34;https://g.alicdn.com/woodpeckerx/jssdk/wpkreporter.js\u0026#34;,\u0026#34;__wpk\u0026#34;); \u0026lt;/script\u0026gt; 动态采样:\n即通过云端下发数据采样率的方式,控制客户端上报数据的频率; 更好的保护监控下游。 自我诊断:\n除了接口,sdk 整体对用户而言就是一个黑盒,因此用户在遇到问题时很容易蒙圈 (如:为啥没有上报数据); sdk 可以提供一个自我诊断的接口,快速排除基础问题。比如,sdk 是否已正常初始化、关键参数是否正常设置等。 增加调试模式,输出更详细的过程日志,方便定位问题。\n渐进式的指引文档:\n图文并茂,循序渐进; 入门,一步步引导用户初识 sdk,领略概貌,学会基本的使用; 进阶,安利 sdk 的深度用法,帮助用户更好的使用 sdk 。 结语 实际在 sdk 的设计和开发过程中,要处理的问题还远不止本文所述的内容,比如 npm 模块开发时本地如何引用,构建的 bundle 大小如何调优等等。不过还是希望阅完此文,对你有所启发。\n","date":"2020-12-01","permalink":"https://loveminimal.github.io/posts/sdk/","summary":"\u003cp\u003e🔔 转载自 \u003ca href=\"https://zhuanlan.zhihu.com/p/272614462\"\u003e如何打造一款标准的 JS-SDK\u003c/a\u003e\u003c/p\u003e\n\u003cimg src=\"/posts/sdk/imgs/sdk-1.jpg\" width=\"400\" style=\"float: right; margin-left: 16px;\" /\u003e\r\n\u003cp\u003eSDK(Software Development Kit,软件开发工具包),一般都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。简单讲, \u003cstrong\u003e面向开发者,针对特定领域的软件包\u003c/strong\u003e ,基于它,开发人员可以快速构建自己的应用 App 。\u003c/p\u003e\n\u003cp\u003e*\u003cstrong\u003e注:比较规范的 SDK 一般都会包含若干的 API 、开发工具集和说明文档。\u003c/strong\u003e\u003c/p\u003e","title":"打造 sdk"},{"content":"什么是 ajax ?\najax(asynchronous javascript and xml),是一种异步请求数据的 web 开发技术。它不是新的编程语言,而是一种使用现有标准的新方法。其最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。\n*注:ajax 不需要任何浏览器插件,但需要用户允许 javascript 在浏览器上执行。\n*进程、线程 1 在讲述 ajax 之前,有必要先了解一些进程和线程方面的知识。\r进程(process)和线程(thread)是操作系统的基本概念,比较抽象,不易掌握。\n对于操作系统,一个任务就是一个 _进程 (process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就是启动了两个记事本进程。\n有些进程不止同时只做一件事,比如 word ,它可以同时进行打字、拼写检查、打印等事情。\n在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为 _线程 (thread)。\n*注:一个进程至少有一个线程。\n哈哈,线程才是真正做事的喽。\r线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。\n多进程和多线程的程序涉及到同步、数据共享的问题,编写进来更复杂。\n阮一峰老师写过一篇 进程与线程的一个简单解释 也不错,浅显易懂,推荐一下。\n操作系统的设计,可以归结为三点:\n以多进程形式,允许多个任务同时进行; 以多线程形式,允许单个任务分成不同的部分进行; 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。 由此,也就容易理解,如果有很多任务需要执行,不外乎三种解决办法:\n排队 。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务; 新建进程 。使用 fork 命令,为每个任务新建一个进程; 新建线程 。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。 *注:一般情况下,一个进程一次只能执行一个任务(即包含一个线程)。\n单线程的 js 以 javascript 语言为例,它是一种单线程语言,所有任务都在一个线程上完成,任务只好排队喽。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现“假死”(javascript 停不下来,也就无法响应用户的行为)。\n那么,javascript 是如何解决这个问题的呢? 事件循环(event loop)。\nevent loop 是一个程序结构,用于等待和发送消息和事件。\n简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为“主线程”;另一个负责主线程与其他线程(主要是各种 i/o 操作)的通信,被称为“event loop 线程”(或“消息线程”)。\n如右图所示,主线程的绿色部分,表示运行时间,橙色部分表示空闲时间。每当遇到 i/o 的时候,主线程就让 event loop 线程去通知相应的 i/o 程序,然后接着往后运行,所以不存在红色的等待时间。等到 i/o 程序完成操作,event loop 线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。\n不难看出,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行模式称为“异步模式”(asynchronous i/o)或“非堵塞模式”(non-blocking mode)。\n这正是 javascript 语言的运行方式 。\n我们前面说过了,执行多任务的话,还可以新建进程或线程,为什么 javascript 是单线程,难道不能实现多线程吗?有兴趣的话,可以自己去捉摸一下。\n多线程的浏览器 2 了解更多浏览器底层运行机制,可以阅读另一篇文章 - 浏览器 🌐\n前面我们已经大概了解了进程和线程,稍微回顾下相关概念。\n处理器(cpu)是计算机的核心,其负责承担计算机的计算任务。\n进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。\n*注:任一时刻,一个单核 cpu 只能运行一个进程,其他进程则处于非运行状态。\n线程是程序执行中单一和顺序流程,是程序执行的最小单元。\n那么,进程和线程是什么关系呢?\n(1)进程是操作系统分配资源的最小单位,线程是程序执行的最小单位;\n(2)一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;\n(3)进程之间相互独立,但同一个进程下和各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号);\n(4)调试和切换:线程上下文切换比进程上下文切换要快得多。\n_浏览器内核 是通过取得页面内容、整理信息(应用 css)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。\nchrome 浏览器为每个 tab 页面单独启用进程,在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)。\n浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:\n- gui 渲染线程;\r- js 引擎线程;\r- 事件触发线程;\r- 定时触发线程;\r- 异步 http 请求线程。 下面我们分别来了解下浏览器的这些常驻线程。\n_1. gui 渲染线程\ngui 渲染线程负责渲染浏览器界面 html 元素,解析 html、css,构建 dom 树和 renderobject 树,布局和绘制等。\n当界面需要重绘(repaint)或由于某种操作引发回流(重排 reflow)时,该线程就会执行。\n在 javascript 引擎运行脚本期间,gui 渲染线程都是处于挂起状态的,gui 更新会被保存在一个队列中等到 js 引擎空闲时立即被执行。\n_2. javascript 引擎线程\njavascript 引擎,也可以称为 js 内核,主要负责处理 javascript 脚本程序,如 v8 引擎。\njs 引擎一直等待着任务队列中的任务的到来,然后加以处理,一个 tab 页面(render 进程)中无论什么时候都只有一个 js 线程在运行 js 程序(因为 js 是单线程的啊)。\n注:gui 渲染线程和 javascript 引擎线程互斥!\n但是,为什么要互斥呢?\n由于 javascript 中可操纵 dom 的,如果在修改这些元素同时渲染界面(即 javascript 线程和 gui 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此,为了防止渲染出现不可预期的结果,浏览器就设置 gui 渲染线程与 javascript 引擎为互斥的关系了。\n_3. 事件触发线程\n当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 js 引擎的处理。\n这些事件可以是当前执行的代码块,如定时任务,也可以是来自浏览器内核的其他线程,如鼠标点击、ajax 异步请求等,但由于 js 的单线程关系,所有这些事件都得排队等待 js 引擎处理。\n_4. 定时触发器线程\n浏览器定时计数器并不是由 javascript 引擎计数的,因为 javascript 引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确。\n通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 js 引擎空闲后执行)。\n*注:w3c 在 html 标准中规定 settimeout 中低于 4ms 的时间间隔算为 4ms 。\n_5. 异步 http 请求线程\nxmlhttprequest 在连接后是通过浏览器新开一个线程请求,在检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,再由 javascript 引擎执行。\n*javascript 运行机制 3 学习一门语言,再怎么了解它的运行机制都是不为过的。\r在了解 ajax 之前,我们再通过以下几个方面,来加强一下对 javascript 的运行机制的认识。\n- javascript 是单线程的;\r- 任务队列;\r- 事件和回调函数;\r- event loop ;\r- 定时器;\r- nodejs 中的 event loop 。 为什么 js 是单线程 javascript 语言的一大特点就是单线程,即同一时间只能做一件事。前面我们已经讲述过为什么 javascript 不能有多个线程(多线程能提高效率啊),我们这里再稍微赘述一下。\n原来,javascript 的单线程,与它的用途有关。\n作为浏览器的脚本语言,javascript 的主要用途是与用户互动,以及操作 dom 。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 javascript 同时有两个线程,一个线程在某个 dom 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?\n所以,为了避免复杂性,从一诞生,javascript 就是单线程,这已经成了这门语言的核心特征。\n为了利用多核 cpu 的计算能力,html5 提出了 web worker 标准,允许 javascript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 dom 。所以,这个新标准并没有改变 javascript 单线程的本质。\n任务队列 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。\n如果排队是因为计算量大,cpu 忙不过来,倒也算了,但是多数时候 cpu 是闲着的,因为 i/o 设备很慢(比如 ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。\n明显对 cpu “压榨”的不够啊,嘿嘿……\rjavascript 语言的设计者意识到,这时主线程完全可以不管 i/o 设备,挂起处于等待中的任务,先运行排在后面的任务。等待 i/o 设备返回了结果,再回过头,把挂起的任务继续执行下去。\n于是,所有任务可以分成两种: _同步任务 (synchronous)和 _异步任务 (asynchronous)。\n同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。\n异步任务指的是,不进入主线程,而进入“任务队列”(task queue)的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。\n具体来说,异步执行的运行机制如下:\n(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。\n(2)主线程之外,还存在一个“任务队列”(task queue)。只要异步任务有了运行结果,就在“任务队列”这中放置一个事件。\n(3)一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件,哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。\n(4)主线程不断重复上面的第三步。\n只要主线程空了,就会去读取“任务队列”,这就是 javascript 的运行机制,且这个过程会不断重复。\n= 有一点需要进一步了解,就是类似于 async/await 这种语法糖的底层执行机制 ❓\n事件和回调函数 “任务队列”是一个事件的队列(也可以理解成消息的队列),i/o 设备完成一项任务,就在“任务队列”中添加一个事件,表示相关的异步任务可以进入“执行栈”了。主线程读取“任务队列”,就是读取里面有哪些事件。\n“任务队列”中的事件,除了 i/o 设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。\n所谓“回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始异步任务,就是执行对应的回调函数。\n“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上的自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的“定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。\nevent loop 主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 event loop (事件循环)。\n如上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部 api,它们在“任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取“任务队列”,依次执行那些事件所对应的回调函数。\n执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。\n定时器 除了放置异步任务的事件,“任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做“定时器”(timer)功能,也就是定时执行的代码。\n定时器功能主要由 settimeout() 和 setinterval() 这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。\n语法如下:\nsettimeout(fn, time) html5 标准规定了 settimeout() 的第二个参数的最小值(最短间隔),不得低于 4ms ,如果低于这个值,就会自动增加。\n*注: settimeout() 只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在 settimeout() 指定的时间执行。\nnodejs 的 event loop nodejs 也是单线程的 event loop,但是它的运行机制不同于浏览器环境。\n根据上图,nodejs 的运行机制如下:\n1. v8 引擎解析 javascript 脚本;\r2. 解析后的代码,调用 node api;\r3. libuv 库负责 node api 的执行;\r4. v8 引擎再将结果返回给用户。 其中, libuv 将不同的任务分配给不同的线程,形成一个 event loop ,以异步的方式将任务的执行结果返回给 v8 引擎。\n除了 settimeout 和 setinterval 这两个方法,nodejs 还提供了另外两个与“任务队列”有关的方法: process.nexttick 和 setimmediate ,它们可以帮助我们加深对“任务队列”的理解。\nprocess.nexttick 方法可以在当前“执行栈”的尾部 \u0026ndash; 下一次 event loop (主线程读取“任务队列”)之前 \u0026ndash; 触发回调函数。也就是说, 它指定的任务总是发生在所有异步任务之前。\nsetimmediate 方法则是当前“任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次 event loop 时执行,这与 settimeout(fn, 0) 很像。\n*注:如果有多个 process.nexttick 语句(不管它们是否嵌套),将全部在当前“执行栈”执行。即多个 process.nexttick 语句总是在当前“执行栈”一次执行完。\n由于 process.nexttick 指定的回调函数是在本次\u0026quot;事件循环\u0026quot;触发,而 setimmediate 指定的是在下次\u0026quot;事件循环\u0026quot;触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查\u0026quot;任务队列\u0026quot;)。\najax 异步的原理 hmm... 终于讲到 ajax 了……\r在正式了解 ajax 之前,我们先来看看它的原理。前面的章节中,我们了解了浏览器的多线程:gui 渲染线程、javascript 引擎线程、事件触发线程、http 请求线程、定时器触发线程。\n对于一个 ajax 请求:\n(1)javascript 引擎首先生成 xmlhttprequest 实例对象, open 过后再调用 send 方法。至此,所有的语句都是同步执行。\n(2)但是从 send 内部开始,浏览器为将要发生的网络请求创建了新的 http 请求线程,这个线程独立于 javascript 引擎线程,于是网络请求异步被发送出去了。另一方面,javascript 引擎并不会等待 ajax 发起的 http 请求收到结果,而是直接顺序往下执行。\n(3)当 http 请求收到 response 后,浏览器事件触发线程捕获到了 ajax 的回调事件,该回调事件并不会立即被执行,而是以先进先出的方式添加到任务队列的末尾,等到 javascript 引擎空闲时,任务队列中排队的任务将会依次被执行,循环读取事件。这些事件回调包括 settimeout、setinterval、click、ajax 异步请求 等。\n(4)在回调事件内部,有可能对 dom 进行操作,此时浏览器便会挂起 javascript 引擎线程,转而执行 gui 渲染线程,进行页面重绘(repaint)或者回流(reflow)。当 javascript 引擎重新执行时,gui 渲染线程又会被挂起,gui 更新将被保存起来,等到 javascript 引擎空闲时立即被执行。\ngui 渲染线程和 javascript 引擎线程是互斥的。 其他线程相互之间,都是可能并行执行的,ajax 并没有破坏 javascript 的单线程机制。\najax 的请求过程 创建 xmlhttprequest 对象; 浏览器与服务器建立连接 open ; 设置超时,回调函数; 浏览器向服务器发送请求 send ; 服务器向浏览器响应请求,客户端获取异步调用返回的数据; 实现局部刷新。 下面我们就按照这个过程来逐步地认识一下 ajax 喽。\n创建 xmlhttprequest 对象 *注:以下 xmlhttprequest 均简写为 xhr。\n所有现代浏览器均支持 xmlhttprequest 对象(ie5 和 ie6 使用 activexobject )。\n为了应对所有的现代浏览器,包括 ie5 和 ie6,请检查浏览器是否支持 xmlhttprequest 对象。如果支持,则创建 xmlhttprequest 对象。如果不支持,则创建 activexobject ,我们不妨自己封装一个函数来实现获取 xhr 对象的目的,如下:\nfunction getxhr() { var xhr; if (window.xmlhttprequest) { xhr = new xmlhttprequest(); } else { // ie5/6 xhr = new activexobject(\u0026#39;microsoft.xmlhttp\u0026#39;); } return xhr; } xhr 请求 其中,2、3、4 三步都为 xhr 请求,我们放在一起来讲述。\nxhr 对象用于和服务器交换数据,如需将请求发送到服务器,使用 xhr 对象的 open() 和 send() 方法即可,如下:\nopen(method, url, async)\r# 规定请求的类型、url 以及是否异步处理请求\r# - method 请求的类型:get 或 post 等\r# - url :文件在服务器上的位置\r# - async :true (异步,默认值) false (同步)\rsend([data])\r# 将请求发送到服务器\r# - data:可选,仅用于 post 请求 来看个实例:\n// get xhr.open(\u0026#39;get\u0026#39;, \u0026#39;/try/ajax/demo_get\u0026#39;, true); xhr.send(); // post xhr.open(\u0026#39;post\u0026#39;, \u0026#39;/try/ajax/demo_post\u0026#39;, true); xhr.send(); // 如果需要像 html 表单那样 post 数据, // 请使用 setrequestheader() 来添加 http 头, // 然后在 send() 方法中传入要发送的数据 xhr.open(\u0026#39;post\u0026#39;, \u0026#39;/try/ajax/demo_post\u0026#39;, true); xhr.setrequestheader(\u0026#39;content-type\u0026#39;, \u0026#39;application/x-www-form-urlencode\u0026#39;); xhr.send(\u0026#39;fname=jack\u0026amp;lname=liu\u0026#39;); xhr 对象如果要用于 ajax 的话,其 open() 方法的 async 参数必须设置为 true ,当使用 async=true 时,请规定在响应处于 onreadystatechange 事件中的就绪状态时执行的函数:\nxhr.open(\u0026#39;get\u0026#39;, \u0026#39;/try/ajax/ajax_info.txt\u0026#39;, true); xhr.onreadystatechange = function () { if (xhr.readystate == 4 \u0026amp;\u0026amp; xhr.status == 200) { // xhr.reponsetext console.log(xhr.responsetext); } }; xhr.send(); xhr 响应 如需获得来自服务器的响应,请使用 xhr 对象的 responsetext (字符串形式的响应数据)或 responsexml (xml 形式的响应数据)属性。\n当请求被发送到服务器时,我们需要执行一些基于响应的任务,每当 readystate 改变时,就会触发 onreadystatechange 事件,其中, readystate 属性存有 xhr 的状态信息。\n属性 描述 onreadystatechange 存储函数(或函数名),每当 readystate 属性改变时,就会调用该函数 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; readystate 存有 xhr 的状态: 0 :请求未初始化 1 :服务器连接已建立 2 :请求已接收 3 :请求处理中 4 :请求已完成,且响应已就绪 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; status 200 :ok 404 : 未找到页面 当 readystate 等于 4 且状态为 200 时,表示响应已就绪。\nxhr.onreadystatechange = function () { if (xhr.readystate == 4 \u0026amp;\u0026amp; xhr.status == 200) { // ... } }; *注: onreadystatechange 事件被触发 4 次(0 - 4),分别是: 0-1、1-2、2-3、3-4 对应着 readystate 的每个变化。\n*http 协议 我们已经初步了解 ajax 了,深入地话,就需要进一步认识一下 http 协议喽。\rhttp 协议(hypertext transfer protocol,超文本传输协议),是一个基于 tcp/ip 通信协议来传递数据(html 文件、图片、查询结果等)的协议。\nhttp 协议工作于“客户端-服务端”架构上,如浏览器(http 客户端)通过 url 向 web 服务器( http 服务端)发送所有请求。\n_http 三点注意事项:\n(1)http 是无连接:无连接的含义是限制每次连接只处理一个请求,采用这种方式可以节省传输时间。服务器处理完客户的请求,并收到客户的应答后,即断开连接。\n无连接???\r(2)http 是媒体独立的:只要客户端和服务器 知道如何处理 的数据内容,任何类型的数据都可以通过 http 发送,客户端以及服务器指定使用适合的 mime-type 内容类型。\n(3)http 是无状态:http 协议是无状态协议。 _无状态 是指协议对于事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则它 必须重传 ,这样可能导致每次连接传送的数据量增大;另一方面,在服务器不需要先前信息时它的应答就较快。\nhttp 消息结构 http 是基于“客户端/服务端”的架构模型,通过一个可靠的链接来交换信息,是一个 无状态的请求/响应 协议。\n一个 http 客户端,是一个应用程序,如 web 浏览器或其他任何客户端,它通过连接服务器达到向服务器发送一个或多个 http 请求的目的。\n一个 http 服务器,同样是一个应用程序,通常是一个 web 服务器,如 apache、nginx 或 iis 服务器等,它通过接收客户端的请求并向客户端发送 http 响应数据。\n一个可靠的链接,http 使用 uri(uniform resource identifiers,统一资源标识符)来传输数据和建立连接。\n_1. 客户端请求消息\n客户端发送一个 http 请求到服务器的请求消息包括以下格式:\n请求行(request line); 请求头部(header); 空行; 请求数据。 _2. 服务器响应消息\nhttp 响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。\nhttp 请求方法 \u0026gt; http 请求方法\n方法 描述 get 请求指定的页面信息,并返回实体主体 head 类似于 get ,只不过返回的响应中没有具体的内容,用于获取报头 post 向指定资源提交数据进行处理请求(如提交表单或者上传文件),数据被包含在请求体中 \u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; put 从客户端向服务器传送的数据取代指定的文档的内容 delete 请求服务器删除指定的页面 connect http/1.1 协议中预留给能够将连接改为管道方式的代理服务器 options 允许客户端服务器的性能 http 响应头信息 响应头信息 说明 allow 服务器支持哪些请求方法(如 get、post 等) content-encoding 文档的编码(encode)方法 content-length 表示内容长度(只有当浏览器 使用持久 http 连接时才需要这个数据) content-type 表示后面的档属于什么 mime 类型 date 当前的 gmt 时间,可以用 setdateheader 来设置这个头以避免转换时间格式的麻烦 expires 应该在什么时候认为文档已经过期,从而不再缓存它 last-modified 文档的最后改动时间 location 表示服务器应当到哪里提取文档,通常不是直接设置的 refresh 表示浏览器应该在多少时间之后刷新文档,以秒计 server 服务器名字。servlet 一般不设置这个值,面是由 web 服务器自己设置 set-cookie 设置和页面关联的 cookie www-authenticate 客户应该在 authorization 中提供什么类型的授权信息 _1. content-type\n其中, content-type (内容类型),用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件。也就是说, content-type 标头告诉客户端实际返回的内容的内容类型。\n语法格式如下:\ncontent-type: text/html; charset=utf-8\rcontent-type: multipart/form-data; boundary=something \u0026gt; 常见的媒体格式类型\n类型 描述 text/html html 格式 text/plain 纯文本格式 text/xml xml 格式 image/gif gif 图片格式 image/jpeg jpg 图片格式 image/png png 图片格式 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 以 application 开头的媒体格式类型 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- application/xhtml+xml xhtml 格式 application/xml xml 格式 application/atom+xml atom xml 聚合格式 application/json json 数据格式 application/pdf pdf 格式 application/msword word 文档格式 application/octet-stream 二进制流数据(如常见的文件下载) application/x-www-form-urlencoded 表单默认的提交数据的格式 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- application/form-data 需要在表单中进行文件上传时,就需要使用该格式 _2. http 状态码\ni.e. http status code\n当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当 浏览器接收并显示网页前 ,此网页所在的服务器会返回一个包含 http 状态码的信息头(server header)用以响应浏览器的请求。\n_http 状态码 由三个十进制数字组成,其中,第一个数字定义了状态码的类型,后两个数字没有分类的作用,总共分为 5 种类型:\n#+caption: http 状态码分类\n分类 描述 1** 信息,服务器收到请求,需要请求者继续执行操作 2** 成功,操作被成功接收并处理 3** 重定向,需要进一步的操作以完成请求 4** 客户端错误,请求包含语法错误或无法完成请求 5** 服务器错误,服务器在处理请求的过程中发生了错误 下面是常见的状态码:\n200 - 请求成功; 301 - 资源(网页等)被永久转移到其它 url; 404 - 请求的资源(网页等)不存在; 500 - 内部服务器错误。 jquery 中的 ajax6 jquery 提供多个与 ajax 有关的方法。\n通过 jquery ajax 方法,您能够使用 http get 和 http post 从远程服务器上请求文本、html、xml 或 json - 同时您能够把这些外部数据直接载入网页的被选元素中。\n编写常规的 ajax 代码并不容易,因为不同的浏览器对 ajax 的实现并不相同。这意味着您必须编写额外的代码对浏览器进行测试。不过,jquery 团队为我们解决了这个难题,我们只需要一行简单的代码,就可以实现 ajax 功能。\njquery 也算是 dom 时代的霸主了……\r在现代 web 项目中,我们已经很少使用 jquery 做主力了,除非你要和万恶的 ie 打交道,我们这里只稍微认识一下 jquery 中 ajax 的一般应用。\n方法 描述 $.ajax() 执行异步 ajax 请求 $.ajaxsetup() 为将来的 ajax 请求设置默认值 $.ajaxstart() 规定第一个 ajax 请求开始时运行的函数 $.ajaxstop() 规定所有的 ajax 请求完成时运行的函数 $.ajaxsend() 规定 ajax 请求发送之前运行的函数 $.ajaxsuccess() 规定 ajax 请求成功完成时运行的函数 $.ajaxerror() 规定 ajax 请求失败时运行的函数 $.load() 从服务器加载数据,并把返回的数据放置到指定的元素中 $.serialize() 编码表单元素集为字符串以便提交 \u0026hellip; \u0026hellip;\naxios4 来看看这个比较火热的 ajax 封装库吧。\raxios 是一个基于 promise 的 http 库,可以用在浏览器和 nodejs 中,它具有以下特性:\n- 从浏览器中创建 xmlhttprequests\r- 从 nodejs 创建 http 请求\r- 支持 promise api\r- 拦截请求和响应\r- 转换请求数据和响应数据\r- 取消请求\r- 自动转换 json 数据\r- 客户端支持防御 xsrf 想了解更多关于 promise 的内容,可以阅读另一篇文件 - promise 。\n*注:axios 依赖原生的 es6 promise 实现而被支持,如果你的环境不支持 es6 promise ,可以使用 polyfill 。\naxios 起步 先安装喽……\nnpm install axios # npm\r\u0026lt;script src=\u0026#34;https://unpkg.com/axios/dist/axios.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; # cdn 我们提供 javascript 软件包的时候,一般也需要提供 npm 和 cdn 两种分发方式。\r在项目(模块化开发)中,我们一般通过如下两种方式引入 axios :\nimport axios from \u0026#39;axios\u0026#39;; // es6 module const axios = require(\u0026#39;axios\u0026#39;); // commonjs module 当我们引入 axios 之后,会默认导出一个 axios 实例,一般情况下就可以满足需求了。可以通过向 axios 传递相关配置来创建请求,如下:\nimport axios from \u0026#39;axios\u0026#39;; // axios(config) axios({ method: \u0026#39;post\u0026#39;, url: \u0026#39;/user/12345\u0026#39;, data: { firstname: \u0026#39;jack\u0026#39;, lastname: \u0026#39;liu\u0026#39;, }, }); 当然,如果默认的 axios 实例不能满足要求时,可以使用自定义配置创建一个新的 axios 实例(如 instance ):\nimport axios from \u0026#39;axios\u0026#39; // axios.create([config]) const instance = axios.create({ baseurl: \u0026#39;https://some-domain.com/api/\u0026#39;, timeout: 1000, headers: {\u0026#39;x-custom-header\u0026#39;: \u0026#39;foobar\u0026#39;} }) *注:使用创建的 axios 实例请求时,请求的配置项将与实例的配置合并。\n= 实际应用场景中,强烈建议重新创建一个新的 axios 实例!方便后续预设配置,自带实例有时候会有一些莫名奇妙的问题……\naxios 请求配置 5 这些都是创建请求时可以用的配置选项, 只有 url 是必需的 ,如果没有指定的 method ,请求将默认使用 get 方法。\n{ // url 是用于请求的服务器 url url: \u0026#39;/user\u0026#39; // method 是创建请求时使用的方法 method: \u0026#39;get\u0026#39;, // default // baseurl 将自动加在 url 前面,除非 url 是一个绝对 url // 它可以通过设置一个 baseurl 便于为 axios 实例的方法传递相对 url baseurl: \u0026#39;https://some-domain.com/api/\u0026#39;, // transformrequest 允许在向服务器发送前,修改请求数据 // 只能用在 put、post 和 patch 这几个请求方法 // 后面数组中的函数必须返回一个字符串,或 arraybuffer,或 stream transformrequest: [function (data, headers) { // 对 data 进行任意转换处理 return data; }], // transformresponse 在传递给 then/catch 前,允许修改响应数据 transformresponse: [function (data) { // 对 data 进行任意转换处理 return data; }], // ... } axios 响应结构 某个请求的响应包含以下信息:\n{ // data 由服务器提供的响应 data: {}, // status 来自服务器响应的 http 状态码 status: 200, // statustext 来自服务器响应的 http 状态信息 statustext: \u0026#39;ok\u0026#39;, // headers 服务器响应的头 headers: {}, // config 是为请求提供的配置信息 config: {}, // request 是生成当前响应的请求 // 在 node.js 中是最后一个 clientrequest 实例 (在重定向中) // 在浏览器中是 xmlhttprequest 实例 request: {} } 使用 then 时,你将接收下面这样的响应:\naxios.get(\u0026#39;/user/12345\u0026#39;).then(function (response) { console.log(response.data); console.log(response.status); console.log(response.statustext); console.log(response.headers); console.log(response.config); }); 配置默认值 你可以指定将被用在各个请求的配置默认值。\n_1. 全局的 axios 默认值\naxios.defaults.baseurl = \u0026#39;http://api.example.com\u0026#39;; axios.defaults.headers.common[\u0026#39;authorization\u0026#39;] = auth_token; axios.defaults.headers.post[\u0026#39;content-type\u0026#39;] = \u0026#39;application/x-www-form-urlencoded\u0026#39;; _2. 自定义实例默认值\n// 创建实例时设置配置默认值 const instance = axios.create({ baseurl: \u0026#39;https://api.example.com\u0026#39;, }); // 实例创建之后可修改默认配置 instance.defaults.headers.common[\u0026#39;authorization\u0026#39;] = auth_token; _3. 配置的优先顺序\n配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数,后者将优先于前者。如下:\n// 使用由库提供的配置默认值来创建实例 // 此时超时配置的默认值是 0 const instance = axios.create(); // 覆写库的超时默认值 // 现在,在超时前,所有请求都会等待 2.5 秒 instance.defaults.timeout = 2500; // 为已知需要花费很长时间的请求覆写超时设置 instance.get(\u0026#39;/longrequest\u0026#39;, { timeout: 5000, }); 拦截器 在请求或响应被 then 或 catch 处理前拦截它们。\n// 添加请求拦截器 axios.interceptors.request.use( function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return promise.reject(error); } ); // 添加响应拦截器 axios.interceptors.response.use( function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return promise.reject(error); } ); *使用 application/x-www-form-urlencoded 格式 默认情况下,axios 将 javascript 对象序列化为 json。要以 application/x-www-form-urlencoded 格式发送数据,使用 qs 库编码数据,如下:\nconst qs = require(\u0026#39;qs\u0026#39;); axios.post(\u0026#39;/foo\u0026#39;, qs.stringify({ bar: 123 })); 框架、插件 如果你想了解更多,请参考 w3cschool axios 。\n","date":"2020-11-26","permalink":"https://loveminimal.github.io/posts/ajax/","summary":"\u003cp\u003e什么是 Ajax ?\u003c/p\u003e\n\u003cimg src=\"/posts/ajax/imgs/ajax-1.png\" width=\"600\" /\u003e\r\n\u003cp\u003eAJAX(Asynchronous JavaScript and XML),是一种异步请求数据的 Web 开发技术。它不是新的编程语言,而是一种使用现有标准的新方法。其最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。\u003c/p\u003e\n\u003cp\u003e*注:AJAX 不需要任何浏览器插件,但需要用户允许 JavaScript 在浏览器上执行。\u003c/p\u003e","title":"ajax"},{"content":"javascript 中的 file、blob 和 base64 。\nblob1 blob 对象表示一个不可变、原始数据的 _类文件对象 ,它的数据可以按文本或二进制的格式进行读取,也可以转换成 readablestream 来用于数据操作。\nblob 表示的不一定是 javascript 原生格式的数据。 file 接口基于 blob,继承了 blob 的功能并将其扩展,使其支持用户系统上的文件。\n要从其他非 blob 对象和数据构造一个 blob ,请使用 blob() 函数;要创建一个 blob 数据的子集 blob ,请使用 slice() 方法;要获取用户文件系统上的文件对应的 blob 对象,请参阅 file 文档。\n接受 blob 对象的 api 也被列在 file 文档中。\n_1. blob() 构造函数\nblob() 构造函数允许通过其它对象创建 blob 对象,比如,用字符串构建一个 blob :\nvar obj = { name: \u0026#39;jack liu\u0026#39; }; var blob = new blob([json.stringify(obj)], { type: \u0026#39;application/json\u0026#39; }); _2. 使用 blob 创建一个指向类型化数组的 url\nvar typearray = getthetypedarraysomehow(); // 传入一个合适的 mime 类型 var blob = new blob([typearray.buffer], { type: \u0026#39;application/octet-stream\u0026#39; }); var url = url.creatobjecturl(blob); // 会产生一个类似 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 这样的 url 字符串 // 你可以像使用普通的 url 那样使用它,比如用在 img.src 上 _3. 从 blob 中提取数据\n一种从 blob 中读取内容的方法是使用 filereader ,以下代码将 blob 的内容作为类型数组读取:\nvar reader = new filereader(); reader.addeventlistener(\u0026#39;loadend\u0026#39;, function () { // reader.result 包含被转化为类型数组 typed array 的 blob }); reader.readasarraybuffer(blob); 另一种读取 blob 中内容的方式是使用 response 对象,下述代码将 blob 中的内容读取为文本:\nvar text = await new response(blob).text(); 通过使用 filereader 的其它方法可以把 blob 读取为字符串或者数据 url 。\nfile2 文件( file )接口提供有关文件的信息,并允许网页中的 javascript 访问其内容。\n通常情况下,file 对象是来自用户在一个 \u0026lt;input\u0026gt; 元素上选择文件后返回的 filelist 对象,也可以是来自由拖放操作生成的 datatransfer 对象,或者来自 htmlcanvaselement 上的 mozgetasfile() api 。\nfile 对象是特殊类型的 blob ,且可以用在任意的 blob 类型的 context 中。比如说, filereader, url.createobjecturl(), createimagebitmap() 及 xmlhttprequest.send() 都能处理 blob 和 file 。\nbase643 base64 是一组相似的二进制到文本(binary-to-text)的 _编码规则 ,使得二进制数据在解释成 radix-64 的表现形式后能够用 ascii 字符串的格式表示出来。\n= base64 这个词出自一种 mime 数据传输编码。\nbase64 编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景,这样是为了保证数据的完整并且不用在传输过程中修改这些数据。\n在 javascript 中,有两个函数被分别用来解码和编码 base64 字符串: atob() 和 btoa() 。\natob() 函数能够解码通过 base-64 编码的字符串数据,相反地, btoa() 函数能够从二进制数据字符串创建一个 base-64 编码的 ascii 字符串。\n// atob() 将 base64 解码 // btoa() 将 字符串转码为 base64 var str = \u0026#39;javascript\u0026#39;; window.btoa(str); // 转码 → amf2yxnjcmlwda== window.atob(\u0026#39;amf2yxnjcmlwda==\u0026#39;); // 解码 → javascript // 中方需要先用 encodeuricomponent 和 decodeuricomponent 转/解码 _编码尺寸增加 ,每一个 base64 字符串实际上代表着 6 比特位,因此,3 字节(1 字节是 8 比特,3 字节为 24 比特)的字符串/二进制文件可以转换成 4 个 base64 字符(4 * 6 = 24 比特)。\n这意味着 base64 格式的字符串或文件的尺寸是原始尺寸的 133% ,如果编码的数据很少,增加的比例可能会更高。\n= 是不是有些混乱?hmm\u0026hellip; 来看下百度百科吧。\nbase64 要求把每三个 8bit 的字节转换为四个 6bit 的字节(3*8 = 4*6 = 24),然后把 6bit 再添加两位高位 0 ,组成四个 8bit 的字节,也就是说,转换后的字符串理论上将要比原来的长 1/3 。\n其具规则如下5:\n1. 把 3 个字节变成 4 个字节\r2. 每 76 个字符加一个换行符\r3. 最后的结束符也要处理 下面来看一个具体的例子。\n转换前: 10101101,10111010,01110110\r等价于: 101011 01,1011 1010,01 110110\r转换后: 00101011,00011011 ,00101001,00110110\r十进制: 43 27 41 54\r对照 =table 1: the base64 alphabet= 码表:\rr b p 2 如上,上面的 24 位编码后的 base64 值为 rbp2 。解码同理,把 rbp2 的二进制位连接上再重组得到的 8 位值,得出原码。\nformdata formdata 接口提供了一种表示表单数据的键值 key/value 的构造方式,并且可以轻松地将数据通过 xmlhttprequest.send() 方法发送出去。如果送出时的编码类型被设为 multipart/form-data ,它会使用和表单一样的格式。\n实现了 formdata 接口的对象可以直接在 for...of 结构中使用,而不需要调用 entries() : for(var p of myformdata) 的作用和 for(var p of myformdata.entries()) 是相同的。\nformdata() 构造函数用来创建一个新的 formdata 对象。\n= 有时候,这种“参考书式的定义”真的让人很……\n_formdata 有会么用呢?\nformdata 类型是在 xmlhttprequest 2 级定义的,它是为序列化表单以及创建与表单格式相同的数据提供便利(用于 xhr 传输)。\n_1. 序列化表单\n将 form 表单元素的 name 与 value 进行组合,实现表单数据的序列化,从而减少表单元素的拼接,提高工作效率。\n\u0026lt;form id=\u0026#34;myform\u0026#34; method=\u0026#34;post\u0026#34; action=\u0026#34;\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;jack\u0026#34; /\u0026gt; \u0026lt;input name=\u0026#34;pswd\u0026#34; type=\u0026#34;password\u0026#34; value=\u0026#34;12345678\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; 我们可以使用这个表单元素作为初始化参数,来实例化一个 formdata 对象。\n// 1. 获取页面已有的一个 form 表单 var myform = document.getelementbyid(\u0026#39;myform\u0026#39;); // 2. 用表单来初始化 var formdata = new formdata(form); // 我们可以根据 name 来访问表单中的字段 var name = formdata.get(\u0026#39;name\u0026#39;); // → jack var pswd = formdata.get(\u0026#39;pswd\u0026#39;); // → 12345678 // 当然,我们也可以在此基础上添加其他数据 formdata.append(\u0026#39;token\u0026#39;, \u0026#39;fajskd.fj23jkf.sdfk\u0026#39;); // 你还可以通过 xhr 来发送数据 var xhr = new xmlhttprequest(); xhr.open(\u0026#39;post\u0026#39;, \u0026#39;http://127.0.0.1/login\u0026#39;); xhr.setheader(\u0026#39;content-type\u0026#39;, \u0026#39;application/x-www-form-urlencoded\u0026#39;); // ?? 此处存疑 xhr.send(formdata); xhr.onload = function () { if (xhr.status === 200) { // ... } }; _2. 直接创建一个 formdata 对象\n使用 formdata 构造函数可以直接创建一个 formdata 对象,如: var formdata = new formdata() ,然后,就可以调用相关方法进行操作。\n_3. 常用方法\n= 无非是“增删查改”那些事儿……\n方法 描述 append(key, value) 向 formdata 中添加新的属性值,对应的属性值存在也不会覆盖 get(key) 返回在 formdata 对象中与给定键关联的第一个值 getall(key) 返回一个包含 formdata 对象中与给定键关联的所有值的数组 has(key) 返回一个布尔值表明 formdata 对象是否包含某些键 set(key, value) 给 formdata 设置属性值,如果 formdata 对应的属性值存在则覆盖原值,否则新增一项属性值 entries 返回一个包含所有键值对的 iterator 对象 keys 返回一个包含所有键的 iterator 对象 values 返回一个包含所有值的 iterator 对象 delete(key) 从 formdata 对象里面删除一个键值对 转换4 filetobase64 主要应用场景:图片预览。\nfunction filetobase64(file, callback) { var reader = new filereader(); reader.readasdataurl(file); reader.onload = function (e) { var base64 = e.target.result; if (typeof callback === \u0026#39;function\u0026#39;) { callback(base64); } else { console.log(\u0026#39;base64: \u0026#39;, base64); } }; } filetoblob 主要应用场景:文件上传。\nfunction filetoblob(file, callback) { var type = file.type; var reader = new filereader(); reader.readasdataurl(file); reader.onload = function (e) { var blob = new blob([e.target.result], { type }); if (typeof callback === \u0026#39;function\u0026#39;) { callback(blob); } else { console.log(\u0026#39;blob: \u0026#39;, blob); return blob; } }; } base64tofile 主要应用场景:文件上传。\nfunction base64tofile(base64, callback(filename)) { var arr = base64.split(\u0026#39;,\u0026#39;), type = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new unit8array(n) while(n--) { u8arr[n] = bstr.charcodeat(n) } return new file([u8arr], filename, {type}) } base64toblob 主要应用场景:文件上传。\n//dataurl 类似为 data:img/jpg;base64,/9j/4aaqskzjrgabaqeasabiaad/2w... function base64toblob(base64, callback) { var arr = base64.split(\u0026#39;,\u0026#39;), // mimestring type = arr[0].match(/:(.*?);/)[1], // bytestring bstr = atob(arr[1]), n = bstr.length, u8arr = new uint8array(n); // for (var i = 0; i \u0026lt; bytestring.length; i++) { // ia[i] = bytestring.charcodeat(i) // } while (n--) { u8arr[n] = bstr.charcodeat(n); } return new blob([u8arr], { type }); } blobtofile 主要应用场景:文件上传。\nfunction blobtofile(blob, filename) { var file = new file([blob], filename, { type: blob.type }); return file; } blobtobase64 主要应用场景:图片预览。\nfunction blobtobase64(blob, callback) { var reader = new filereader(); reader.readasdataurl(blob); reader.onload = function (e) { if (typeof callback === \u0026#39;function\u0026#39;) { callback(e.target.result); } else { console.log(\u0026#39;base64: \u0026#39;, e.target.result); return e.target.result; } }; } ","date":"2020-11-26","permalink":"https://loveminimal.github.io/posts/file-conversion/","summary":"\u003cp\u003eJavaScript 中的 file、blob 和 base64 。\u003c/p\u003e","title":"file conversion"},{"content":"拖放 是一种常见的特性,即抓取对象以后拖到另一个位置。在 html5 中,拖放是标准的一部分,任何元素都能够拖放。\n*注:internet explorer 9+, firefox, opera, chrome, 和 safari 支持拖动。\n示例1 \u0026lt;div id=\u0026#34;drop-box\u0026#34; ondrop=\u0026#34;drop(event)\u0026#34; ondragover=\u0026#34;allowdrop(event)\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;img id=\u0026#34;drag-ele\u0026#34; src=\u0026#34;./images/drap.png\u0026#34; draggable=\u0026#34;true\u0026#34; ondragstart=\u0026#34;drag(event)\u0026#34; width=\u0026#34;200\u0026#34; height=\u0026#34;160\u0026#34; /\u0026gt; function allowdrop(e) { e.preventdefault(); } function drag(e) { e.datatransfer.setdata(\u0026#39;text\u0026#39;, e.target.id); } function drop(e) { e.preventdefault(); var data = e.datatransfer.getdata(\u0026#39;text\u0026#39;); e.target.appendchild(document.getelementbyid(data)); } 它看上去也许有些复杂,不过我们可以分别研究拖放事件的不同部分。\n拖放 为了方便后文描述,我们称被拖元素为“小拖”,放置到的目标元素为“小盒” : )\n1. 设置元素为可拖放\n首先,为了使元素可拖动,需要把“小拖”的 draggable 属性设置为 true ,如下:\n\u0026lt;img draggable=\u0026#34;true\u0026#34; / \u0026gt; 2. 设置拖动的触发事件\n然后,规定当元素被拖动的时候,会发生什么。\n在上面的例子中, ondragstart 属性调用了一个函数 drag(event) ,它规定了被拖动的数据。\n如何设置这些数据呢? datatransfer.setdata() ,我们用它来设置被拖数据的数据类型和值,如下:\nfunction drag(e) { e.datatransfer.setdata(\u0026#39;text\u0026#39;, e.target.id); } 其中, text 是一个 domstring ,表示要添加到“小拖”中的拖动数据的类型,值是“小拖”的 id 。\nie 浏览器中 e.originalevent.datatransfer.setdata 可能会产生以下报错:\n意外地调用了方法或属性访问; 参数无效。 为什么?如何解决呢?原来 setdata(key, value) 的 key 在 ie 中只能设置为 text 或 text 等指定的可选类型(chrome 中无此限制), value 最好使用 json.stringfy() 序列化一下。\nie 不死,天下不安。\r3. 设置“小盒”可被放置\nondragover 事件规定在何处放置“小拖”的数据。\n默认情况下,无法将数据/元素放置到其他元素中。如果需要设置允许放置,必须阻止对于元素的默认处理方式,如通过调用 ondragover 事件的 event.preventdefault() 方法。\nevent.preventdefault(); 4. 进行放置\n当放置“小拖”时,会发生 drop 事件。在上面的例子中, ondrop 属性调用了一个函数 drop(event) ,如下:\nfunction drop(e) { // 调用 `preventdefault()` 来避免浏览器对数据的默认处理 // `drop` 事件的默认行为是以链接形式打开 e.preventdefault(); // 通过 `datatransfer.getdata(\u0026#39;text\u0026#39;)` 方法获得“小拖”设置的数据 // 该方法将返回在 `setdata()` 方法中设置的相同类型的任何数据 var data = e.datatransfer.getdata(\u0026#39;text\u0026#39;); // 这里 `data` 就是前面设置的 `e.target.id` e.target.appendchild(document.getelementbyid(data)); } ","date":"2020-11-10","permalink":"https://loveminimal.github.io/posts/drag-and-drop/","summary":"\u003cp\u003e\u003cem\u003e拖放\u003c/em\u003e 是一种常见的特性,即抓取对象以后拖到另一个位置。在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e*注:Internet Explorer 9+, Firefox, Opera, Chrome, 和 Safari 支持拖动。\u003c/code\u003e\u003c/p\u003e","title":"drag and drop"},{"content":"🔔 本文摘录自 浏览器同源政策及其规避方法\n什么是跨域 跨域,是指浏览器不能执行其他网站的脚本,它是浏览器的同源策略造成的,是浏览器对 javascript 实施的安全限制。\n那么,什么是同源策略呢?\n同源策略,是由 netscape 公司于 1995 年引入浏览器的,目前,所有浏览器都实行这个策略。\n为什么需要同源策略呢?\n同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。\n= 每一件事物都有其产生的源头……\n最初,它的含义是,a 网页设置的 cookie ,b 网页不能打开,除非这两个网页“同源” \u0026ndash; 协议相同、域名相同、端口相同。\n= cookie 往往用来保存用户的登录状态,所以“同源策略”是必需的,否则 cookie 可以共享,互联网就毫无安全可言了。\n随着互联网的发展,“同源策略”越来越严格。目前,如果非同源,共有三种行为受到限制:\ncookie、localstorage 和 indexdb 无法读取; dom 无法获得; ajax 请求不能发送。 跨域的限制当然是有必要的,但实际应用过程中也常常需要在保证相对安全的情况下规避上述的三种限制。\n= 往往是这样,建立的束缚就是用来打挣脱的…… 在同源策略不允许跨域的那一刻,也就产生了需要跨域的需求。\ncookie cookie 是服务器写入浏览器的一小段信息 ,只有同源网页才能共享。但是,如果两个网页的一级域名相同,只是二级域名不同,浏览器允许通过设置 =document.domain= 共享 cookie 。\n*注:cookie 是服务器写入浏览器的信息哦!\n举例来说,a 网页是 http://w1.example.com/a.html ,b 网页是 http://w2.example.com/b.html ,那么只要设置相同的 document.domain ,两个网页就可以共享 cookie 。\ndocument.domain = \u0026#39;example.com\u0026#39;; 现在,a 网页通过脚本设置一个 cookie :\ndocument.cookie = \u0026#39;test1=hello\u0026#39;; b 网页就可以读到这个 cookie :\nvar allcookie = document.cookie; *注:这种方法只适用于 cookie 和 iframe 窗口,localstorage 和 indexdb 无法通过这种方法(要使用 postmessage api)。\n另外,服务器也可以在设置 cookie 的时候,指定 cookie 的所属域名为一级域名,比如 .example.com :\nset-cookie: key=value; domain=.example.com; path=/ 如此,二级域名和三级域名不用做任何设置,都可以读取这个 cookie 。\n= 旨在理解吧,已经是被时代淘汰了的东东了……\niframe 如果两个网页不同源,就无法拿到对方的 dom ,典型的例子是 iframe 窗口和 window.open 方法打开的窗口,它们与父窗口无法通信。\n如果两个窗口一级域名相同,只是二级域名不同,那么设置 document.domain 属性,就可以规避同源策略,拿到 dom 。\n对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题:\n片段识别符(fragment identifier); window.name ; 跨文档通信 api (cross-document messaging)。 片段识别符 片段识别符 (fragment identifier),是指 url 的 # 号后面的部分,如 http://example.com/x.html#fragment 的 #fragment 。如果,只是改变片段识别符,页面不会重新刷新。\n父窗口可以把信息,写入子窗口的片段识别符,如下:\nvar src = originurl + \u0026#39;#\u0026#39; + data; document.getelementbyid(\u0026#39;myiframe\u0026#39;).src = src; 子窗口通过监听 hashchange 事件得到通知,如下:\nwindow.onhashchange = checkmessage; function checkmessage() { var message = window.location.hash; // ... } 同样的,子窗口也可以改变父窗口的片段标识符:\nparent.location.href = target + \u0026#39;#\u0026#39; + hash; window.name 浏览器窗口有 window.name 属性,它的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页就可以读取它。\n来看一个具体的例子吧。\n父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入 window.name 属性,如下:\nwindow.name = data; 接着,子窗口跳回一个与主窗口同域的网址:\nlocation = \u0026#39;http://parent.url.com/xxx.html\u0026#39;; 然后,主窗口就可以读取子窗口的 window.name 了:\nvar data = document.getelementbyid(\u0026#39;myframe\u0026#39;).contentwindow.name; 这种方法的优点是, window.name 容量很大,可以放置非常长的字符串;缺点是必须监听子窗口 window.name 属性的变化,影响网页性能。\n= 不强制兼容 ie 的话,实在不想碰这玩意儿。..\nwindow.postmessage 片段识别符和 window.name 两种方法都属于破解,html5 为了解决这个问题,引入了一个全新的 api \u0026ndash; 跨文档通信 api (cross-document messaging)。\n= 所谓破解,就是把公鸡的工作交给大鹅去做,虽然也能叫你起床,但总归不那么悦耳。\n这个 api 为 window 对象新增了一个 window.postmessage 方法,允许跨窗口通信,不论这两个窗口是否同源。\n父窗口 http://aaa.com 向子窗口 http://bbb.com 发消息,调用 postmessage 方法就可以了,如下:\nvar popup = window.open(\u0026#39;http://bbb.com\u0026#39;, \u0026#39;title\u0026#39;); popup.postmessage(\u0026#39;hello world!\u0026#39;, \u0026#39;http://bbb.com\u0026#39;); 其中, postmessage 方法的:\n第一个参数是具体的信息内容; 第二个参数是接收消息的窗口的源(origin),即“协议+域名+端口”,也可以设为 * ,表示不限制域名,向所有窗口发送。 子窗口向父窗口发送消息的写法类似:\nwindow.opener.postmessage(\u0026#39;nice to see you\u0026#39;, \u0026#39;http://aaa.com\u0026#39;); 父窗口和子窗口都可以通过 message 事件,监听对方的消息:\nwindow.addeventlistener( \u0026#39;message\u0026#39;, function (e) { console.log(e.data); }, false ); message 事件的事件对象 event ,提供了以下三个属性:\nevent.source :发送消息的窗口; event.origin :消息发向的网址; event.data :消息内容。 下面的例子是,子窗口通过 event.source 属性引用父窗口,然后发消息,如下:\nwindow.addeventlistener(\u0026#39;message\u0026#39;, receivemessage); function receivemessage(event) { event.source.postmessage(\u0026#39;nice to see you!\u0026#39;, \u0026#39;*\u0026#39;); } event.origin 属性可以过滤不是发给本窗口的消息:\nwindow.addeventlistener(\u0026#39;message\u0026#39;, receivemessage); function receivemessage(event) { if (event.origin !== \u0026#39;http://aaa.com\u0026#39;) return; if (event.data === \u0026#39;hello world\u0026#39;) { event.source.postmessage(\u0026#39;hello\u0026#39;, event.origin); } else { console.log(event.data); } } 通过 window.postmessage ,读写其他窗口的 localstorage 也成为了可能。\n= 其实吧,只要是能序列化字符串的信息,都可以传递。\n例如,主窗口写入 iframe 子窗口的 localstorage :\nwindow.onmessage = function (e) { if (e.origin !== \u0026#39;http://bbb.com\u0026#39;) { return; } var payload = json.parse(e.data); localstorage.setitem(payload.key, json.stringfy(payload.data)); // ?? payload.data }; 其中,父窗口发送消息的代码如下:\nvar win = document.getelementbytagname(\u0026#39;iframe\u0026#39;)[0].contentwindow; var obj = { name: \u0026#39;jack\u0026#39; }; win.postmessage(json.stringfy({ key: \u0026#39;storage\u0026#39;, data: obj }), \u0026#39;http://bbb.com\u0026#39;); = 下面来个加强版的。..\n// 加强版的父窗口发消息 /// var win = doucment.getelementbytagname(\u0026#39;iframe\u0026#39;)[0].contentwindow; var obj = { name: \u0026#39;jack\u0026#39; }; // 存入对象 win.postmessage( json.stringfy({ key: \u0026#39;storage\u0026#39;, method: \u0026#39;set\u0026#39;, data: obj }), \u0026#39;http://bbb.com\u0026#39; ); // 读取对象 win.postmessage(json.stringfy({ key: \u0026#39;storage\u0026#39;, method: \u0026#39;get\u0026#39; }), \u0026#39;*\u0026#39;); window.onmessage = function (e) { if (e.origin != \u0026#39;http://aaa.com\u0026#39;) return; console.log(json.parse(e.data).name); // → \u0026#34;jack\u0026#34; }; // 加强版的子窗口接收消息 /// window.onmessage = function (e) { if (e.origin !== \u0026#39;http://bbb.com\u0026#39;) return; var payload = json.parse(e.data); switch (payload.method) { case \u0026#39;set\u0026#39;: localstorage.setitem(payload.key, json.stringfy(payload.data)); break; case \u0026#39;get\u0026#39;: var parent = window.parent; var data = localstorage.getitem(payload.key); parent.postmessage(data, \u0026#39;http://aaa.com\u0026#39;); break; case \u0026#39;remove\u0026#39;: localstorage.removeitem(payload.key); break; } }; = 从本质上来说,这几种方法都是利用浏览器的某些信息保留机制,把信息序列化为字符串,以参数形式在跨域窗口之间的传递。\najax 浏览器的同源策略规定, ajax 请求只能发给同源的网址 ,否则就报错。\n除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制: jsonp、websocket、cors 。\n= 这里架设的服务器是用来代理客户端的,是正向代理。\njsonp jsonp 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。\n很简单,但缺点在于 jsonp 只支持 get 请求。\njsonp 的基本思想是什么?\n原来,网页通过添加一个 \u0026lt;script\u0026gt; 元素,向服务器请求 json 数据的做法是不受同源策略限制的;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。\n= 通过 script 标签的 src 属性值作为请求地址,666 。\n首先,网页动态插入 \u0026lt;script\u0026gt; 元素,由它向跨源网址发出请求,如下:\nfunction addscripttag(src) { var script = doucment.createelement(\u0026#39;script\u0026#39;); script.setattribute(\u0026#39;type\u0026#39;, \u0026#39;text/javascript\u0026#39;); script.src = src; document.body.appendchild(script); } window.onload = function () { addscripttag(\u0026#39;http://example.com/jp?callback=foo\u0026#39;); }; function foo(data) { console.log(\u0026#39;your public ip address is: \u0026#39; + data.ip); } 上述代码通过动态添加 \u0026lt;script\u0026gt; 元素,向服务器 example.com 发出请求。\n*注:该请求的查询字符串有一个 callback 参数,用来指定回调函数的名字,在对于 jsonp 是必需的。\n服务器收到这个请求后,会将数据放在回调函数的参数位置返回,如下:\nfoo({ ip: \u0026#39;8.8.8.8\u0026#39;, }); 由于 \u0026lt;script\u0026gt; 元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了 foo 函数,该函数就会立即调用。作为参数的 json 数据被视为 javascript 对象,而不是字符串,因此避免了使用 json.parse 的步骤。\nwebsocket websocket 是一种通信协议,使用 ws:// (非加密)和 wss:// (加密)作为协议前缀。该协议 不实行同源策略 ,只要服务器支持,就可以通过它进行跨源通信。\n来看个例子,下面是浏览器发出的 websocket 请求的头信息:\nget /chat http/1.1\rhost: server.example.com\rupgrade: websocket\rconnection: upgrade\rsec-websocket-key: x3jjhmbdl1ezlkh9gbhxdw==\rsec-websocket-protocol: chat, superchat\rsec-websocket-version: 13\rorigin: http://example.com 其中, origin 表示该请求的请求源(origin),即发自哪个域名。\n*注:正是因为有了 origin 这个字段,websocket 才没有实行同源策略,因为服务器可以根据这个字段,判断是否允许本次通信。\n如果该域名在白名单内,服务器就会做出如下响应:\nhttp/1.1 101 switching protocols\rupgrade: websocket\rconnection: upgrade\rsec-websocket-accept: hsmrc0smlyukagmm5oppg2hagwk=\rsec-websocket-protocol: chat cors cors (cross-origin resource sharing)是跨域资源分享的缩写,它是 w3c 标准,是 跨源 ajax 请求的根本解决办法。相比 jsonp 只能发 get 请求,cors 允许任何类型的请求。\ncors 详解 cors 允许浏览器向跨源服务器,发出 xmlhttprequest 请求,从而克服了 ajax 只能同源使用的限制。接下来,我们来深入认识下 cors 的内部机制。\ncors 需要浏览器和服务器同时支持, 整个 cors 通信过程,都是浏览器自动完成 ,不需要用户参与。\n对于开发者来说,cors 通信与同源的 ajax 通信没有差别,代码完全一样。浏览器一旦发现 ajax 请求资源,就会自动添加一些附加的头信息 ,有时还会多出一次附加的请求,但用户不会有感觉。\n= 浏览器是个好宿主,直接帮你(用户)解决了。本质上讲,cors 在浏览器端而言,就是浏览器对用户的 ajax 请求的“封装代理”。\n因此,实现 cors 通信的关键是服务器,只要服务器实现了 cors 接口,就可以跨源通信。\n两种请求 浏览器将 cors 请求分为两类: 简单请求(simple request)和非简单请求(not-so-simple request)。\n只要同时满足以下两在条件,就属于简单请求,如下:\n请求方法是以下三种之一: head、get、post ; http 的头信息不超出以下几种字段: - accept\r- accept-language\r- content-language\r- last-event-id\r- content-type 其中, content-type 只限于三个值 application/x-www-form-urlencode、multipart/form-data、text/plain 。\n这是为了兼容表单,因为历史上表单一直可以发出跨域请求。ajax 的跨域设计就是,只要表单可以发,ajax 就可以直接发。\n*注:凡是不同时满足上面两个条件,就属于非简单请求,浏览器对这两种请求的处理,是不一样的。\n简单请求 1. 基本流程\n对于简单请求,浏览器直接发出 cors 请求,具体来说,就是在头信息中,增加一个 origin 字段。\n如下面这个例子,浏览器发现这次跨源 ajax 请求是简单请求,就自动在头信息中,添加一个 origin 字段:\nget /cors http/1.1\rorigin: http://api.bob.com ←\rhost: api.alice.com\raccept-language: en-us\rconnection: keep-alive\ruser-agent: mozilla/5.0... 上面的头信息中, origin 字段用来说明,本次请求来自哪个源(协议+域名+端口),服务器根据这个值决定是否同意这次请求。\n1.1 不在服务器许可范围内\n如果 origin 指定的源不在许可范围内,服务器会返回一个正常的 http 回应。浏览器发现这个回应的头信息没有包含 access-control-allow-origin 字段,就知道出错了,从面抛出一个错误,被 xmlhttprequest 的 onerror 回调函数捕获。\n*注:这种错误无法通过状态码识别,因为 http 回应的状态码可能是 200 。\n1.2 在服务器许可范围内\n如果 origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段,如下:\naccess-control-allow-origin: http://api.bob.com\raccess-control-allow-credentials: true\raccess-control-expose-headers: foobar\rcontent-type: text/html; charset=utf-8 上面的信息头中,有三个与 cors 请求相关的字段,都以 access-control- 开头。\n(1)access-control-allow-origin\n该字段是必须的,它的值要么是请求时 origin 字段的值,要么是一个 * (表示接受任意域名的请求)。\n(2)access-control-allow-credentials\n该字段可选,它的值是一个布尔值,表示是否允许发送 cookie 。\n默认情况下,cookie 不包括在 cors 请求之中,设为 true ,即表示服务器明确许可 cookie 可以包含在请求中,一起发给服务器。\n*注:其实,这个值也只能设为 true ,如果服务器不要浏览器发送 cookie ,删除该字段即可。\n(3)access-control-expose-headers\n该字段可选,cors 请求时, xmlhttprequest 对象的 getresponseheader() 方法只能拿到 6 个基本字段: cache-control、content-language、content-type、expires、last-modified、pragma 。如果想拿到其他字段,就必须在 access-control-expose-headers 里面指定。如下面的例子中就指定了 foobar ,如此便可以通过 getresponseheader('foobar') 来返回 foobar 字段的值。\n2. withcredentials 属性\n上面说到,cors 请求默认不发送 cookie 和 http 认证信息。如果要把 cookie 发到服务器,一方面要服务器同意,指定 access-control-allow-credentials 字段:\naccess-control-allow-credentials: true 另一方面,开发者必须在 ajax 请求中打开 withcredentials 属性,如下:\nvar xhr = new xmlhttprequest(); xhr.withcredentials = true; 否则,即使服务器同意发送 cookie ,浏览器也不会发送。或者,服务器要求设置 cookie ,浏览器也不会处理。\n但是,如果省略 withcredentials 设置,有的浏览器还是会一起发送 cookie 。这时,可以显式关闭 withcredentials :\nxhr.withcredentials = false; 需要注意的是,如果要发送 cookie , access-control-allow-origin 就不能设为星号,必须指定的、与请求网页一致的域名。同时,cookie 依然遵循同源政策,只有服务器域名设置的 cookie 才会上传,其他域名的 cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的 cookie 。\n非简单请求 预检请求 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 put 或 delete ,或者 content-type 字段的类型是 application/json 。\n非简单请求的 cors 请求,会在正式通信之前,增加一次 http 查询请求,称为预检请求(prefight)。\n浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 http 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 xmlhttprequest 请求,否则就报错。\n来看一段浏览器的 javascript 脚本。\nvar url = \u0026#39;http://api.alice.com/cors\u0026#39;; var xhr = new xmlhttpreuqest(); xhr.open(\u0026#39;put\u0026#39;, url, true); xhr.setrequestheader(\u0026#39;x-custom-header\u0026#39;, \u0026#39;value\u0026#39;); xhr.send(); 上面代码中,http 请求的方法是 put ,并且发送一个自定义头信息 x-custom-header 。\n浏览器发现,这是一个非简单请求,就自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 http 头信息:\noptions /cors http/1.1\rorigin: http://api.bob.com\raccess-control-request-method: put\raccess-control-request-headers: x-custom-header\rhost: api.alice.com\raccept-language: en-us\rconnection: keep-alive\ruser-agent: mozilla/5.0... “预检”请求用的请求方法是 options ,表示这个请求是用来询问的。头信息里面,关键字段是 origin ,表示请求来自哪个源。\n除了 origin 字段,“预检”请求的头信息包括两个特殊字段。\n(1)access-control-request-method\n该字段是必须的,用来列出浏览器的 cors 请求会用到哪些 http 方法,上例是 put 。\n(2)access-control-request-headers\n该字段是一个逗号分隔的字符串,指定浏览器 cors 请求额外发送的头信息字段,上例是 x-custom-header 。\n预检请求的回应 服务器收到“预检”请求以后,检查了 origin、access-control-request-method 和 access-control-request-headers 字段以后,确认允许跨源请求,就可以做出反应。\nhttp/1.1 200 ok\rdate: mon, 01 dec 2008 01:15:39 gmt\rserver: apache/2.0.61 (unix)\raccess-control-allow-origin: http://api.bob.com\raccess-control-allow-methods: get, post, put\raccess-control-allow-headers: x-custom-header\rcontent-type: text/html; charset=utf-8\rcontent-encoding: gzip\rcontent-length: 0\rkeep-alive: timeout=2, max=100\rconnection: keep-alive\rcontent-type: text/plain 上面的 http 回应中,关键的是 access-control-allow-origin 字段,表示 http://api.bob.com 可以请求数据。该字段可也可以设为星号,表示同意任意跨源请求。\naccess-control-allow-origin: * 如果服务器否定了“预检”请求,会返回一个正常的 http 回应,但是没有任何 cors 相关的头信息字段。这时,浏览器就会认定,服务器不同意“预检”请求,因此触发一个错误,被 xmlhttprequest 对象的 onerror 回调函数捕获,控制台会打印出如下的报错信息:\nxmlhttprequest cannot load http://api.alice.com.\rorigin http://api.bob.com is not allowed by access-control-allow-origin. 服务器回应的其他 cors 相关字段如下:\naccess-control-allow-methods: get, post, put\raccess-control-allow-headers: x-custom-header\raccess-control-allow-credentials: true\raccess-control-max-age: 1728000 (1)access-control-allow-methods\n该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。\n*注:返回的是所有支持的方法,而不单是浏览器请求的那个方法,这是为了避免多次“预检”请求。\n(2)access-control-allow-headers\n如果浏览器请求包括 access-control-request-headers 字段,则 access-control-allow-headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检”中请求的字段。\n(3)access-control-allow-credentials\n该字段与简单请求时的含义相同。\n(4)access-control-max-age\n该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒,在些期间,不用发出另一条预检请求。\n浏览器的正常请求和回应 一旦服务器通过了“预检”请求,以后每次浏览器正常的 cors 请求,就都跟简单请求一样,会有一个 origin 头信息字段。服务器的回应,也都会有一个 access-control-allow-origin 头信息字段。\n下面是“预检”请求之后,浏览器的正常 cors 请求:\nput /cors http/1.1\rorigin: http://api.bob.com\rhost: api.alice.com\rx-custom-header: value\raccept-language: en-us\rconnection: keep-alive\ruser-agent: mozilla/5.0... 上面头信息的 origin 字段是浏览器自动添加的。\n下面是服务器正常的回应:\naccess-control-allow-origin: http://api.bob.com\rcontent-type: text/html; charset=utf-8 上面头信息中, access-control-allow-origin 字段是每次回应都必定包含的。\n","date":"2020-11-03","permalink":"https://loveminimal.github.io/posts/cross-domain/","summary":"\u003cp\u003e🔔 本文摘录自 \u003ca href=\"http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html\"\u003e浏览器同源政策及其规避方法\u003c/a\u003e\u003c/p\u003e","title":"跨域"},{"content":"🔔 本文主要摘录自 问问学五笔1\n五笔是什么 我们主要讲 86 版的五笔,它的通用性最好。\n五笔的基本原理是什么呢?\n汉字都是由笔划或部首组成的。为了输入汉字,我们把汉字折成一些最常用的基本单位 \u0026ndash; 字根 ,字根可以是汉字的偏旁部首,可以是部首的一部分,甚至是笔划。\n取出这引起字根后,把它们按一定的规律分类,再把这些字根依据科学原理分配在键盘上,作为输入汉字的基本单位。\n当要输入汉字时,我们就按照汉字的书写顺序依次按键盘上与字根对应的键,组成一个代码,系统根据输入字根组成的代码,在五笔输入的字库中检索出所要的字。\n拆字 → 分配 → 组代 → 字库检索\r我们已经了解了五笔的基本原理,那么究竟应该如何学习五笔呢?\n三个方面,如下:\n知道键盘上的每个键位对应着哪些字根; 学习如何把汉字拆分成五笔字根; 输入字根对应的字母,必要时要键入识别码。 分三步:把冰箱门打开,把大象装进去,把冰箱门关上。\r键盘分区和区位号 五笔基本字根有 130 种,加上一些基本字根的变型,共有 200 个左右,这些字根分配在键盘上的 25 个键(除 z 外)上,每个键位都对应着几个甚至几十个字根。\n我们知道,汉字有五种基本笔划: 横(提)、竖(竖勾)、撇、捺(点)、折 ,所有的字根都是由这五种笔划组成的。\n按照每个字根的起笔笔划,把这些字根分为五个区,如下:\n每个区有五个位,按一定的顺序编号,就叫 区位号 (都是从键盘中间开始,向外扩展进行编号),如下:\n如何输入这五个基本笔划?\n笔划 一 丨 丿 丶 乙 代码 ggll hhll ttll yyll nnll 字根的输入 → 更多详查 五笔字根分解图详解2\n键名字根 让我们再来看看上面这张图中标红的字体,每个区位选取了一个最常用的字根作为键的名字,该字根称为 键名字根 。\nhmm... 怎么说呢,“被”这个字我经常使用,但每次都要输入“puhc”四个键...\r如何输入键名字根呢?对应的键按四下就行了,如:\n键名字根 王 土 大 \u0026hellip; 代码 gggg ffff dddd xxxx 字根歌 31 t 禾竹一撇双人立,反文条头共三一 41 y 言文方广在四一,高头一捺谁人去 32 r 白手看头三二斤 42 u 立辛两点六门疒 33 e 月彡(衫)乃用家衣底(爱头豹脚舟字底) 43 i 水旁兴头小倒立 34 w 人和八,登祭头 44 o 火业头,四点米 35 q 金勺缺点无尾鱼,犬旁留叉儿一点夕,氏无七 45 p 之字军,摘礻(示)衤(衣)(之字军盖建道底) \u0026mdash;- \u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;- \u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 11 g 王旁青头戋(兼)五一(“兼”与“戋”同音) 21 h 目具上止卜虎皮(“具上”指具字的上部) 12 f 土士二干十寸雨(可别忘了革字底) 22 j 日早两竖与虫依 13 d 大犬三羊古石厂(羊底龙头套上套下) 23 k 口与川,字根稀 14 s 木丁西 24 l 田甲方框四车力(“方框”即“口”) 15 a 工戈草头右框七(“右框”即“匚”) 25 m 山由贝,下框几 \u0026mdash;- \u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;- \u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 51 c 又巴马,丢矢矣,(“矣”去“矢”为“厶”) 54 n 已半巳满不出己,左框折尸心和羽 52 x 慈母无心弓和匕,幼无力(“幼无力”即“幺”) 55 b 子耳了也框向上。(“框向上”即“凵”) 53 v 女刀九臼山朝西。(“山朝西”即“彐”) 字根间的结构关系 拆字 是学习五笔的一个重要环节,光背会了字根,有的汉字不知道拆成什么样的字根,也是无法输入的。\n我们是由浅入深地开始讲,先谈谈字根间的结构关系 \u0026ndash; 单、散、连、交 。\n单,就是指这个字根本身就是一个汉字,包括:\n五种基本笔划(一、丨、丿、丶、乙); 25 个键名字根; 字根中的汉字。 散,就是指构成汉字折字根不止一个,且汉字之间有一定的距离,比如“苗”、“汉”字等。\n连,是指一个字根与一个单笔划相连,比如“且”、“尺”字等。\n交,是指两个或多个字根交叉重叠构成的汉字,比如“本”、“申”字等。\n由两个以上字根组成的汉字,称为 合体字 。\n其中,对于四个或四个以上的字根的合体字,它的输入方法是按照书写顺序,取第一、二、三、末四个字根的编码。\n成字字根 在字根总表中,除了键名字根外,本身就是汉字的字根,称为 成字字根 ,比如“马、手、刀”等。这样的成字字根一共有 65 个,但这些字经常要输入,所以需要掌握其输入方法。\n怎么输入呢?\n成字字根的输入方法是:先打一下该字根所在的键,再打该字根的第一、第二及最末一个单笔画。\n即: 键名+首笔代码+次笔代码+末笔代码。\n汉字的拆分 在拆分汉字的时,先要注意按书写顺序来拆分汉字,然后对里面的一些复杂字根,按照它的自然男人界限进行拆分,对界线不明显了,就要按拆分原则进行拆分。\n那么,书写顺序、拆分原则,指的都是什么呢?\n在书写汉字时,讲究“先左后右,先上后下,先横后竖,先撇后捺,先内后外,先中间后两边,先进门后关门”等。这些都是语文的基本知识,就不多说了。在拆字时,同样要注意书写顺序。\n拆分原则,归纳起来有四点:“取在优先,兼顾直观,能散不连,能连不交。”\n有些汉字,它们所含的字根相同,但字根之间的相对位置不同,比如“旭”和“旮”等。我们把汉字各部分间的位置关系类型叫做 字型 ,在五笔中,把汉字分为三种字型:左右型、上下型、杂合型。\n** 识别码\n我们知道,一个合字体的取码规则是这个字的一、二、三、末字根,这只是针对四个字根以上的汉字。如果是这个字只有二个字根或三个字根构成,比如“叭”,这时怎么输入呢?\n们试试,在五笔状态下键入“叭”的两个字根的编码,kw,我们发现出现在第一条的是“只”字,原来“叭”与“只”都是由字根“口”和“八”组成的,其编码也是 kw。\n由于编码少,信息量不足,会造成重码。\n如何消除重码呢?\n汉字的笔画有 5 种,字型有 3 种,所以末笔字型交叉识别码共 15 种,也就是每个区位的前三位是作为识别码来用的。\n末笔约定 我们在使用识别码输入汉字时,对汉字的末笔有一些约定,需要注意。\n首先,为了有足够多的区分能力,对“辶”、“廴”的字和全包围字,它们的“末笔”规定为被包围部分的末笔。为什么要这样约定呢?\n例如,如果“辶”为末笔的话,“边”、“连”的识别码都是 v,就无法区分了,所以才这样约定。\n哈,约定末笔是为了便于打识别码。\r其次,对“九、刀、七、力、匕”等字根,当它们参加“识别”时一律用“折笔”作为末笔。\n然后,“我”、“贱”、“成”等字的“末笔”,遵循“从上到下”的原则,末笔应该是“丿”。\n最后,还有带单独点的字,比如“义”,“太”,“勺”等,我们把点当作末笔,并且认为“丶”与附近的字根是“连”的关系,所以为杂合型,识别码为 43,也就是 i。\n我们在学习五笔输入法的过程中,识别码的判断是一个难点,虽然只有很少的字需要加识别码,但为了提高录入速度,还是要掌握这部分内容的。\n一级简码 在五笔中,挑出了在汉语中使用频率最高的 25 个汉字,把它们分布在键盘的 25 个字母上,并称之为 一级简码 。\n输入一级简码的方法是: 按一下简码字所在的键,再按一下空格。\n","date":"2020-11-02","permalink":"https://loveminimal.github.io/posts/wubi/","summary":"\u003cp\u003e🔔 本文主要摘录自 \u003ca href=\"http://www.wubizi.net/xuewubi/\"\u003e问问学五笔\u003c/a\u003e\u003ca href=\"http://www.wubizi.net/xuewubi/\"\u003e1\u003c/a\u003e\u003c/p\u003e","title":"五笔那些事儿"},{"content":"在认识、学习 nginx 之前,我们先来看一下代理是什么,如果你已经非常熟悉代理,可以直接跳到第二个章节。\n关于代理1 *注:该节原创发布于慕课网,作者:咚咚呛。\n要说清楚什么是反向代理,先来了解下最简单的 c/s (client-server)架构,即以单个节点作为后端 server 的 c/s 架构。\n单点服务的 c/s 架构 在普通的开发中,单点 server 的服务方式非常常见,比如使用 django 框架做后台服务的时候,就可以通过简单的 =run server= 命令起一个临时的服务,接着通过地址就可能访问到这个服务了。\n当然,对于 django 而言, run server 命令只是提供开发周期的调试使用的便捷工具,更一般的情况是通过 uwsgi 把这个 django 服务跑起来,以获取比 run server 更好的服务性能。\n对于请求量非常少的服务,这样的部署不会有什么问题,但随着服务请求量越来越多,这样的部署架构就很有问题了。\n首先,从服务器的物理特性来看,这个服务器可能就不能支持这么高的请求量。 这种情况下,就迫使开发者去把服务迁移到一个 cpu 更强、内存更高,综合性能更好的服务器。\n在不考虑成本的条件下,总可以找到性能更好的服务器来支撑服务,然而,这是不现实的,地主家也没有余粮啊……\r其次,如果服务 server 单节点发生了故障,就必然会影响整个服务。 因为对于众多的 client 客户端都是连接的一个 server 服务端,一旦这个节点不可用了,势必会影响所有使用了客户端的用户。\n那么,如何解决这两个显而易见的问题呢?\n如果有一种横向拓展架构,使得服务可以支撑的请求量可以随着服务的横向拓展面增加就好了。\n基于代理的 c/s 架构 一种基于代理的可横向拓展的 c/s 架构,如下:\n在这个部署的架构中,除了 server 节点外,还多出一个叫 proxy 的节点,这个节点的做什么的呢?\nproxy 节点,用于把接收的所有请求都转发到它后面的 server 节点中,然后等待 server 节点处理请求,再从 server 节点取回执行结果返回到 client 。所以,实际上 proxy 节点不处理任何的请求。\n下面让我们来看一下,这种架构是怎么解决了上述问题的,之后再思考 proxy 的这个角色。\n1. 服务器性能不足的问题\n在这个架构里,假设 server 节点 s1 性能到达瓶颈了,不能处理更多请求了。我们可以添加 server 节点 s2 ,同时告诉 proxy 节点,让它把部分原来转发到 s1 节点的请求转发到 s2 节点。如此,通过服务分流减少压力的方法就可以解决原来 s1 节点性能不足的问题了。\n2. 单点服务器挂掉的问题\n在这个架构里,假设 server 节点 s1 和 s2 能够提供的服务是一样的。如果 s1 挂掉了,proxy 节点存在,且能够察觉到 s1 挂掉了的话,就让 proxy 节点把原来要转发给 s1 节点的请求转发到 s2 进行处理,如此,通过服务冗余的方法就可以解决原来 s1 突然挂掉影响服务的问题了。\n那么,proxy 节点到底是什么呢?\nproxy 是什么 proxy ,就是代理。 如何理解呢?\n在第一个问题中,proxy 节点通过服务分流的方法来减少 s1 的压力。对于原来应该被 s1 服务的,却由于被 proxy 节点转发而被 s2 服务的 client 客户端而言,其实 client 并不知道它的请求是由 s1 处理的,还是由 s2 处理的,它只需要往 proxy 节点发送请求就好了,剩下的工作就由 proxy 节点去解决了。\n也就是说,proxy 节点相当于一个中介,“代理” client 去寻找实际的 server 节点去完成服务。\n这种模式在现实生活中也非常常见,在买房的时候,通常由房产中介帮助你完成和卖者之间的手续,而不需要你亲自去处理这些事情,你只需要处理自己与中介之间的手续就可以了,这里的中介,就和我们的 proxy 节点工作非常类似。\n同样的,在第二个问题中,client 并不需要感知 s1 是否还能正常工作,只需要把请求发送给 proxy 节点,由它去帮忙处理就可以了。\n由上可知,基于代理的可横向拓展的 c/s 架构中,proxy 节点就是我们的代理节点。\n正向代理和反向代理 *开始先做个总结:\r所谓“正向代理”其实应该称为“客户端代理”,而“反向代理”应该称为“服务端代理”。\n本质上来讲,代理都是存在于 client 和 server 之间,但是由于性质不同,可以分为正向代理和反向代理两种。那么,什么是反向代理?什么是正向代理呢?\n我们来看一个直观的例子,假设有 a、b 和 c 三人,他们之间存在借钱的关系。\n1. 正向代理\na 需要钱,a 知道 c 有很多钱,想向 c 借钱; 但是 a 和 c 有矛盾,于是 a 想到通过 b 去借 c 的钱; b 向 c 借到钱了 ,c 并不知道 a 的存在; b 再把借到的钱交给 a 。 在这个过程中,b 就充当了代理借钱的角色,并且是代替 a 去借钱的,这样就是正向代理。\n比如国内日常使用的“科学上网” vpn 工具,就是正向代理了。比如,小明要访问 twitter ,但是访问不了,就委托 vpn 帮他访问,然后再把响应结果返回给他。 twitter 只知道是 vpn 访问它的,并不知道也不关心 vpn 把结果返回给谁。 对于小明们来说,vpn 就是他们的访问代理,即正向代理。\r2. 反向代理\na 需要钱,c 有很多钱,a 不知道 c 有很多钱; a 找 b 借钱; b 知道 c 有很多钱; b 向 c 借钱,并把借到的钱给 a ,而没有使用自己的钱借给 a ; a 拿到钱后,以为钱是 b 的,a 不知道 c 的存在。 在这个过程中,b 也充当了代理代理借钱的角色,不过这次不是代替 a 去借的,而是用 c 的钱借给 a 的,换言之即是代替 c 将钱代给了 a ,这就是反向代理。\n同样都是 a 借钱:\r正向代理是 a 委任 b 去借。b 去借钱,借钱的人只知道钱借给了 b ,并不知道、也不关心 b 拿来做什么用。\n反向代理是 a 去向 b 借钱,a 并不知道、也不关心 b 的钱是从哪儿来的。\n3. 两者区别\n1)服务对象不同:\n正向代理,代理的是客户端,也就是例子中的 a ,服务端并不知道实际发起请求的客户端; 反向代理,代理的是服务端,也就是例子中的 c ,客户端并不知道实际提供服务的服务端。 figure:正向代理\nfigure:反向代理\n2)安全模型不同:\n正向代理,允许客户端通过它访问任意网站并且隐藏客户端自身,因此必须采取安全措施以确保仅为授权的客户端提供服务; 反向代理,对外都是透明的,访问者并不知道自己访问的是代理,并不知道服务节点的存在,认为处理请求的就是代理节点。 总而言之,正向代理是从客户端的角度出发,服务于局域网用户,以访问非特定的服务,其中最典型的例子就是翻墙;反向代理正好与此相反,从服务端的角度出发,服务于所有用户,隐藏实际的服务节点,服务节点的架构对用户透明,以代理节点统一对外服务。\nnginx 那些事2 了解了什么是代理,下面我们要进入正题认识下 nginx 了。\nnginx 是异步框架的网页服务器,也可以用作反向代理、负载平衡器和 http 缓存,该软件由 伊戈尔·塞索耶夫 创建并于 2004 年站首次公开发布,官方测试 nginx 能够支支撑五万并发链接,并且 cpu 、内存等资源消耗非常低,运行非常稳定。\nnginx 这么强劲,都有些什么应用场景呢?如下:\nhttp 服务器。nginx 可以独立提供 http 服务,用做网页静态服务器; 虚拟主机。nginx 可以实现在一台服务器虚拟出多个网站,如个人网站使用的虚拟主机; 反向代理、负载均衡。nginx 可以实现请求分流,避免单点服务器宕机影响服务。 安装 → 具体安装过程可以参考该链接。\n# 准备工作 yum install gcc-c++ # 安装需要的 gcc 环境 yum install -y pcre pcre-devel # 安装 perl 兼容的正则表达式库 yum install -y zlib zlib-devel # nginx 使用 zlib 对 http 包进行 gzip yum install -y openssl openssl-devel # 支持 https # 下载安装 nginx - http://nginx.org # 可以修改为你要的版本,目前最前为 1.19.3 wget http://nginx.org/download/nginx-1.8.0.tar.gz tar zxvf nginx-1.8.0.tar.gz cd nginx-1.8.0 # 使用 configure 命令创建 makefile 文件 ./configure --prefix=/usr/local/nginx # 指向安装目录 make \u0026amp;\u0026amp; make install # nginx 的启动和访问: cd nginx # 进入 nginx 安装目录 cd sbin \u0026amp;\u0026amp; ./nginx # 进入 nginx 的执行目录并启动 ./nginx ps aux | grep nginx # 启动后台查看进程 # nginx 的一些常用操作: ./nginx -s stop # 停止 nginx ./nginx -s quit # 强制停止 nginx ./nginx -t # 校验 nginx 配置正确与否 ./nginx -s reload # 重启 nginx,用于修改配置文件后更新 nginx 状态 静态网站部署 将我们的网页内容(假设为 index 文件夹 )上传到服务器的 /usr/local/nginx 下,并更新 /usr/local/nginx/conf 下的 nginx.conf 配置文件,如下:\nworker_processes 1; ... http { ... server { listen 80; server_name localhost; location / { root index; # 目录名称 index index.html index.htm; # 文件名称 } ... } } 重启 nginx 即可,打开服务器的 ip 即可访问。\n配置虚拟主机 (1)上传静态网站:\n将 index 目录上传至 /usr/local/nginx/index 下; 将 regist 目录上传至 /usr/local/nginx/regist 下。 (2)修改 nginx 的配置文件:\n# 配置 index 访问资源 server { listen 80; # 监听端口 server_name localhost; # 域名或 ip location / { # 访问路径配置 root index; index index.html index.htm; } ... } # 配置 regist 访问资源 server { listen 81; server_name localhost; location / { root regist; index regist.html; } } (3)重启 nginx ,可以通过 80,81 访问不同的资源。\n综上,就实现了一台虚拟主机部署两个项目,一个 nginx 虚拟出来了两个主机,实现了 端口号配置虚拟主机 。\n当然,还可以通 过域名配置虚拟主机 ,如下:\n# 配置 index 访问资源 server { listen 80; # 监听端口 server_name www.travel.com; # 域名或 ip location / { # 访问路径配置 root index; index index.html index.htm; } ... } # 配置 regist 访问资源 server { listen 80; # 端口号相同 server_name www.regist.com; # 域名不同,以此进行区分 location / { root regist; index regist.html; } } 反向代理 这里我们将实现好的案例(war 包)部署到服务器的 tomcat 中的根目录下:\n为了操作方便(不需要输入对应的项目名称访问)将项目命名为 root.war ; 将服务器 /usr/local/tomcat/apache-tomcat-7.0.57/webapps 下的内容删除,将打好的包导入即可。 此时,输入相应 ip:port 如 192.168.245.129:8080 即可正常访问。\n接下来,我们看看如何用 nginx 配置反向代理:\n重新启动 nginx ,输入 www.travel.com 即可实现访问。\n负载均衡 什么是负载均衡呢?\n负载均衡(i.e. load balance),就是分摊到多个操作单元上进行执行,例如 web 服务器、ftp 服务器、企业关键应用服务器等,从而共同完成工作任务。\n实际应该部署到不同的服务器上,这里为了演示方便,就在同一台服务器配置三个 tomcat 。\n将刚才存放工程的 tomcat 复制三份,修改端口分别为 8080,8081,8082 ,修改 server.xml 的端口,分别启动这三个 tomcat 服务。\n配置负载均衡:\nworker_process 1; ... http { ... # 配置要代理的网址 # 默认如下配置是平权的,随机选概率相同 1:1:1 # 也可以通过加权,如: # server 192.168.245.129:8080 weight=2; # 此时的权重比为 2:1:1 upstream tomcat-travel { server 192.168.245.129:8080; server 192.168.245.129:8081; server 192.168.245.129:8082; } # 配置 index 访问资源 server { listen 80; server_name www.travel.com; location / { # root index; # 对应要代理的网址 proxy_pass http://tomcat-travel; index index.html index.htm; } ... } } 如 nginx 这种配置性的软件,用时查询即可,重在理解它的原理。\rproxypass3 4 配置语法:\nsyntax:\tproxy_pass url;\rdefault: —\rcontext: location, if in location, limit_except 其中,url 用于设置代理服务器的协议和地址,以及可选的 uri ,一般表现形式为:\nprotocol://ip:port[uri] protocal://domain[uri] 由于 url 末尾 是否存在 uri 的处理逻辑不同,我们下面来着重分析一下。\n1. url 末尾存在 uri\n*注意 / 也算是 uri\n处理逻辑:代理请求时,会先将 请求的 uri 中 localtion 匹配的部分 替换成 proxy_pass 指定的 uri ,再将 最终的 uri 拼接到 代理地址 ,才是 最终访问的 url 。\n例如,有如下配置:\nlocaltion /proxy { proxy_pass http://127.0.0.1:8099/svr1; # uri 为 \u0026#39;/svr1\u0026#39; } 我们发起如下请求: http://localhost:8088/proxy/index.html ,详细解析如下表。\n部分 说明 请求的 uri /proxy/index.html location 匹配的部分 /proxy proxy_pass 指定的 uri /svr1 最终的 uri /svr1/index.html 代理地址 http://127.0.0.1:8099 最终访问的 url http://127.0.0.1:8099/svr1/index.html 即:\n原本访问路径为: http://localhost:8088/proxy/index.html , 实际请求路径为: http://127.0.0.1:8099/svr1/index.html 。 2. url 末尾不存在 uri\n处理逻辑:代理请求时,直接将 请求的 uri 拼接到 代理地址 ,就是 最终访问的 url 。\n例如,有如下配置:\nlocation /proxy2 { proxy_pass http://127.0.0.1:8099; # 无 uri } 我们发起如下请求: http://localhost:8088/proxy2/index.html ,详细解析如下表。\n部分 说明 请求的 uri proxy2/index.html 代理地址 http://127.0.0.1:8099 最终访问的 url http://127.0.0.1:8099/proxy2/index.html 即:\n原本访问路径为 http://localhost:8088/proxy2/index.html , 实际请求路径为 http://127.0.0.1:8099/proxy2/index.html 。 看 proxy_pass 的使用的方法本质上就是就这么简单,官网文档这部分文档内容也不是很多,有时候事物往往是由于我们理解不当,而被过分的复杂化了。\rupstream 这个方法主要是方便把单独的代理存储为独立的文件进行区分和设置,可以更好地进行多个服务器的代理设置,包括每个子服务器的权重,以及分发的方式等。\n配置范例:\nupstream backend { # backend 为当前配置的名称,与 proxy_pass 参数对应 ip_hash; # 表示负载均衡配置 server backend1.example.com weight=5; server backend2.example.com:8080; server unix:/tmp/backend3; } server { location / { proxy_pass http://backend; } } 关于 ip_hash ,它表示负载均衡的配置,它的设置可以使每个请求按访问的 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 的问题。备选为 url_hash ,表示按用户的访问 url 来进行分配,这样访问相同的 url 时会指向同一台服务器,主要用于下载站点,可以节约带宽资源。\n其他 nginx 一个 server 配置多个 location 1 nginx 作为代理服务器,可以配置多个 location,通过访问不同路径来访问不同目录。比如, location / 用于访问官网首页, location /docs 用于访问帮助文档。\nserver { listen\t8088; server_name\tlocalhost; location / { root /path/to/site/index; index\tindex.html index.htm; } location /docs { alias /path/to/docs;\t# 注意这里是 alias index\tindex.html index.htm; } …… } alias 和 root 有什么区别呢?\nroot 和 alias 都可以定义在 location 模块中,都是用来指定请求资源的真实路径,比如:\nlocation /abc/ { root /data/www; } 请求 http://ip:port/abc/123.png 时,在服务器里面对应的真正的资源是 /data/www/abc/123.png,也就是说, root 真实路径是 root\t指定的值加上 location 指定的值。\n而 alias 指定的路径是 location 的别名,不管 location 的值怎么写,资源的真实路径都是 alias 指定的路径,比如:\nlocation /abc/ { alias /data/www; } 请求 http://ip:port/abc/123.png 时,在服务器里对应的真正的资源是 /data/www/123.png,而不再包含 location 指定的值了。\n注意:\n在一个 location 中, alias 可以存在多个,但是 root 只能有一个; alias 只能存在与 location 中,但是 root 可以用在 server、http 和 location 中。 ","date":"2020-10-23","permalink":"https://loveminimal.github.io/posts/nginx/","summary":"\u003cp\u003e在认识、学习 Nginx 之前,我们先来看一下代理是什么,如果你已经非常熟悉代理,可以直接跳到第二个章节。\u003c/p\u003e","title":"nginx"},{"content":"css 中的 transform、transition 和 animation ,简称 tta。\ncss 什么的这种逻辑性略弱、规则性很强的领域,边用边深入就好,不常用的记了也会忘。\r简介 transform 其实是描述元素的静态属性,一旦写到 style 里面,将会直接显示作用,本身不会呈现任何动画效果。\ntransition 和 animation 两者都能实现动画效果, transfrom 通常是配合这两者使用的。\n那么, transition 和 animation 又有什么不同呢?\n其主要区别在于:\n动画不需要事件触发,过渡需要; 过渡只有一组(两个:开始-结束) 关键帧,动画可以设置多个。 transform 变换 transform 属性应用于元素的 2d 或 3d 转换,这个属性允许你将元素旋转,缩放,平移,倾斜(拉伸)等。\ntransform: none|transform-functions; transform-functions 是什么呢?如下表:\n#+caption: 常用的变换函数值\n值 描述 \u0026hellip; matrix(n,n,n,n,n,n) 定义 2d 转换,使用六个值的矩阵 translate(x,y) 定义 2d 转换 scale(x[,y]) 定义 2d 缩放转换 rotate(angle) 定义 2d 旋转,在参数中规定角度 skew(x-angle,y-angle) 定义沿着 x 和 y 轴的 2d 倾斜转换 \u0026hellip; transition 过渡 transition 属性设置元素当过渡效果。\ntransition: property duration timing-function delay; #+caption: 过渡效果属性值\n值 描述 transition-property 指定 css 属性的名称 transition-duration 指定过渡持续时间 transition-timing-function 指定过渡函数 transition-delay 指定过渡延迟时间 *注:应始终指定 transition-duration 属性,否则持续时间为 0 , transition 不会有任何效果。\n.select { ... width: 100px; transition: width 2s; ... } .select:hover { width: 200px; } transition 的优点在于简单易用,但是它有几个在的局限:\n需要事件触发,所以没法在网页加载时自动发生; 是一次性的,不能重复发生,除非一再触发; 只能定义开始状态和结束状态,不能定义中间状态; 一条规则只能定义一个属性的变化,不能涉及多个属性。 自古有矛必有盾!\rcss animation 就是为了解决这些问题而提出的。\nanimation 动画 1 css3 可以创建动画,它可以取代许多网页动画图像、flash 动画和 javascript 实现效果。\n那么,css3 动画是什么呢?\n动画是使元素从一种样式逐渐变化为另一种样式的效果,你可以改变任意多的样式次数,且应该用百分比来规定变化发生的时间,或用关键词 from (0%)和 to (100%)。\nanimation: name duration timing-function delay iteration-count direction fill-mode play-state; #+caption: 动画属性表\n值 描述 animation-name 指定关键帧动画的名称 animation-duration 指定动画播放所需时间 animation-timing-function 指定动画播放方式 animation delay 指定动画开始时间 animation-iteration-count 指定动画的播放次数,默认为 1 ,若为 infinite ,则无限次循环播放 animation-direction 指定动画的播放方向,默认为 normal ,若为 alternate ,则轮流反射播放 animation-fill-mode 规定当动画不播放时(完成或有延迟未开始播放时),要应用到元素的样式 \u0026hellip; hmm... 其实这种属性表是令人讨厌的,还是具体的例子看起来更加明了,另外,大多数情况下,你并不需要了解这么多属性。\r*注:必须定义动画的名称和动画的持续时间,如果省略持续时间,动画将无法运行。\n还是来看个例子吧。\n.select { ... animation: myanimation 5s; ... } @keyframes myanimation { 0% {background: red;} 25% {background: yellow;} 50% {background: blue;} 100% {background: green;} } 看!关键在于 =@keyframes= 可以让你定义指定的帧! 你只需要定义的个动画名称和持续时间,然后用 =@keyframes= 去按百分比指定帧效果就可以了。\n","date":"2020-10-19","permalink":"https://loveminimal.github.io/posts/css-tta/","summary":"\u003cp\u003eCSS 中的 \u003ccode\u003etransform、transition\u003c/code\u003e 和 \u003ccode\u003eanimation\u003c/code\u003e ,简称 TTA。\u003c/p\u003e\n\u003cdiv class=\"oh-essay\"\u003e\r\nCSS 什么的这种逻辑性略弱、规则性很强的领域,边用边深入就好,不常用的记了也会忘。\r\n\u003c/div\u003e","title":"css 中的动效"},{"content":"开发环境 jenkins 的 git branches 和项目名称对照表及其说明和备忘。\n备忘 公司 erp\nhttp://192.168.4.223:34567/erp/login.aspx liuzengxi / 123456 相关账号地址 开发 jenkins:\n= 好吧,这玩意儿基本上已经废弃了……*\nhttp://192.168.51.218:6024/ user: admin pswd: admin 生产 jenkins:\nhttp://jenkinsiyin.oa.com/ liuzengxi/liuzengxi@i-yin.com.cn(账号/密码) *如果访问不了在 hosts 文件中增加 192.168.51.223 jenkinsiyin.oa.com\ngit:\nhttp://new-git.oa.com/ elk (测试)\n192.168.51.223 添加到 hosts http://kibana-test.i-yin.com.cn\n(生产)\nesignsys.i-yin.com.cn 对应的 elk:\nhttp://kibana.i-yin.com.cn/app/kibana#/discover?_g=()\u0026amp;_a=(columns:!(_source),index:fde18b30-5d4b-11ec-bf49-b71ea1532d69,interval:auto,query:(language:lucene,query:''),sort:!('@timestamp',desc))\nuser/pswd: saas / abcd@2019\n相关部署环境 最新 共享部署石墨文档\n新疆税局 工作时间:10:00-13:30 15:30-19:30\n该项目为独立项目,兼容 ie8 兼容模式,原生 html+css+js 😱\ngit 仓库:http://new-git.oa.com/zhizhi/xinjiangshuiwu.git\n签章系统 签章系统的私有化部署项目较多,以不同分支进行管理。\n对外演示:\n管理后台 http://esignsys.i-yin.com.cn/sign-sys-admin/#/login 应用前台 http://esignsys.i-yin.com.cn/sign-sys/#/login 以下是签章系统环境,每套都是独立的,不要再混了\n- 10.9.2.29 sign.oa.com(标准版)\n- 10.9.2.28(北京五矿)\n- xjbdc.i-yin.com.cn(不动产外网环境)\n- 192.168.49.224(招银外网环境)\n- 192.168.51.251(新钢项目)\n招银理财外网地址: http://121.201.40.131:8810/sign-sys/#/login\njenkins 的 git branches 项目名称 是否需要本地构建 说明 sign-sys-front 签章系统前端 ✔ http://new-git.oa.com/liangqiang/sign-system.git sign-sys-front-admin 签章系统后台管理系统 ✔ http://new-git.oa.com/liangqiang/sign-system-admin.git iyin-sign-front-operation 签章系统运营平台 ✔ http://new-git.oa.com/zhizhi/sign-system-operation.git sign-sys-demos 签署演示 http://new-git.oa.com/zhizhi/sign-sys-demos.git saas 生产运维系统 jenkins 的 git branches 项目名称 是否需要本地构建 说明 saas-front-develop 生产运维系统前端代码 ✘ http://new-git.oa.com/saasplatform/front/saas.git 云签 jenkins 的 git branches 项目名称 是否需要本地构建 说明 cloud-sign-admin-front 云签后台管理前端 ✔ http://new-git.oa.com/liangqiang/cloud-sign-admin-front.git cloud-sign-front 云签前端 ✔ http://new-git.oa.com/liangqiang/cloud-sign-front.git 合同平台 jenkins 的 git branches 项目名称 是否需要本地构建 说明 contract_web_front 合同平台前端 ✘ http://new-git.oa.com/tanggz/contract.git contract-platform-admin-font 合同平台 admin 项目 ✘ http://new-git.oa.com/liangqiang/contract-platform-admin-font.git contract-small1 合同平台小程序 http://new-git.oa.com/tanggz/contract_small1.git contract-ukey-localsign 合同平台本地签 http://new-git.oa.com/loveminimal/contract-ukey-localsign.git contract-sign-sys-demos 合同签署演示 http://new-git.oa.com/loveminimal/sign-sys-demos.git 官网项目 jenkins 的 git branches 项目名称 是否需要本地构建 说明 ay-mobile 安印官网移动端 ✘ http://new-git.oa.com/hongjin/ay-mobile.git new-iyin-website 安印新官网 http://new-git.oa.com/frontend/iyin/new-iyin-website.git evidence 存证 http://new-git.oa.com/tanggz/evidence.git sign-verify 验签 http://new-git.oa.com/hongjin/sign-verify.git iyin-demo http://new-git.oa.com/liangqiang/iyin-demo.git ","date":"2020-09-29","permalink":"https://loveminimal.github.io/posts/work/","summary":"\u003cp\u003e开发环境 Jenkins 的 Git Branches 和项目名称对照表及其说明和备忘。\u003c/p\u003e","title":"work"},{"content":"i.e. nmn、numbered musical notation\n_简谱 2 是指一种简易的记谱法,在字母简谱和数字简谱两种。其起源于 18 世纪的法国,后经德国人改良,遂成今日之貌。一般所称的简谱,系指数字简谱。\n数字简谱以可动唱名法为基础,用 1、2、3、4、5、6、7 代表音阶中的 7 个基本级,读音为 do、re、mi、fa、sol、la、si ,英文由 c、d、e、f、g、a、b 表示,休止以 0 表示。每一个数字的时值相当于五线谱的 4 分音符。\n基本要素 一般来说,所有音乐的构成有四个基本要素,而其中最重要的是“音的高低”和“音的长短”。\n1. 音的高低\n任何一首曲子都是高低相间的音组成的,从钢琴上直观看就是越往左面的键盘音越低,越往右面的键盘音越高。就数字简谱来说,在数字的正上方或下方有若干个点 . 、 : 符,点的多少可以理解为强度,越多越强,在其上方代表高音,在其下方代表低音。原理表现为物体振动频率,越高音振动频率越高,反之亦然。\n2. 音的长短\n除了音的高低外,还有一个重要的因素就是音的长短。音的高低和长短的标注决定了该曲子有别于另外的曲子,因此成为构成音乐的最重要的基础元素。原理表现为声音的时值长短。\n3. 音的力度\n音乐的力度很容易理解,也叫强度。一首音乐作品总是会有一些音符的力度比较强一些,有些地方弱一些。而力度的变化是音乐作品中表达情感的因素之一。原理表现为振幅的大小。\n4. 音质\n也可以称为音色。也就是发出音乐的乐器或人声。同样旋律的音高,男生和女生唱就不一样的音色,小提琴和钢琴的音色就不一样。\n上述四项构成了任一首乐曲的基础元素,应该说简谱基本可以将这些基础元素确标注。\n基础知识 1. 音符\n音符,表示音的高低的基本符号,用七个阿拉伯数字标记,它们的写法和读法如下:\n写法 1 2 3 4 5 6 7 i 读法 do re mi fa sol la si do 哆 来 米 发 索 拉 西 哆 以上各音其相对关系都是固定的,除了 3--4、7--i 是半音外,其它相邻两个音都是全音。\n音符是和音高紧密相连的,没有一个不带音高的音符。\n音符是数字符号如 1、2、3、4、5、6、7 就表示不同的音高,广义上说音乐里总共就有 7 个音符。\n1.1 音符高低\n为了标记更高或更低音,则在基本符号的上面或下面加上小圆点。在简谱中,不带点的基本符号叫做中音;在基本符号上面加上一个点叫高音,加两个点叫做倍高音,加三个点叫超高音;在基本符号下面加一个点叫低音,加两个点叫倍低音,加三个点叫超低音。\n记在简谱基本音符号下面的小圆点,叫低音点,它表示将基本音符降低一个音组,取降低一个纯八度。记两个圆点,表示将基本音符号降低两个音组,即降低两个纯八度。\n记在简谱基本音符号上面的小圆点,叫高音点,它表示将基本音符号升高一个音组,即升高一个纯八度。记两个圆点,表示升高两个音组,即升高两个纯八度。\n1.2 音符长短\n音乐中的音符除了有高低之分外,当然还要表示长短之分。这里引用一个基础的音乐术语 \u0026ndash; 拍子。拍子是表示音符长短的重要概念。\n表示音乐的长短需要有一个相对固定的时间概念。简谱里将音符分为全音符、二分音符、四分音符、八分音符、十六分音符、三十二分音符等。\n在这几个音符里面最重要的是四分音符,它是一个基本参照度量长度,即四分音符为一拍。这里的一拍是一个相对时间度量单位。一拍的长度没有限制,可以是 1 秒也可以是 2 秒或半秒。假如一拍是一秒的长度,那么二拍就是两秒;一拍定为半秒的话,两拍就是一秒的长度。一旦这个基础的一拍定下来,那么比一拍长或短的符号就相对容易了。\n用一条横线 - 在四分音符的右面或下面来标注,以此来定义音符的长短。下面列出了学用音符和它们的长度标记:\n音符名称 写法 时值 全音符 x--- 四拍 二分音符 x- 二拍 四分音符 x 一拍 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; 八分音符 x 半拍 - \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; 十六分音符 x 四分之一拍 = \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; 三十二分音符 x 八分之一拍 ≡ 通过上表可以看出:横线有标注在音符后面的,也有标注在音符下面的,横线标记的位置不同,被标记的音符的时值也不同。从表中可以发现一个规律,就是:要使音符时值延长,在四分音符右边加横线 - ,这时的横线叫延时线。延时线越多,音持续的时间(时值)越长。相反,音符下面的横线越多,则该音符时间越短。\n1.3 半音与全音\n音符与音符之间是有“距离”的,这个距离是一个相对可计算的数值。在音乐中,相邻的两个音之间最小的距离叫半音,两个半音距离构成一个全音。表现在钢琴上就是钢琴键盘上紧密相连的两个键盘就构成半音,而隔一个键盘的两个键盘就是全音。\n1.4 附点音符\n附点就是记在音符右边的小圆点,表示增加前面音符时值的一半,带附点的音符叫做附点音符。\n1.5 反复记号\n表示记号内的曲调反复唱(奏)。\n1.6 休止符\n音乐中除了有音的高低,长短之外,也有音的休止。表示声音休止的符号叫 休止符 ,用 0 标记。通俗点说就是没有声音,不出声的符号。\n休止符与音符基本相同,也有六种。但一般直接用 0 代替增加的横线,每增加一个 0 ,就增加一个四分休止符时的时值。\n2. 增时线和减时线\n简谱中,音的长短是在基本音符的基础上加短横线、附点、延音线和连音符号表示的。\n短横线的用法有两种:写在基本音符右边的短横线叫 增时线 ,增时线越多,音的时值就越长。不带增时线的基本音符叫四分音符,每增加一条增时线,表示延长一个四分音符的时间。写在基本音符下面的短横线叫 减时线 ,减时线越多,音就越短,每增加一条减时线,就表示缩短为原音符音长的一半。\n写在音符右边的小圆点叫做 附点 ,表示延长前面音符时值的一半。附点往往用于四分音符和少于四分音符的各种音符,带附点的音符叫附点音符。\n3. 变化音\n临时改变音的高低的符号叫临时变音记号,主要有升号、降号、还原记号等。升号定在音符左上方,表示该音要升高半音,如 #1 表示将 1 升高半音,在吉他上的奏法就是向高品位方向进一格。降号写在音符左上方,表示该音要降低半音,如 b3 表示将 3 降低半音,在吉他上的奏法就是向低品位方向退一格,空弦音降半音就要退到低一弦上去。还原号是将一小节内 # 或 b 过的某个音回到原来的位置。\n以上临时变音记号都是一小节内才起作用,过了这小节就不起作用了,一小节也就是 |xxxx| 。\n将标准的音符升高或降低得来的音,就是变化音。将音符升高半音,叫升音,用 # (升号) 来表示;将标准的音降低半音,叫降音,用 b (降号)表示;基本音升高一个全音叫重升音,用 x (重升)表示;基本音降一个全音叫重降音,用 bb (重降音)表示;将已经升高(包括重升)或降低(包括重降)的音,要变成原始的音,则用还原记号 ♮ 表示。\n4. 调号\n按照一定的次序和位置记在谱号的后面的记号叫做 调号 。调号总是只用同类的变音记号,即升记号或降记号。简谱的调号一般是用 1 等于 a、b、c、d、e、f、g 来表示,如 1c则表示该简谱是 c 调来记谱,如果要表示升降号的调,则在字母前加#号或者b` 号,可以参考一些简谱图片示例。\n5. 节奏和节拍\n掌握读谱,首先要掌握节奏,练习掌握节奏要能准确的击拍。击拍的方法是:手往下拍是半拍,手掌拿起有半拍,一下一上是一拍。\n乐曲或歌曲中,音的强弱有规律地循环出现,就形成节拍。节拍和节奏的关系,就像列队行进为整齐的步伐(节拍)和变化着的鼓点(节奏)之间的关系。\n单拍指每小节一个强拍。复拍每小节有一个强的,有若干个次强的。\n6. 调式音阶\n按照一定关系结合在一起的几个音(一般是七个音左右)组成一个有主音(中心音)的音列体系,构成一个调式。\n把调式中的个音,从主音到主音,按一定的音高关系排列起来的音列,叫 音阶 。\n6.1 大调式\n凡是音阶排列符合 全、全、半、全、全、全、半 结构的音阶,就是自然大调。这是使用的最广泛的调式。\n一般来说,一首音乐作品的开始音符是使用 1、3 或 5 的,而结束在 1 上的就是大调音乐,比如国歌就是大调音乐。要想真正搞懂大调音乐,必须要学习和声知识。\n6.2 小调式\n小调式有三种形式:\n自然小调:凡是音阶符合 全、半、全、半、全、全 结构的音阶,叫自然小调; 和声小调:升高自然小调音阶的第 vii 级,叫和声小调; 旋律小调:在自然小调音阶上行时升高它的 4、5 ,而下行时还原 5、4 叫旋律小调。 小调音乐一般第一个音符是从 6 或 3 开始,而结束在 6 上。比如《莫斯科郊外的晚上》就是小调音乐。同大调一样,要想真正搞懂小调音乐,必须要学习和声知识。\n7. 装饰音\n装饰音的作用主要是用来装饰旋律。它们用记号或小音符表示,装饰音的时值很短。有:\n倚音:指一个或数个依附于主要音符的音,倚音时值短暂。有前倚音、后倚音之分; 颤音:由主要音和它相邻的音快速均匀地交替演奏; 波音:由主要音和它上方或下方相邻的音快速一次或再次交替而成; 滑音:主要音向上或向下滑向某个音。滑音分上滑音、下滑音两种。滑音除声乐演唱这一技巧外,一切弦乐器都可演奏,但钢琴等键盘乐器是无法演奏这一技巧的。 8. 其他相关符号\n8.1 顿音记号\n用三角符标记在音符的上面,表示这个音要唱(奏)得短促、跳跃。\n8.2 连音线\n用上弧线标记在音符的上面,它有两种用法:\n延音线:如果是同一个音,则按照拍节弹奏完成即可,不用再弹奏; 连接两个以上不同的音,也称圆滑线,要求唱(奏)得连贯、圆滑。 8.3 重音记号\n用 \u0026gt; 或 □ 或 sf 标记在音符的上面,表示这个音要唱(奏)得坚强有力;当 \u0026gt; 与 □ 两个记号同时出现时,表示更强。\n8.4 保持音记号\n用 - 标记在音符的上面,表示这个音在唱(奏)时要保持足够的时值和一定的音量。\n8.5 小节线\n用竖线将每一小节划分开线叫小节线。\n8.6 终止线\n终止线是在乐曲最后,将要结束的地方,这里就不能单纯的画小节线,而是要区别一般的小节线来表示,这种表示的方法是用两条竖线,其中一条细线,一条粗线并行,细的一条在前,粗的一条在后,这叫“终止线”,表明乐曲终了。\n8.7 换气号\n用记号 v 标记,不服水土在此处换气。\n乐理识谱1 基本乐理是学习乐器的基础。初学者学习笛箫等乐器,在能够吹响并熟练掌握指法后,除了需要重点学习和掌握吹奏技巧和基本功如口风控制,气息把握等,还需要对基本乐理进行学习,因为想学好乐器,唱谱、节奏很重要。本文重点讲述基本乐理与识谱。\n笛箫类乐器不同于钢琴等乐器,除少数专业的笛子谱用五线谱记谱外,一般笛箫大多用简谱记谱。简谱所适用的规则,在笛箫谱上同样适用。但是笛箫谱不仅具有简谱的一般特征,还具有本身的专业特征。在谱面上还会标注笛箫演奏所涉及的各种演奏技法,如吐音(单吐、双吐)叠音、打音、颤音、指法等,而在单纯的简谱中则不会有这些内容。\n音的高低 在简谱中,用以表示音的高低及相互关系的基本符号为七个阿拉伯数字,即 1、2、3、4、5、6、7 ,唱作 do、re、mi、fa、sol、la、si ,称为 唱名 。单用以上七个音是无法表现众多音乐形象的。在实际作品中,还有一些更高或更低的音,如在基本音符上方加记一个 . ,表示该音升高一个八度,称为高音;加记两个 : ,则表示该音升高两个八度,称为倍高音。在基本音符下方加记一个 . ,表示该音降低一个八度,称为低音;加记两个 : ,则表示该音降低两个八度,称为倍低音。\n音的长短 在简谱中,如果音符时值的长短表示方法如下图:\n带有两个附点的单纯音符为复附点音符,第二个附点表示增长第一个附点时值的一半,即音符时值的四分之一。复附点常用于器乐曲中,在歌曲中很少使用。\n连音线和延音线 延音线 是一条向上或向下弯曲的弧线,其作用是:将两个或两个以上具有相同音高的音符相连,在演唱或演奏时作为一个音符,它的长度等于所有这些音符的总和。在单声部音乐中,连线永远写在和符干相反的方向。\n连音线 表示的是演奏法,可以把几个不同音高的音连在一起,表示为:在钢琴上是连音奏法,表示这几个音要演奏得连贯、圆滑。前者只是相同的音,而后者则不是。\n切分音 除了常用的强弱变化外,还有用“切分法”来进行强弱变化,所谓的切分法就是通过延长弱拍音符的时值使强拍的重音位置向前移到弱拍,改变了乐曲中的“以强拍为重音”的规律这个原弱拍上的音被称为切分音。切分音的记法是在一小节或一拍之内记成一个音,跨小节的记成加连线的两个音。\n休止符 在乐谱中表示音乐的休止(停顿)的符号称为 休止符 。简谱的休止符用 o 表示。休止符是一种不发声的符号,又称为“无声的音符”。在音乐中,休止符一般起句逗作用,并能加强歌(乐)曲的表现力,变化歌(乐)曲的情绪,使曲调的进行表现出对比的效果。\n休止符停顿时间的长短与音符的时值基本相同,只是不用增时线,而用更多的 o 来代替,每增加一个 o ,表示增加一个相当于四分休止符的停顿时间, o 越多,停顿的时间越长。同音符的音长一样,一个 o 表示休止一拍。\n反复记号 音乐反复记号有以下四种:\n第一种:终止线前一个冒号。分两种情况演奏:如果之前有一个终止线,在终止线之后有一个冒号的,则反复这两个反复记号中夹着的一段;如果之前没有反复记号,则从头开始反复。\n第二种:d.c. 记号。意大利语: da capo ,从头反复。当出现 fine 记号时则反复至 fine 记号结束。\n第三种:d.s. 记号。意大利语: dal segno ,从记号处重复。记号为 ※ ,从该记号处重复。\n第四种:反复跳越记号。反复跳越记号是段落反复记号的一种补充。一般有 1、2 两段,弹奏时从头到 1 结束,再从头跳过 1 弹 2 ,然后结束。\n拍子的拍号 将旋律的强拍与弱拍用固定音值进行强弱循环,有规律地组合,称为 拍子 。拍子分为单拍子与复拍子两种。每小节的每一拍由一个完整音符即单纯音符组成的拍子,称为单拍子;每小节的每一拍由一个附点音符或与其等值的音符组成的拍子,称为复拍子。用以表示不同拍子的符号称为拍号。拍号一般标记在调号的后边。例如: 1=c 2/4, 1=g 3/4 。\n标准的乐谱前面会标出速度,例如 “120” 表示每分钟 120 拍,那么 1拍=0.5秒 ,演奏者则需要对节奏(音长)进行掌控,这一点笔者认为都需要不断的学习才能掌握节奏的控制。但很多笛箫曲并不像歌曲一样有明确的拍长,比如曲谱并没有标出拍子的时值,而是标有“自由地”或者“欢快地”、“节奏自由(或者曲谱上有草字头的标记)”等等字眼,这就是笛箫的魅力所在,每个人对曲子的理解不同,可以结合作者的原作和自己的理解,自己把握曲子的节奏,这也并不是节奏全无的演奏,使乐曲的美感完全丧失,演奏者也需要不断的学习才能够演奏出一支富有情感的曲子。\n调与调号 调由两部分组成,即主音的高度与调式类别。如自然大调音阶 1、2、3、4、5、6、7、i , 1(do) 的高音等同于键盘中的 c 音,则此音阶称为 c 自然大调音阶;自然小调音阶 6、7、1、2、3、4、5、6 中, 6(la) 的音高等同于键盘中的 a 音,则此音阶称为 a 自然小调音阶。在简谱中,歌曲、乐曲调的高低均按大调的高低确定,即按憗(do)音的音高确定调的高低。因此 c 大调与 a 小调在简谱中的调号均为 1=c ,它不代表歌曲的调式。\n调号是用以确定歌曲、乐曲(或调子)高度的定音记号。在简谱中,调号是用以确定 1(do) 音的音高位置的符号。例如当一首简谱歌曲为 d 调是,其调号就为 1=d 。\n常用技术符号 笛箫音的强弱是与通过风门送进乐器内的空气量有关的。吹奏同一个音时,一般来说,强奏时的用气量比弱奏时用的气量大。乐谱中符号 p 表示弱音,符号 p 的数量越多表示音越弱;符号 f 表示强音,符号 f 的数量越多表示音越强。渐强符号为 \u0026lt; ,渐弱记号为 \u0026gt; 。其余的笛箫谱中常见的大部分符号如下图所示:\n","date":"2020-09-08","permalink":"https://loveminimal.github.io/posts/nmn/","summary":"\u003cp\u003ei.e. NMN、Numbered Musical Notation\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e_简谱\u003c/em\u003e \u003ca href=\"https://baike.baidu.com/item/%E7%AE%80%E8%B0%B1\"\u003e2\u003c/a\u003e 是指一种简易的记谱法,在字母简谱和数字简谱两种。其起源于 18 世纪的法国,后经德国人改良,遂成今日之貌。一般所称的简谱,系指数字简谱。\u003c/p\u003e\n\u003cp\u003e数字简谱以可动唱名法为基础,用 \u003ccode\u003e1、2、3、4、5、6、7\u003c/code\u003e 代表音阶中的 7 个基本级,读音为 \u003ccode\u003edo、re、mi、fa、sol、la、si\u003c/code\u003e ,英文由 \u003ccode\u003eC、D、E、F、G、A、B\u003c/code\u003e 表示,休止以 \u003ccode\u003e0\u003c/code\u003e 表示。每一个数字的时值相当于五线谱的 4 分音符。\u003c/p\u003e","title":"简谱"},{"content":"\r饮食调整,贵在平衡。保证必需营养吸收,减少不必要的食物摄入。\r七分饱 什么是七分饱呢?\n如何把握七分饱呢?\n七成饱,大部分人找不到这个点,经常把胃里感觉满的八成饱当成最低标准,甚至到了多吃一口就觉得胀的九成饱。\n因为大多数人都边吃边说笑,边吃边谈生意,边吃边上网看电视,分散注意力后,就很难感受到饱感的变化,不知不觉地饮食过量。\n怎么能感觉这么细致的差异呢?\n专心致志细嚼慢咽,放慢速度是关键。\n从第一口开始,感受自己对食物的急迫感,每吃一口后感受饥饿感的逐渐消退,胃里面逐渐的充实,体会这些不同饱感程度的区别。\n然后,找到七成饱的点,把它作为自己的日常食量,就能预防饮食过量。\n早餐 早餐是一天所有能量之源,一定要吃好早餐才能有充足的力量来进行一天的工作和生活。\n鸡蛋猪排三明治 简单的早餐也会有大营养,简易三明治加一杯热牛奶一份水果,这样食材很普遍却是可以满足人体内需求的重要营养。早餐一定要吃,而且要吃的丰富,别再用没时间找借口伤害你的身体。\n午餐 午餐吃个 5 ~ 7 分饱。\n晚餐 晚餐,少吃或不吃(以果蔬代之)。\n减肥不等于饿肚子 为了瘦下来,你还是节食挨饿吗?减肥不是节食。相信我,饿不了几顿的……\n减肥的关键是提升身体的热量缺口,也就是提升热量输出,降低热量摄入,以此来促进脂肪的分解。而挨饿只会让身体感到饥荒,从而导致身体基础代谢值下降,正确地饮食方法要牢记以下几个方面。\n1、用低热量的食物代替各种高热量的食物\n这样不用减少食物分量,也能控制卡路里摄入,还能避免饥饿感的出现。\n我们要选择清蒸、水煮的方法代替各种红烧、煎炸的做法,比如水煮土豆代替红烧土豆,可以避免食物热量大大飙升,选择饱腹感强、体积大的食物代替饱腹感差,体积小的食物,比如生菜、西蓝花代替各种加工零食。\n2、学会多喝水\n水是没有热量的,不会让你发胖。多喝水可以减缓饥饿感的出现,控制进食量,还能促进身体代谢循环,加速废物的排出,有助于脂肪的代谢。\n建议,每天的喝水量在 2l 左右,多个时间段补充,尽量在饭前喝,饭后跟睡前要少喝水。喝水要喝热水、温水,不要喝冷水,拒绝各种冷饮跟加工果汁,避免多余热量的摄入。\n3、三餐要规律,不要路过任何一餐。\n不吃早餐,跳过晚餐,平时饥一餐饱一餐的行为,容易诱发肠胃疾病,还容易在下一餐进行报复性进食,胃容量也会被撑大,不利于减肥。而规律的饮食习惯,有助于肠胃健康,让肠胃运转更加高效,从而减少脂肪的堆积。我们要三餐定时,保持饭吃八分饱,可以有效控制胃容量,健康地瘦下来。\n4、不要拒绝主食\n主食可以给身体补充碳水化合物,提供代谢动力。正常人每天的碳水主食摄入量在 250-300g 左右,减肥期间,我们可以降为 180-200g 左右,补充身体所需的碳水能力。\n此外,主食的选择方面,我们可以粗细粮结合,少吃一些面食跟米饭,适当吃一些糙米、燕麦、全麦包等粗粮,以此来控制升糖系数,延长饱腹时间,这样可以有效降低暴食几率,提升减肥成功率。\n至于量呢?七分饱 ❗️❗️❗️\n参考链接 https://new.qq.com/omn/20220507/20220507a0d3n900.html https://baijiahao.baidu.com/s?id=1626245545642571828\u0026amp;wfr=spider\u0026amp;for=pc https://mini.eastday.com/mobile/171210000039438.html https://jingyan.baidu.com/article/fcb5aff7c2e094adaa4a71b1.html ","date":"2020-08-31","permalink":"https://loveminimal.github.io/posts/diet-modification/","summary":"\u003cdiv class=\"oh-essay\"\u003e\r\n饮食调整,贵在平衡。保证必需营养吸收,减少不必要的食物摄入。\r\n\u003c/div\u003e","title":"饮食调整"},{"content":"linux 下源代码的编译安装,及其编译过程和可能遇到的问题。\n如何编译安装 1 下载源码包,解压、进入源代码目录后,执行以下命令:\n./configure make make install # 多数时候需要 `sudo` 以上是典型的使用 gnu 的 autoconf 和 automake 产生的程序的安装步骤。\n如何理解编译过程 2 3 ./configure configure 脚本是用来检查当前安装平台的开发环境,比如是不是有 cc 或 gcc ,也用来供用户指定程序包的编译参数、启用特性、安装路径等等。\n一般用来生成 makefile ,为下一步的编译做准备。 如下所示,可以通过在 configure 后加上参数来对安装进行控制,比如:\n./configure --prefix=/usr 意味着将该软件安装在 /usr 下面,执行文件就会安装在 /usr/bin (而不是默认的 /usr/local/bin ),资源文件就会安装在 /usr/share (而不是默认的 /usr/local/share )。\n# 通用的几个选项\r--prefix= # 指定安装的路径\r--sysconfdir= # 指定配置文件目录\r--enable-feature= # 启用某个特性\r--disable-feature # 禁用某个特性\r--with-function # 启用某个功能\r--without-function # 禁用某个功能 你可以通过 ./configure --help 查看更多。\nmake make 是 linux 开发套件里面自动化编译的一个控制程序。每个源代码都有专用的 makefile ,在执行 make 时依据 makefile 这个配置文件,调用指定的预处理器做处理、调用指定的编译器做处理、编译文件的顺序操作等。\n一般情况下,它所使用的 makefile 控制代码是由 configure 这个设置脚本根据给定的参数和系统环境生成的。\n*cmake5 cmake 就是一个与 make 同级别的编译工具,只不过它依靠的不是 makefile 作为编译规则,而是根据 cmakelists.txt 来编译的。它比 make 更高级,可以根据不同平台、不同的编译器,通过编写 cmakelists.txt ,可以控制生成的 makefile ,从而控制编译过程。\n如果有嵌套目录,子目录下可以有自己的 cmakelists.txt 。\nmake install 这条命令用来进行安装(当然有些软件需要先运行 make check 或 make test 来进行一些测试),一般需要你有 root 权限(因为要向系统写入文件)。\n其实是一些脚本,根据 makefile 文件中的设置将编译完成的文件安装到预定目录,如将创建出的二进制文件放到指定的二进制目录、库文件放到指定的库目录等等。\n扩展 - c/c++ 编译过程 4 相关名词 编译 ,是读取源程序(字符流),对之进行词法和语法的分析,将高级语言转换为等效的汇编代码,再转换为机器代码,保存到目标文件 =*.obj= 中(如果编译通过)。\n链接 ,是将有关的目标文件(库文件、.o 文件)彼此互相连接,即在一个文件中引用的符号同在另一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。\n编译过程 编译分为两个过程: 预处理阶段 和 编译、优化阶段 。\n预处理阶段:\n宏 #define ; 条件编译指令,如 #ifdef, #ifndef, #else, #elif, #endif 等; 头文件包含 #include \u0026lt;iostream\u0026gt; ; 特殊符号,如 line 标识被解释为当前行号, file 被解释为当前被编译的 c 源程序的名称。 编译、优化阶段:\n针对代码优化,不依赖具体计算机; 针对计算机的优化。 汇编把汇编语言代码翻译成目标机器指令,生成目标文件( .o 或 .obj 文件),至少需要表提供 3 张表:\n导出符号表 - 该目标文件可以提供的符号及地址; 未解决符号表 - 该目标文件告诉链接器哪些符号没找到地址; 地址重定向表。 链接的时候,链接器会为目标文件的 未解决符号表 里的符号在其他文件里寻找地址,但是每个目标文件的地址都是从 0x0000 开始的,这就导致直接将对方文件中符号拿过来用会是不正确的。为了区分不同的文件,链接器在链接时就会结每个目标文件的地址进行调整,如为 .o 导出的符号地址都加上起始地址。然而,因为加上了起始地址,符号在自身文件中的实际地址就不对了,需要再用一张 地址重定向表 记录符号相对自身文件的地址。\n链接过程 链接方式分为:静态链接和动态链接。\n静态链接 ,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中,这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。\n动态链接 ,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所做的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚拟地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。\ntodo 进阶 - linux 下源码编译安装详解 6","date":"2020-05-13","permalink":"https://loveminimal.github.io/posts/source-code-compile/","summary":"\u003cp\u003eLinux 下源代码的编译安装,及其编译过程和可能遇到的问题。\u003c/p\u003e","title":"源码编译安装"},{"content":"安装 去 manjaro 官网下载镜像 iso ,使用 rufus 刻录,安装过程……\n换源 1. 编辑\ncd /etc/pacman.d cp mirrorlist mirrorlist.bak nano mirrorlist 文件头部添加如下内容:\n## country : china\rserver = http://mirrors.tuna.tsinghua.edu.cn/manjaro/stable/$repo/$arch\r## country : china\rserver = https://mirrors.ustc.edu.cn/manjaro/stable/$repo/$arch #. 添加 archlinuxcn 源\ncd /etc nano pacman.conf 文件尾部添加如下内容:\n## 清华大学 (ipv4, ipv6, http, https)\r[archlinuxcn]\rserver = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch 安装 archlinuxcn-keyring ,如下:\nsudo pacman -s archlinuxcn-keyring 2. 更新\nsudo pacman -syy # 滚动升级一下(可选) sudo pacman -syyu 映射 caps lock 编辑 .zshrc 或 .bashrc ,添加如下内容:\nsetxkbmap -option ctrl:swapcaps 然后,执行 source ~/.zshrc 或 source ~/.bashrc 使配置生效。\n使用 xmodmap 工具 2 3 # 自定义映射表 xmodmap -pke \u0026gt; ~/.xmodmap # 在 ~/.xmodmap 中做好想要的修改 # 如,把 `shift_r` 映射为 `esc` keycode 62 = escape nosymbol escape # 如,把 `caps_lock` 映射为 `control_l` keycode 66 = control_l nosymbol control_l # 测试新的配置文件 xmodmap ~/.xmodmap 中文输入法 安装 fcitx 及其相关依赖,如下:\nsudo pacman -s fcitx fcitx-im fcitx-configtool 然后添加输入法配置文件 ~/.xprofile ,添加以下内容:\nexport gtk_im_module=fcitx\rexport qt_im_module=fcitx\rexport xmodifiers=\u0026#34;@im=fcitx\u0026#34; 之后,重启电脑后,添加新的输入法即可。\n五笔输入 windows 上使用的极品五笔,感觉很不错。 linux 上的 fcitx 也是包含 wubi 输入的,但是词库有点可怜, 所以 rime 是一个不错的选择,它是一个输入法框架,可以按需引入。\n1. fcitx rime\n去 rime 官网下载,如果使用拼音输入的话,按照其教程操作后即可,以下内容针对 fcitx 五笔的初始化(ibus 的皮肤不好看,官方的是针对 ibus 的), fcitx 的安装见 中文输入法 ↑ 。\n永远记住先把工具用起来,再慢慢研究配置。\nsudo pacman -s fcitx-rime cd ~/.config/fcitx/rime/ # 克隆极点五笔的配置,足够好用 git clone https://github.com/kylebing/rime-wubi86-jidian.git cp rime-wubi86-jidian/* ./ 如此,重启电脑后,在 fcitx config 工具中添加 rime 后,注销电脑再次登录后即生效。\n2. ibus rime\n# 安装 sudo pacman -s ibus ibus-rime rime-wubi *附上 ibus 在 =.xrpofile= 中的值,如下:\n#ibus\rexport gtk_im_module=ibus\rexport xmodifiers=@im=ibus\rexport qt_im_module=ibus\r# 自动重启 ibus 后台服务\ribus-daemon -x -d 中文字体1 # 文泉驿字体 sudo pacman -s wqy-bitmapfont wqy-microhei wqy-microhei-lite wqy-zenhei 美化 字段 选项 window decorations arc osx white transparent global theme blur-glassy maia plasma style blur-glassy 工具 工具 描述 plank 类 mac 的 doc 栏 albert 搜索 mac 的 spotlight faq obs 录制窗口撕裂 system settings → display and monitor → compositor → tearing prevention (\u0026#34;vsync\u0026#34;) 修改为 never 。\n","date":"2020-05-06","permalink":"https://loveminimal.github.io/posts/manjaro-linux/","summary":"","title":"manjaro linux"},{"content":" 道法自然!\n关于《老子》 《老子》一书,原著不过五千言,可以说,几乎是一字就涵盖一个观念的好文章,一句就涵盖有三玄三要的妙义。它告诉了我们许多法则。\n1 道可道,非常道。名可名,非常名。无,名天地之始。有,名万物之母。故常无,欲以观其妙。常有,欲以观其徼。此两者,同出而异名,同谓之玄。玄之又玄,众妙之门。\n2 天下皆知美之为美,斯恶已。皆知善之为善,斯不善已。故有无相生,难易相成,长短相较,高下相倾,音声相和,前后相随。是以圣人处无为之事,行不言之教,万物做焉而不辞。生而不有,为而不恃,功成而弗居。夫唯弗居,是以不去。\n3 不尚贤,使民不争;不贵难得之货,使民不为盗;不见可欲,使民心不乱。\n是以圣人之治,虚其心,实其腹,弱其志,强其骨。常使民无知、无欲。使夫智者不敢为也,为无为,则无不治。\n4 道冲而用之或不盈,渊兮似万物之宗。挫其锐,解其纷,和其光,同其尘。湛兮似或存,吾不知谁之子,象帝之先。\n5 天地不仁,以万物为刍狗;圣人不仁,以百姓为刍狗。天地之间,其犹橐籥乎,虚而不屈,动而愈出。多言数穷,不如守中。\n6 谷神不死,是谓玄牝,玄牝之门,是为天地根。绵绵若存,用之不勤。\n7 天长地久,天地所以能长且久者,以其不自生,故能长生。是以圣人后其身而身先,外其身而身存。非以其无私邪?故能成其私。\n8 上善若水,水善利万物而不争,处众人之所恶,故几于道。居善地,心善渊,与善仁,言善信,正善治,事善能,动善时。夫唯不争,故无尤。\n9 持而盈之,不如其已;揣而棁之,不可长保。金玉满堂,莫之能守;富贵而骄,自遗其咎。功遂身退,天之道。\n10 载营魄抱一,能无离乎?专气致柔,能婴儿乎?涤除玄览,能无疵乎?爱民治国,能无知乎?天门开阖,能无雌乎?明白四达,能无为乎?生之,畜之,生而不有,为而不恃,长而不宰,是谓玄德。\n11 三十辐共一毂,当其无,有车之用。埏埴以为器,当其无,有器之用。凿户牖以为室,当其无,有室之用。故有之以为利,无之以为用。\n12 五色令人目盲,五音令人耳聋,五味令人口爽。驰骋畋猎,令人心发狂。难得之货,令人行妨。是以圣人为腹不为目,故去彼取此。\n13 宠辱若惊,贵大患若身。何谓宠辱若惊?宠为下,得之若惊,失之若惊,是为宠辱若惊。何谓贵大患若身?吾所以有大患者,为吾有身。及吾无身,吾有何患?故贵以身为天下,若可寄天下。爱以身为天下,若可托天下。\n14 视之不见名曰夷,听之不闻名曰希,搏之不得名曰微,此三者不可致诘,故混而为一。其上不皦,其下不昧,绳绳不可名,复归于无物。是谓无状之状,无物之象,是谓惚恍。迎之不见其首,随之不见其后。执古之道,以御今之有。能知古始,是谓道纪。\n15 古之善为士者,微妙玄通,深不可识。夫唯不可识,故强为之容。豫兮若冬涉川,犹兮若畏四邻,俨兮其若容,涣兮若冰之将释,敦兮其若朴,旷兮其若谷,浑兮其若浊。孰能浊以静之徐清,孰能安以动之徐生。保此道者不欲盈。夫唯不盈,故能蔽不新成。\n16 致虚极,守静笃。万物并作,吾以观复。夫物芸芸,各复归其根。归根曰静,是谓复命。复命曰常,知常曰明。不知常,妄作凶。知常容,容乃公,公乃王,王乃天,天乃道,道乃久。没身不殆。\n17 太上,下知有之,其次,亲而誉之;其次,畏之;其次,侮之。信不足焉,有不信焉。悠兮其贵言,功成事遂,百姓皆谓我自然。\n18 大道废,有仁义。智慧出,有大伪。六亲不和有孝慈。国家昏乱有忠臣。\n19 绝圣弃智,民利百倍。绝仁弃义,民复孝慈。绝巧弃利,盗贼无有。此三者以为文不足,故令有所属。见素抱朴,少私寡欲。\n20 绝学无忧,唯之与阿,相去几何?善之为恶,相去若何?人之所畏,不可不畏。荒兮其未央哉!众人熙熙,如享太牢,如登春台。我独泊兮其未兆,如婴儿之未孩,儽儽兮若无所归。众人皆有余,而我独若遗。我愚人之心也哉!沌沌兮。俗人昭昭,我独昏昏,俗人察察,我独闷闷。澹兮其若海,飂兮若无止。众人皆有以,而我独顽且鄙。我独异于人,而贵食母。\n21 孔德之容,惟道是从。道之为物,惟恍惟惚。惚兮恍兮,其中有象;恍兮惚兮,其中有物;窈兮冥兮,其中有精。其精甚真,其中有信。自古及今,其名不去,以阅众甫。吾何以知众甫之状哉?以此。\n22 曲则全,枉则直,洼则盈,敝则新,少则得,多则惑,是以圣人抱一为天下式。不自见故明,不自是故彰,不自伐故有功,不自矜故长。夫唯不争,故天下莫能与之争,古之所谓曲则全者,岂虚言哉!诚全而归之。\n23 希言自然。故飘风不终朝,骤雨不终日。孰为此者?天地。天地尚而不能久,而况于人乎?故从事于道者,道者同于道,德者同于德,失者同于失。同于道者,道亦乐得之。同于德者,德亦乐得之。同于失者,失亦乐得之。信不足焉,有不信焉。\n24 企者不立,跨者不行,自见者不明,自是者不彰,自伐者无功,自矜者不长,其在道也,曰:余食赘行,物或恶之,故有道者不处。\n25 有物混成,先天地生。寂兮!寥兮!独立而不改,周行而不殆,可以为天下母,吾不知其名,字之曰道,强为之名曰大。大曰逝,逝曰远,远曰反。故道大,天大,地大,王亦大。域中有四大,而王居其一焉。人法地,地法天,天法道,道法自然。\n26 重为轻根,静为躁君。是以圣人终日行而不离辎重,虽有荣观,燕出超然。奈何万乘之主,而以身轻天下?轻则失本,躁则失君。\n27 善行无辙迹,善言无瑕谪,善数不用筹策,善闭无关楗而不可开,善结无绳约而不可解。是以圣人常善救人,故无弃人;常善救物,故无弃物。是谓袭明。故善人者,不善人之师;不善人者,善人之资。不贵其师,不爱其资,虽智大迷,是谓要妙。\n28 知其雄,守其雌,为天下溪;为天下溪,常德不离,复归于婴儿。知其白,守其黑,为天下式;为天下式,常德不忒,复归于无极。知其荣,守其辱,为天下谷;为天下谷,常德乃足,复归于朴。朴散则为器,圣人用之,则为官长,故大制不割。\n29 将欲取天下而为之,吾见其不得已。天下神器,不可为也,为者败之,执者失之。故物或行、或随,或歔、或吹,或强、或羸,或挫、或隳。是以圣人去甚、去奢、去泰。\n30 以道佐人主者,不以兵强天下。其事好还,师之所处,荆棘生焉。大军之后,必有凶年。善者果而已,不敢以取强。果而勿矜,果而勿伐,果而勿骄,果而不得已,果而勿强。物壮则老,是谓不道,不道早已。\n31 夫佳兵者,不祥之器,物或恶之,故有道者不处。君子居则贵左,用兵则贵右。兵者不祥之器,非君子之器,不得已而用之。恬淡为上,胜而不美,而美之者,是乐杀人。夫乐杀人者,则不可得志于天下矣。吉事尚左,凶事尚右。偏将军居左,上将军居右,言以丧礼处之。杀人之众,以哀悲泣之,战胜以丧礼处之。\n32 道常无名,朴虽小,天下莫能臣也。侯王若能守之,万物将自宾。天地相合,以降甘露,民莫之令而自均。始制有名,名亦既有,夫亦将知止,知止可以不殆。譬道之在天下,犹川谷之于江海。\n33 知人者智,自知者明。胜人者有力,自胜者强。知足者富,强行者有志。不失其所者久,死而不亡者寿。\n34 大道泛兮,其可左右。万物恃之而生而不辞,功成不名有,衣养万物而不为主。常无欲,可名于小;万物归焉而不为主,可名为大。以其终不自为大,故能成其大。\n35 执大象天下往。往而不害,安平太。乐与饵,过客止。道之出口,淡乎其无味,视之不足见,听之不足闻,用之不足既。\n36 将欲歙之,必固张之。将欲弱之,必固强之。将欲废之,必固兴之。将欲夺之,必固与之。是谓微明。柔弱胜刚强。鱼不可脱于渊,国之利器不可以示人。\n37 道常无为而无不为,侯王若能守之,万物将自化。化而欲作,吾将镇之以无名之朴。无名之朴,夫亦将无欲。不欲以静,天下将自定。\n38 自三十八章开始,是《老子》下篇,又名下经,整个连起来,上经讲道,勉强给它一个范围,是讲“道之体”,讲“道”的根本。下经讲“德”,德是讲用,在古代文学上解释“德”为“得”,好像一个东西得到手里,所以是“德者得也”。现代的名词是说其“成果”“效用”。\n下经开始讲“德”,就是讲道的作用,以及他的现象。下面很多的话,看起来是一样,仔细研究起来,有很大的差别。因为上经讲“体”,下经讲“用”。旧的观念说,上经讲“道”,下经讲“德”。所以,同样德字句,从道的角度看,与德的角度看是不同的。\n上德不德,是以有德,下德不失德,是以无德。上德无为而无以为,下德为之而有以为。上仁为之而无以为,上义为之而有以为。上礼为之而莫之应,则攘臂而扔之。故失道而后德,失德而后仁,失仁而后义,失义而后礼。夫礼者,忠信之薄而乱之首。前识者,道之华而愚之始。是以大丈夫处其厚,不居其薄,处其实,不居其华,故去彼取此。\n39 昔之得一者,天得一以清,地得一以宁,神得一以灵,谷得一以盈,万物得一以生,侯王得一以为天下贞。其致之,天无以清将恐裂,地无以宁将恐发,神无以灵将恐歇,谷无以盈将恐竭,万物无以生将恐灭,侯王无以贵高将恐蹶。故贵以贱为本,高以下为基。是以侯王自谓孤、寡、不谷,此非以贱为本邪?非乎!故致数舆无舆。不欲琭琭如玉,珞珞如石。\n40 反者道之动,弱者道之用。天下万物生于有,有生于无。\n41 上士闻道,勤而行之;中士闻道,若存若亡;下士闻道,大笑之。不笑,不足以为道。故建言有之,明道若昧,进道若退,夷道若颣。上德若谷,大白若辱,广德若不足,建德若偷,质真若渝。大方无隅,大器晚成,大音希声,大象无形。道隐无名,夫唯道,善贷且成。\n42 道生一,一生二,二生三,三生万物。万物负阴而抱阳,冲气以为和。人之所恶,唯孤寡不谷,而王公以为称。故物或损之而益,或益之而损。人之所教,我亦教之。强梁者不得其死,吾将以为教父。\n43 天下之至柔,驰骋天下之至坚。无有入无闲,吾是以知无为之有益。不言之教,无为之益,天下希及之。\n44 名与身孰亲,身与货孰多,得与亡孰病,是故甚爱必大费,多藏必厚亡。知足不辱,知止不殆,可以长久。\n45 大成若缺,其用不弊;大盈若冲,其用不穷。大直若屈,大巧若讷。躁胜寒,静胜热,清静为天下正。\n46 天下有道,却走马以粪;天下无道,戎马生于郊。祸莫大于不知足,咎莫大于欲得。故知足之足,常足矣。\n47 不出户,知天下;不窥牖,见天道。其出弥远,其知弥少。是以圣人不行而知,不见而名,不为而成。\n48 为学日益,为道日损,损之又损,以至于无为,无为而不为。取天下常以无事,及其有事,不足以取天下。\n49 圣人无常心,以百姓心为心。善者吾亦信之,不善者吾亦善之,德善。信者吾信之,不信者吾亦信之,德信。圣人在天下歙歙,为天下浑其心,圣人皆孩之。\n50 出生入死,生之徒十有三,死之徒十有三。人之生,动之死地亦十有三。夫何故,以其生生之厚。\n盖闻善摄生者,陆行不遇兕虎,入军不被甲兵。兕无所投其角,虎无所措其爪,兵无所容其刃。夫何故,以其无死地。\n51 道生之,德畜之,物形之,势成之,是以万物莫不尊道而贵德。道之尊,德之贵,夫莫之命而常自然。故道生之,德畜之,长之育之,亭之毒之,养之覆之。生而不有,为而不恃,长而不宰,是谓元德。\n52 天下有始,以为天下母。既得其母,以知其子;既知其子,复守其母,没身不殆。塞其兑,闭其门,终身不勤;开其兑,济其事,终身不救。见小曰明,守柔曰强。用其光,复归其明,无遗身殃。是谓习常。\n53 使我介然有知,行于大道,惟施是畏。大道甚夷,而民好径。朝甚除,田甚芜,仓甚虚。服文彩,带利剑,厌饮食,财货有余,是谓盗夸,非道也哉。\n54 善建者不拔,善抱者不脱,子孙以祭祀不辍。修之于身,其德乃真;修之于家,其德乃余;修之于乡,其德乃长;修之于国,其德乃丰;修之于天下,其德乃普。故以身观身,以家观家,以乡观乡,以国观国,以天下观天下。吾何以知天下然哉?以此。\n55 含德之厚,比于赤子。蜂虿(chai4)虺(hui3)蛇不螫(zhe1),猛兽不据,攫(jue2)鸟不搏。骨弱筋柔而握固,未知牝牡之合而全(朘juan1)作,精之至也。终日号而不嗄(sha4),和之至也。知和曰常,知常曰明,益生曰祥。心使气曰强,物壮则老,谓之不道,不道早已。\n56 知者不言,言者不知。塞其兑,闭其门,挫其锐,解其分,和其光,同其尘,是谓元同。故不可得而亲,不可得而疏,不可得而利,不可得而害,不可得而贵,不可得而贱,故为天下贵。\n57 以正治国,以奇用兵,以无事取天下。吾何以知其然哉?以此。天下多忌讳,而民弥贫。民多利器,国家滋昏。人多伎巧,奇物滋起。法令滋彰,盗贼多有。故圣人云:我无为而民自化,我好静而民自正,我无事而民自富,我无欲而民自朴。\n58 其政闷闷,其民淳淳;其政察察,其民缺缺。祸兮福之所倚,福兮祸之所伏。孰知其极?其无正。正复为奇,善复为妖,人之迷,其是固久。是以圣人方而不割,廉而不刿,直而不肆,光而不耀。\n59 治人事天,莫若啬。夫唯啬,是谓早服。早服谓之重积德,重积德,则无不克。无不克,则莫知其极。莫知其极,可以有国,有国之母,可以长久。是谓深根固柢,长生久视之道。\n60 治大国若烹小鲜,以道莅天下,其鬼不神;非其鬼不神,其神不伤人;非其神不伤人,圣人亦不伤人。夫两不相伤,故德交归焉。\n61 大国者下流,天下之交,天下之牝。牝常以静胜牡,以静为下。故大国以下小国,则取小国;小国以下大国,则取大国。故或下以取,或下而取。大国不过欲兼畜人,小国不过欲入事人。夫两者各得所欲,大者宜为下。\n62 道者,万物之奥,善人之宝,不善人之所保。美言可以市,尊行可以加人。人之不善,何弃之有。故立天子,置三公,虽有拱璧以先驷马,不如坐进此道。古之所以贵此道者何,不曰以求得,有罪以免邪,故为天下贵。\n63 为无为,事无事,味无味。大小多少,报怨以德。图难于其易,为大于其细。天下难事,必作于易;天下大事,必作于细。是以圣人终不为大,故能成其大。夫轻诺必寡信,多易必多难,是以圣人犹难之,故终无难矣。\n64 其安易持,其未兆易谋。其脆易泮,其微易散。为之于未有,冶之于未乱。合抱之木生于毫末,九层之台起于累土,千里之行始于足下。为者败之,执者失之。是以圣人无为故无败,无执故无失。民之从事,长于几成而败之,慎终如始,则无败事。是以圣人欲不欲,不贵难得之货;学不学,复众人之所过,以辅万物之自然,而不敢为。\n65 古之善为道者,非以明民,将以愚之。民之难治,以其智多。故以智治国,国之贼;不以智治国,国之福。知此两者亦楷式,常知楷式,是谓玄德。玄德深矣,远矣,与物反矣。然后乃至大顺。\n66 江海所以能为百谷王者,以其善下之,故能为百谷王。是以欲上民,必以言下之;欲先民,必以身后之。是以圣人处上而民不重,处前而民不害,是以天下乐推而不厌。以其不争,故天下莫能与之争。\n67 天下皆谓我道大,似不肖。夫唯大,故似不肖。若肖,久矣其细也夫。我有三宝,持而保之:一曰慈,二曰俭,三曰不敢为天下先。慈故能勇,俭固能广,不敢为天下先,死矣。夫慈,以战则胜,以守则固。天将救之,以慈卫之。\n68 善为士者不武,善战者不怒,善胜敌者不与,善用人者为之下。是谓不争之德,是谓用人之力,是谓配天,古之极。\n69 用兵有言:“吾不敢为主而为客,不敢进寸而退尺。”是谓行无行,攘无臂,执无兵,扔无敌。祸莫大于轻敌,轻敌几丧吾宝。故抗兵相加,哀者胜矣。\n70 吾言甚易知,甚易行。天下莫能知,莫能行。言有宗,事有君。夫唯无知,是以不我知。知我者希,则我者贵,是以圣人被褐怀玉。\n71 知不知上,不知知病。夫唯病病,是以不病。圣人不病,以其病病,是以不病。\n72 民不畏威,则大威至。无狎其所居,无厌其所生。夫唯不厌,是以不厌。是以圣人自知不自见,自爱不自贵,故去彼取此。\n73 勇于敢则杀,勇于不敢则活,此两者或利或害。天之所恶,孰知其故,是以圣人犹难之。天之道,不争而善胜,不言而善应,不召而自来,繟然而善谋。天网恢恢,疏而不失。\n74 民不畏死,奈何以死惧之。若使民常畏死,而为奇者,吾得执而杀之,孰敢。常有司杀者杀,夫代司杀者杀,是谓代大匠斫,夫代大匠斫者,希有不伤其手矣。\n75 民之饥,以其上食税之多,是以饥。民之难治,以其上之有为,是以难治。民之轻死,以其上求生之厚,是以轻死。夫唯无以生为者,是贤于贵生。\n76 人之生也柔弱,其死也坚强。万物草木之生也柔脆,其死也枯槁。故坚强者死之徒,柔弱者生之徒。是以兵强则不胜,木强则兵。强大处下,柔弱处上。\n77 天之道,其犹张弓与!高者抑之,下者举之;有余者损之,不足者补之。天之道,损有余而补不足;人之道则不然,损不足以奉有余。孰能有余以奉天下?唯有道者。是以圣人为而不恃,功成而不处,其不欲见贤。\n78 天下莫柔弱于水,而攻坚强者莫之能胜,以其无以易之。弱之胜强,柔之胜刚,天下莫不知,莫能行。是以圣人云:受国之垢,是谓社稷主;受国不祥,是为天下王。正言若反。\n79 和大怨,必有余怨,安可以为善?是以圣人执左契,而不责于人。有德司契,无德司彻。天道无亲,常与善人。\n80 小国寡民,使有什伯之器而不用,使民重死而不远徙。虽有舟舆,无所乘之;虽有甲兵,无所陈之;使民复结绳而用之。甘其食,美其服,安其居,乐其俗。邻国相望,鸡犬之声相闻,民至死不相往来。\n81 信言不美,美言不信。善者不辩,辩者不善。知者不博,博者不知。圣人不积,既以为人已愈有,既以与的已愈多。天之道,利而不害。圣人之道,为而不争。\n","date":"2020-03-17","permalink":"https://loveminimal.github.io/posts/lao-zi/","summary":"\u003cblockquote\u003e\n\u003cp\u003e道法自然!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"道德经"},{"content":"\r🔔 http://www.ruanyifeng.com/blog/2019/10/tmux.html\nwindows 终于有了个 windows-terminal 吧,配置文件还是个 txt .... 哎,微软啊,你妹。\rlayouts 布局、window 窗口、pane 窗格 布局 层级 描述 窗口 命令 描述 窗格 命令 描述 * server 服务 * c 新建窗口 * % 水平分屏 * session 会话 * \u0026amp; 关闭窗口 * \u0026quot; 垂直分屏 * window 窗口 * l 切换窗口 * x 关闭窗格 * pane 窗格 * n 切换到下一个窗口 * ; 切换窗格 * * p 切换到上一个窗口 * o 顺时针切换窗格 * * w 窗口的菜单列表 * c-o 逆时针转换窗格 * * * m-o 顺时针转换窗格 配置 r1r2 新建 ~/.tmux.conf 文件,并写入:\n# set new default prefix # 修改主键 unbind c-b set-option -g prefix c-j # mouse # 激活鼠标模式 set-option -g -q mouse on # easy split pane commands # 修改分屏按键 bind h split-window -h bind v split-window -v unbind \u0026#39;\u0026#34;\u0026#39; unbind % ","date":"2020-02-26","permalink":"https://loveminimal.github.io/posts/tmux/","summary":"\u003cimg src=\"https://www.wangbase.com/blogimg/asset/201910/bg2019102005.png\" width=\"300\" /\u003e\r\n\u003cp\u003e🔔 \u003ca href=\"%E5%8F%8B%E6%83%85%E9%93%BE%E6%8E%A5\"\u003ehttp://www.ruanyifeng.com/blog/2019/10/tmux.html\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"oh-essay\"\u003e\r\nWindows 终于有了个 Windows-Terminal 吧,配置文件还是个 txt .... 哎,微软啊,你妹。\r\n\u003c/div\u003e","title":"十分钟掌握 tmux"},{"content":" wiki 相关课程资料 📌 https://blog.csdn.net/sirobot/article/details/122917866\n北京大学课程资料整理:\nhttps://github.com/lib-pku/libpku\n浙江大学课程攻略共享计划:\nhttps://github.com/qsctech/zju-icicles\n清华大学计算机系课程攻略:\nhttps://github.com/pkuanonym/rekcarc-tsc-uht\n中国科学技术大学课程资源:\nhttps://github.com/ustc-resource/ustc-course\n上海交通大学课程分享:\nhttps://github.com/coolphilchen/sjtu-courses/\nwin10 如何使用输入法快速插入当前时间 参考 windows10 如何使用微软输入法快速输入时间(yyyy-mm-dd hh:mm:ss格式)\n右键微软输入法托盘图标,选择 用户自定义短语 选项,如下:\n添加用户自定义的短语:\n短语处,完整内容如下:\n%yyyy%-%mm%-%dd% %hh%:%mm%:%ss% 添加后,切换一下输入法,输入 sj ,默认输入法首选项就是当前时间了。\n我的主机上设置的快捷键是 iid ,短语内容为 %yyyy%-%mm%-%dd% %hh%:%mm% ,如下:\n总之就是挺爽的吧!当然,你也可以设置其他自定义输入,而且可以导出完整的自定义选项,更换主机后,也很容易启用。\n= 事实证明,这种动态生成时间的方式,在你更换主机后,只能再次手动地去创建。\n其他输入法应该也有类似的定义办法,很不错。\nvscode 折叠策略 vscode 默认的折叠策略(wrapping strategy)为 simple ,如下:\n偶尔更换了下 editor 的字体,以同一编辑器字体和网页字体,出现了自动换行不能正确截断的问题。如图所示,总有一部分会显示不完全。\n经过测试,只需要把折叠策略,修改为 advanced 即可。\n= 不得不说,vscode 是真的好用哦,处理文本真的很方便。*\ndns 设置 公司 dns 备注 安印 192.168.51.168 中国互联网络信息中心 1.2.4.8 210.2.4.8 百度 180.76.76.76 阿里 223.5.5.5 223.6.6.6 腾讯 119.29.29.29 win10 设置右下角显示秒钟 打开‘控制面板’,在地址栏输入 控制面板、时钟和区域 ,如下:\n=其他设置 后 =时间 选项卡,如下:\n打开‘运行’(win-r),输入 regedit ,打开‘注册表编辑器’后,在地址栏输入 计算机、hkey_current_user\\software\\microsoft\\windows\\currentversion\\explorer\\advanced 后,右键 advanced ,新建一个‘dword(32 位)’值,命名为 showsecondsinsystemclock ,并赋值为 1 ,然后重启或注销电脑即可。\n关于 iframe 好吧,老掉牙的技术了,当前许多浏览器已经限制了该功能,了解可能参考:\nhttps://www.cnblogs.com/bester-ace/articles/9292779.html https://www.cnblogs.com/hq233/p/9849939.html 奶头乐理论 1 刺激他们的欲望,降低他们的工资,借钱给他们花,让他们忙的停不下来,同时开放大量的娱乐项目,使他们又不至于崩溃。\n当娱乐大量占用人们的时间,让人们丧失思考的能力,这一社会麻醉剂将会带来“马太效应”,沉迷的人继续沉迷,清醒的人保持清醒,人与人的差距,甚至阶层间的差距也就拉大了。\n从而消磨他们的斗志,抹平阶级跃迁的愿望。\nv30 pro 您可以在华为官网查询您设备的相关备件价格。点击后方的链接选择您设备型号和颜色即可,点击链接 https://consumer.huawei.com/cn/support/sparepart-price/\n亲亲,为您提供一个便捷的查询服务店方法,点击下方官网链接您可直接查询服务店具体信息,您可根据自己的实际情况选择距离您比较近的服务店前往哦~查询链接(建议收藏哦~): https://consumer.huawei.com/cn/support/service-center/\n华为授权服务中心(濮阳县红旗路)\r4.96 分 5.6km\r屏幕外层玻璃维修\r电脑维修\r服务日\r河南省-濮阳市-濮阳县红旗路与建新路交叉口向东 100 米路南\r0393-3339678\r营业时间:9:00-18:00(周一周三至周日)\r特别提醒: 门店每周二休息,营业时间为每周一、周三-周日 09:00-18:00,此期间可通过华为服务 app 选择就近授权服务门店或官方寄修服务,如需帮助请拨打电话 16639328222;针对屏幕外层玻璃维修,本店支持直面手机屏幕外层玻璃维修;针对笔记本业务本店仅提供检测、软件问题处理服务,暂不提供硬件维修服务,给您带来不便敬请谅解 git 解决每次拉取、提交代码时都需要输入用户名和密码 在家目录运行 git config --global credential.helper store ,然后在拉取时输入正确的用户名和密码,就可以成功记录下来。\n解决 git bash 不能登录 mysql4 在使用 git bash 登录 mysql 时会卡死,怎么办呢?加上 winpty 就好了,如下:\nwinpty mysql -u root -p 厚墨书源 = 这个软件算是彻底废了…… 挺好用的,可惜喽~\n下面每一条链接都是一个书源,一个一个复制到厚墨里用网络导入,粘贴版是导入单个书源的,无法导入链接\n- https://cdn.jsdelivr.net/gh/chuner821/deepink/repository.json\r- https://cdn.jsdelivr.net/gh/wenmoux/sources@latest/wen.json\r- https://cdn.jsdelivr.net/gh/jiyiv5/xhm_sy/repository.json\r- https://cdn.jsdelivr.net/gh/jackiu1997/jackiudeepinksource/repository.json\r- https://cdn.jsdelivr.net/gh/xd-maxsouls/deepink/repository.json\r- https://cdn.jsdelivr.net/gh/mellovo/novel-booksource@vv2/repository.json\r- https://cdn.jsdelivr.net/gh/kaitl/booksource/repository.json\r- https://cdn.jsdelivr.net/gh/17303772831/booksourcerepository/repository.json\r- https://cdn.jsdelivr.net/gh/qingy1/deeplink-booksource/qingy1.json\r- https://cdn.jsdelivr.net/gh/258a/akoasm/repository.json\r- https://gitee.com/diskrubbish/diskrubbish_deepink_sources/raw/master/repository.json element-ui 单元格点击事件,行点击+单元格点击,获取某一行的 index \u0026lt;el-table :data=\u0026#34;tabledata\u0026#34; stripe @cell-click=\u0026#34;addsubaccount\u0026#34; :row-class-name=\u0026#34;tablerowclassname\u0026#34;\u0026gt; \u0026lt;el-table-column prop=\u0026#34;installer\u0026#34; label=\u0026#34;主子账号\u0026#34;\u0026gt; \u0026lt;/el-table-column\u0026gt; \u0026lt;/el-table\u0026gt; 相应逻辑:\n//下面是利用给表格添加 classname, 添加 index tablerowclassname ({row, rowindex}) { //把每一行的索引放进 row // console.log(row,rowindex) row.index = rowindex; //拿到的索引赋值给 row 的 index, 在这个表格中能拿到 row 的里面都会包含 index return \u0026#39;row-remarks\u0026#39; //classname(类名) }, addsubaccount(row){ //获取焦点弹出关联多个子账号 console.log(row.index) }, //如果需要区分那一数列的才能触发需要判断下 prop 的值 addsubaccount(row,column){ //获取焦点弹出关联多个子账号 console.log(row.index) //获取下标 console.log(column.property ) //获取判断条件 if(column.property == \u0026#39;prop 的值\u0026#39;){ //prop 的值是自己设置的,注意别重复设置同一个值 } }, //如果表格既有行点击,又有单元格点击,在行点击事件里判断 prop, 等不等于你行点击的 prop, 如果等于直接 return false 跳出 vue 文件的命名规范 → 详见 https://juejin.cn/post/6844903840626507784#heading-9\n#+begin_quote 其实刚开始我写 vue 文件的时候也不注意,各种驼峰啊、大写开头 (pascalcase) 还是横线连接 (kebab-case) 混着来,谁叫 vue 都可以,在 风格指南 中也没有定论。不过基于本项目我还是整理了一套文件的命名规则。 #+end_quote\n1.components\n所有的 component 文件都是以大写开关(pascalcase),这也是官方所推荐的,但除了 index.vue ,如:\n@/src/components/backtotop/index.vue @/src/components/charts/line.vue @/src/views/example/components/button.vue 2.js 文件\n所有的 .js 文件都遵循横线连接 (kebab-case),如:\n@/src/utils/open-window.js @/src/views/svg-icons/require-icons.js @/src/components/markdowneditor/default-options.js 3.views\n在 views 文件下,代表路由的 .vue 文件都使用横线连接 (kebab-case),代表路由的文件夹也是使用同样的规则,如:\n@/src/views/svg-icons/index.vue @/src/views/svg-icons/require-icons.js 使用横线连接 (kebab-case) 来命名 views 主要是出于以下几个考虑。\n横线连接 (kebab-case) 也是官方推荐的命名规范之一; views 下的。vue 文件代表的是一个路由,所以它需要和 component 进行区分 (component 都是大写开头); 页面的 url 也都是横线连接的,比如 https://www.xxx.admin/export-excel ,所以路由对应的 view 应该要保持统一 ; 没有大小写敏感问题。 设置 git-bash 为 emacs 默认 shell → https://blog.csdn.net/csfreebird/article/details/9719221\n(setq explicit-shell-file-name \u0026#34;c:/program files/git/bin/bash.exe\u0026#34;) (setq shell-file-name explicit-shell-file-name) (add-to-list \u0026#39;exec-path \u0026#34;c:/program files/git/bin\u0026#34;) ide active → jetbrains 系列产品重置试用方法\nhttps://plugins.zhile.io\nhttps://gitee.com/loveminimal/ide.git\nubuntu 下清空 dns 缓存 为什么要清空 dns 缓存呢?\n大多数的 dns 客户端会把域名解析的结果缓存到本地,这样可以提升对于同一个地址的访问速度。当您打开一个单页面的时候,通常会有多次对同一个域名的访问请求。基本上每个文件、图片、样式表……这些都是在同一个页面内部的对同一个域名的 dns 解析请求。\n所以如果您已经在本地缓存了不正确的 dns 条目,那么您需要清空您的缓存来使 dns 客户端提出新的 dns 请求并更新解析结果。当然,您也可以等缓存的 dns 条目过期以后让系统自动冲掉该条目……这通常需要 24 个小时。\n在 ubuntu 中冲掉 dns 缓存的方式是重新启动 nscd 守护程序\n# 安装 nscd (如果没有) sudo aptitude install nscd # 清除 dns sudo /etc/init.d/nscd restart github 图片不加载问题 修改 c:\\windows\\system32\\drivers\\etc\\hosts 文件,尾部添加如下内容:\n# github start\r192.30.253.112 build software better, together\r192.30.253.119 gist.github.com\r151.101.184.133 assets-cdn.github.com\r151.101.184.133 raw.githubusercontent.com\r151.101.184.133 gist.githubusercontent.com\r151.101.184.133 cloud.githubusercontent.com\r151.101.184.133 camo.githubusercontent.com\r151.101.184.133\tavatars.githubusercontent.com\r# github end vue/cli 3 切换为 dart-sass 首先,安装包 :\nnpm i sass sass-loader -d # or yarn add sass sass-loader -de 然后,修改 vue.config.js ,增加如下配置:\nmodule.exports = { css: { loaderoptions: { sass: { implementation: require(\u0026#39;sass\u0026#39;), // this line must in sass option }, } //... }; glob glob 最早是出现在 unix 系统的命令中,是用来匹配文件路径的。除了在命令行中,我们在程序中也会有匹配文件路径的需求。于是,很多编程语言有了对 glob 的实现,如 python 中的 glob 模块,php 中的 glob 方法。\n下面是 node-glob 的匹配规则:\nglob desc * 匹配任意 0 或多个字符 ? 匹配任意一个字符 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- [...] 匹配中括号中的字符 ![...] 匹配不在中括号中的字符 ^[...] 匹配不在中括号中的字符 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- !(pattern i pattern i pattern) 不满足括号的所有模式 ?(pattern i pattern i pattern) 满足 0 或 1 个括号中的模式 +(pattern i pattern i pattern) 满足 1 或多个括号中的模式 @(pattern i pat* i pat?ern) 满足 1 个括号中的模式 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- ** 跨路径匹配任意字符 来看几个最简单常用的例子吧。\n它将匹配类似 scripts/index.js、scripts/nested/index.js 和 scripts/nested/twice/index.js 的文件。\n\u0026#39;scripts/**/*.js\u0026#39; 取反。\n\u0026#39;script/**/*.js\u0026#39;, \u0026#39;!scripts/vendor/\u0026#39;, \u0026#39;scripts/vendor/react.js\u0026#39;\r\u0026#39;**/*.js\u0026#39;, \u0026#39;!node_modules/\u0026#39; 设计模式准则 我以前给一些公司讲一些设计模式的培训课,我一再提到,那 23 个经典的设计模式和 oo 半毛钱关系没有,只不过人家用 oo 来实现罢了。\n设计模式就三个准则:\n中意于组合而不是继承, 依赖于接口而不是实现, 高内聚,低耦合。 你看,这完全就是 unix 的设计准则。\n文件的描述符和重定向 文件描述符 是和文件的输入、输出相关联的非负整数,linux 内核(kernel)利用文件描述符来访问文件。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。\n系统预留文件描述符:\n0 - =stdin= 标准输入; 1 - =stdout= 标准输出; 2 - =stderr= 标准错误。 实例:\n# 输出重定向 # 1. 截取模式保存到文件 - 写入到文件之前,文件内容首先会被清空 echo \u0026#34;this is a text line one\u0026#34; \u0026gt; test.txt # 2. 追加模式保存到文件 - 写入到文件之后,会追加到文件结尾 echo \u0026#34;this is a text line one\u0026#34; \u0026gt;\u0026gt; test.txt # 标准错误输出的重定向方法 cat linuxde.net # → cat: linuxde.net: no such file or directory # 1. 没有任何错误提示,正常运行 cat linuxde.net 2\u0026gt; out.txt # 2. 错误信息被保存到了 out.txt 文件中 cat linuxde.net \u0026amp;\u0026gt; out.txt # 3. 将错误输出丢弃到 /dev/null 中,特殊的设备文件 - 黑洞 cat linuxde.net 2\u0026gt; /dev/null # 输入重定向 echo \u0026lt; test.txt 数据归档和解压缩 首先要弄清两个概念:打包和压缩。 打包 是指将一大堆文件或目录变成一个总的文件; 压缩 则是将一个大的文件通过一些压缩算法变成一个小文件。\n1. tar 命令\n利用 tar 命令,可以把一大堆的文件和目录全部打包成一个文件,这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。\n# 语法\rtar (选项) (参数) 实例:\n# 打包、压缩 tar -cvf log.tar log2012.log # 仅打包,不压缩! tar -zcvf log.tar.gz log2012.log # 打包后,以 gzip 压缩 tar -jcvf log.tar.bz2 log2012.log # 打包后,以 bzip2 压缩 # 查询 tar -tvf log.tar # 直接查询 tar -ztvf log.tar.gz # 查询以 gzip 压缩的文件 tar -jtvf log.tar.bz2 # 查询以 bzip2 压缩的文件 # 解压缩 tar -zxvf log.tar.gz # 以 gzip 解压缩 tar -jxvf log.tar.bz2 # 以 bzip2 解压缩 tar -zxvf log.tar.gz -c log # 以 gzip 解压缩在目录 log 其中:\n选项 说明 -v 显示操作过程 -f \u0026lt;file\u0026gt; --file=\u0026lt;fiel\u0026gt; 指定备份文件 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; -c --create 建立新的备份文件 -t --list 列出备份文件的内容 -x --extract --get 从备份文件中还原文件 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; -z --gzip --ungzip 通过 gzip 指令处理备份文件 -j 支持 bzip2 解压文件 -c \u0026lt;dir\u0026gt; 在特定目录解压缩 小结:\n压 缩:tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称\r查 询:tar -jtv -f filename.tar.bz2\r解压缩:tar -jxv -f filename.tar.bz2 -c 欲解压缩的目录 关于 css 中设置 height 为 100% 不起作用 w3c 的规范,百分比的高度在设定时需要根据这个元素的父元素的高度。\nweb 浏览器有计算有效宽度时会考虑浏览器窗口的打开宽度,缼省为页面整个横向宽度。\n事实上,浏览器根本就不计算内容的高度,除非内容超出了视窗范围(导致滚动条出现),缺省为 height: auto 。或者你给整个页面设置一个绝对高度,否则浏览器就会简单的让内容往下堆砌,页面的高度根本就无需考虑。\n!!!个人实践,在给 html、body 设置 background 相关属性的时候情况会很奇特,给其内的元素设置背景的时候就不存在这些奇葩问题。\n所以,当我们想要设置竖直高度的百分比,需要对 html、body 进行一些初始化设置,如:\nhtml { /* 设置根元素高度 */ height: 100%; } body { /* 设置 body 高度 */ height: 100%; } /* body 内元素 .container */ .container { height: 60%; background: #f66; } 其实试一下,你就会发现,如果直接对 body 设置 background 的背景图片或是背景色,都会占满整个容口。\njavascript 对 url 的编码和解码 有时候,你会发现一些 url 链接是编码过的,如这样: http%3a%2f%2fw3cschool.cn%2fmy%20test.asp%3fname%3dst%c3%a5le%26car%3dsaab 。\njavascript 中使用 encodeuricomponent() 方法可以对 uri 进行编码;使用 decodeuricomponent() 方法可以对 uri 进行解码。\nw3c 提供了简单的实现,如下:\nvar uri=\u0026#34;http://w3cschool.cn/my test.php?name=ståle\u0026amp;car=saab\u0026#34;; var uri_encode=encodeuricomponent(uri); document.write(uri_encode); document.write(\u0026#34;\u0026lt;br\u0026gt;\u0026#34;); document.write(decodeuricomponent(uri_encode)); ↓↓↓\nhttp%3a%2f%2fw3cschool.cc%2fmy%20test.php%3fname%3dst%c3%a5le%26car%3dsaab\rhttp://w3cschools.com/my test.asp?name=ståle\u0026amp;car=saab emacs 宏操作 https://www.jianshu.com/p/6ad946eb8ebc\nkey/command description c-x ( 开启宏记录 c-x ) 关闭宏记录 c-x e 执行刚录制的宏 c-u n c-x e 执行 n 次刚录制的宏 m-x name-last-kbd-marco 给刚记录的宏命名 m-x insert-kbd-marco 把刚命名的宏记录写入到文件中 可以设置一个专门的文件(如 ~/.emacs.d/macro.el )来记录宏,然后在 init.el 中加载改文件( (load-file \u0026quot;~/.emacs.d/macro.el\u0026quot;) ), 如此便可以实现持久化。\n如这个例子:用宏定义了下翻 15 行和上翻 15 行的快捷键。\n;; macro.el (fset \u0026#39;next-lines \u0026#34;\\c-u15\\c-n\u0026#34;) (fset \u0026#39;previous-lines \u0026#34;\\c-u15\\c-p\u0026#34;) ;; init.el ;; ... ;; 加载 macro.el (load-file \u0026#34;~/.emacs.d/macro.el\u0026#34;) ;; 绑定快捷键 (global-set-key (kbd \u0026#34;c-x n ret\u0026#34;) \u0026#39;next-lines) (global-set-key (kbd \u0026#34;c-x p ret\u0026#34;) \u0026#39;previous-lines) 如何设置终端 256 色 https://stackoverflow.com/questions/63950/how-to-make-emacs-terminal-colors-the-same-as-emacs-gui-colors?r=searchresults\n设置 term 在 .bashrc 文件中,如下:\nexport term=xterm-256color 如此,便设置好了。\n加入我们使用在终端中使用 emacs ,执行 m-x list-colors-display ,便可以看到 256 色已经全部激活,如此,终端下使用 emacs 和 emacs gui 的颜色便相差无几了。\ninput 中 placeholder、disabled 状态样式修改 问题场景:\n有时按业务需求更改 input 中 placeholder 样式和 disabled 状态下的样式; ios 和安卓移动端样式兼容性问题,样式不一致。 处理如下:\ninput::-webkit-input-placeholder { color: #ccc; -webkit-text-fill-color: #ccc; opacity: 1; -webkit-opacity: 1; } input:disabled { background: none; color: #333; -webkit-text-fill-color: #333; opacity: 1; -webkit-opacity: 1; } input:disabled::-webkit-input-placeholder { color: #ccc; -webkit-text-fill-color: #ccc; opacity: 1; -webkit-opacity: 1; } 相关延伸:\n::-webkit-input-placeholder {} 使用 webkit 内核的浏览器 :moz-placeholder {} firefox 版本 4-18 ::moz-placeholder {} firefox 版本 19+ -ms-input-placeholder {} ie 浏览器 css 换行 [[https://www.cnblogs.com/nangezi/p/9230062.html][→ 参考链接]]\n文本换行有很多方式:\n\u0026lt;br/\u0026gt; 标签元素,能够强制使得所在位置文本换行; \u0026lt;p\u0026gt; 元素, \u0026lt;div\u0026gt; 设定宽度,都可以对文本内容实现自适应换行; 对于长单词或链接,默认不会断开换行,方式 2 就不能够在这些文本内部进行换行,此时需要 word-wrap: break-word; 或 word-break: break-all; 实现强制断行。 1. 强制不换行\ndiv { white-space: nowrap; } /* white-space: - normal 默认 - pre 换行和其他空白字符都将受到保护 - nowrap 强制在同一行内显示所有文本,直到文本结束或者遭遇 \u0026lt;br\u0026gt; 对象 ,*/ 2. 控制文本换行\ndiv { word-break: normal; word-break: break-all; word-break: keep-all; } /* word-break: - normal 依据亚洲语言与非亚洲语言的文本规则,允许在字内换行 - break-all 该行为与亚洲语言的 normal 相同,也允许非亚洲语言文本行的任意字内断开,该值适合包含一些非亚洲文本的亚洲文本 - keep-all 与所有非亚洲语言的 normal 相同,对于中文、韩文、日文,不允许字断开,适合包含少量亚洲文本的非亚洲文本 ,*/ 3. 强制单词内或链接内断行\ndiv { word-wrap: break-word; } /* word-wrap: 属性用来表明是否允许浏览器在长单词和链接内进行断句 - normal 只在允许的断字点换行 - break-word 在长单词或 url 地址内部进行换行 ,*/ js 获取 dpi //获取 dpi function js_getdpi() { var arrdpi = new array(); if ( window.screen.devicexdpi != undefined ) { arrdpi[0] = window.screen.devicexdpi; arrdpi[1] = window.screen.deviceydpi; } else { var tmpnode = document.createelement( \u0026#34;div\u0026#34; ); tmpnode.style.csstext = \u0026#34;width:1in;height:1in;position:absolute;left:0px;top:0px;z-index:99;visibility:hidden\u0026#34;; document.body.appendchild( tmpnode ); arrdpi[0] = parseint( tmpnode.offsetwidth ); arrdpi[1] = parseint( tmpnode.offsetheight ); tmpnode.parentnode.removechild( tmpnode ); } return arrdpi; } // 将 px 转成 mm let mm = pxvalue/dpi*2.54*10; // dpi 是上面获取的,注意对应 xy 轴 小程序跳转 h5 时 url 参数截断 → 参考链接\n先来看一个例子,原来的 url 为 https://ultimavip.cn/m/mposter.html?source=gxw_001_t_mposter ,跳转后变为 https://ultimavip.cn/m/mposter.html ,参数 ?source=gxw_001_t_mposter 丢失了,为什么呢?编码问题。\n// 跳转到 h5 页面的小程序代码 targeturl: function() { console.log(this.data.mod_texturl); wx.navigateto({ url: \u0026#39;../webview/webview?url=\u0026#39; + encodeuricomponent(this.data.mod_texturl) // 此处需要编码,因为有 \u0026#39;?\u0026#39; ,可能浏览器不认 }) } // 跳转到的 h5 页面进行解码 onload: function(options) { this.setdata({ targeturl: decodeuricomponent(options.url); // 用 decodeuricomponent 进行解码 }) console.log(options.url); } 滚动懒加载的实现 → 参考链接\n什么时候需要懒加载呢?数据量大,一页显示不完,网页渲染事件长,影响体验。如何解决?分页,或数据懒加载。\n先设定了基础前提,假设视窗可以显示 30 数据,总共有 56 条数据要展示。\n如何实现数据懒加载呢?先来看三个属性:\nscrollheight ,元素总高度,包含滚动条中的内容,只读; scrolltop ,当元素出现滚动条时,向下拖动滚动条时,内容向上滚动的距离,可读写; clientheight ,元素内容及其边框所占的空间大小,即可视区域大小高度。 如何判断滚动条到底部了呢?很显然,当 scrollheight - scrolltop - clientheight = 0 时,滚动条就到底部了。\n来看代码,在第一次请求数据的时候,先设置一个变量来记录请求次数(其实后台也是做分页的处理):\n// 初始化首页页码 let currentpage = 1; // this.currentpage = 1 // 获取首页数据,apigettabledata 为定义的获取数据的接口 // data 为请求参数 this.apigettabledata(data).then(res =\u0026gt; { $this.totalpage = res.totalpage; // 这里需要知道总页数 $this.tabledata = res.data; // 表格数据 }) 监听表格 dom 对象的滚动事件:\nlet dom = document.queryselector(targetdom); dom.addeventlistener(\u0026#39;scroll\u0026#39;, function() { let scrolldistance = dom.scrollheight - dom.scrolltop - dom.clientheight; if(scrolldistance \u0026lt;= 0) { // 为 0 证明滚动条已经到底,可以请求接口 if(this.currentpage \u0026lt; this.totalpage) { // 当前页数小于总页数继续请求 this.currentpage++; // 当前页数自增 // 请求接口代码 // data 为请求参数 this.apigettabledata(data).then(res =\u0026gt; { this.tabledata = $this.tabledata.concat(res.data); // 将请求回来的数据和当前展示的数据合并 }) } } }) 如此,就实现表格滚动下拉时的数据懒加载。\n刷新 dns windows 下 刷新 dns 的方法:打开 cmd → 输入 ipconfig /flushdns 。 github 有时候,连接很慢,甚至有打不开的状况,此时,可以尝试刷新一下 dns ,会有意象不到的效果哦。\n","date":"2020-01-28","permalink":"https://loveminimal.github.io/posts/wiki/","summary":"\u003cimg alt=\"picture 8\" src=\"/posts/wiki/imgs/1912db1b19fa392956a9d4c48a0c6354b8549ccf469303b891ec1a2a462d8fc2.png\" /\u003e","title":"wiki"},{"content":"🔔 文中内容,多数摘录自南怀瑾先生的《易经杂说》。\n书不尽言,言不尽意。自觉圣智,完成人格。 \u0026ndash; 南怀瑾\n洁净精微 “洁净精微,易教也。”\n“洁净”包括了宗教的、哲学的含义,也就是说学了《易经》,他的心理、思想、情绪无论在任何情况下,都会非常宁静,澄洁。“精微”两字则是科学的,是无比的细密精确,所以学《易》的人,要头脑非常冷静。\n三易 何为三易?《连山易》、《归藏易》和《周易》。\n《连山易》是神农时代的《易》,所画八卦的先后位置,和《周易》的八卦位置是不一样的。黄帝时代的《易》为《归藏易》。《连山易》以艮卦开始,《归藏易》以坤卦开始,到了《周易》则以乾卦开始,这是三易的不同之处。\n《易经》的三原则 《易经》三大原则:变易、简易和不易。\n变易 第一,所谓变易,是《易经》告诉我们,世界上的事,世界上的人,乃至宇宙万物,没有一样东西是不变的。在时、空当中,没有一事、没有一物、没有一情况、没有一思想是不变的,不可能不变,一定要变的。时间不同,环境不同,情感亦不同,万事万物,随时随地,都在变中,非变不可,没有不变的事物。\n印度佛学中的“无常”,是一种佛理,意思是世界上没有一种东西能永恒存在的,和《易经》中的变易有异曲同工之妙。中华文化中的《易经》,是讲原则,宇宙中的万事万物,没有不变的,非变不可,这是原则。印度人则是就现象而言,见现象有感,遂而名之为“无常”。\n简易 第二简易,也是最高的原则,宇宙间无论如何奥秘的事物,当我们的智慧够了,了解它以后,就变得很平常,很平凡而且非常简单。这就是把复杂的道理,予以简化,所以叫作简易。那么,《易经》首先告诉我们宇宙间的事物无时不变,尽管变得法则及其复杂,宇宙万事万物再错综复杂得现象,在我们懂了原理、原则以后,就非常简单了。\n不易 第三不易,万事万物随时随地都在变得,可是却有一项永远不变的东西存在,就是能变出来万象的那个东西却是不变的,那是永恒存在的。它能变万有、万物、万事,永远不变。\n理、象、数 《易经》的三个法则(内涵):理、象、数。\n理是属于哲学的,宇宙间万事万物既都有它的理,也必有它的象;反过来说,宇宙间任何一个现象,也一定有它的理,同时每个现象,又一定有它的数。万事万物都有它的理、它的象和它的数。《易经》的每一卦、每一爻、每一点,都包含有理、象、数三种涵义在内。人处在世界上,与这个世界的关系,不停地在变,只要发生了变,便包含了它的理、象、数。人的智慧如果懂了事物的理、象、数,就会知道这事物的变,每个现象,到了一定的数,一定会变,为什么会变,有它的道理,完全明白了这些,就万事通达了。理、象、数通了,就能知变、通、达,万事前知了。\n卦与八卦 什么叫作卦?卦就是挂起来的现象,八卦就是告诉我们宇宙间有八个东西,这个东西的现象挂出来,就是八卦。\n卦 ☰ 乾 天 ☷ 坤 地 ☲ 离 日、火 ☵ 坎 月、水 ☳ 震 雷 ☴ 巽 风 ☶ 艮 山 ☱ 兑 泽 乾为天,坤为地,☰ 、☷ 这两个符号代表了时间、空间、宇宙。天地以内,日月旋转,遂生雷风。雷风相薄,雷动为风,风卷成雷。山泽。\n在宇宙间,除了这八个大现象以外,没有九个,已不能七个,只有八个卦,而且都是对立的。\n先天八卦 天地定位。山泽通气。雷风相薄。水火不相射。八卦相错。 \u0026ndash; 《说卦传》\n什么叫 “先天” ?以哲学的观点说,宇宙万物没有形成以前,即是所谓的先天,有了宇宙万物,那就是 “后天” 了。无为先天,有为后天。先天、后天,只是一种代号的作用,以逻辑来说,这只是一种界说,用以划分出阶段范围而已。\n字是写的,卦是画的,所以我们叫作画卦。卦的图案,每个卦都有三画,我们成为三卦画,卦中的画叫 “爻” 。爻者,交也。为什么“爻”就是交?这是说明卦在告诉我们,宇宙间万事万物,时时都在交流,不停地发生关系,引起变化,所以叫作“爻”。\n大象无形,不能拘泥。\n乾、坤、离、坎四个大卦,挂在那里大家都看得见的,就是天、地、日、月四个大象。\n天上云动,以为 ☳ ,震为雷。天下云卷,以为 ☴ ,巽为风。雷电震动以后,阳变为阴,阴变成阳,就变成了巽,气流摩擦,又发生雷电,这两个不断地互相转化。这两个卦的位置相对,名为“对宫卦”。古人说“宫”,就是位置、方位。同样,艮 ☶ 的对宫卦兑 ☱ 。\n其次要注意的,是先天八卦图的“数”,乃依据八卦排列的秩序产生的。“数”在《易经》里是很奇妙的,人们在遇到不如意的事之后,往往认为这些事的发生,是有定数的。\n乾一 → 兑二 → 离三 → 震四,此为 顺 。巽五 → 坎六 → 艮七 → 八坤,必为 逆 。九在中央。\n兑二 乾一 巽五 离三 九中 坎六 震四 坤八 艮七 乾三连,坤六断,震仰盂,艮覆碗,离中虚,坎中满,兑上缺,巽下断。\n这八个大卦,是古人告诉我们,天地间就是这八大现象在变化,这些图案都是相对的。\n后天八卦 假使学《易经》学到需要在某一方面应用,而且用得有功效,就要特别研究后天八卦了。“先天八卦”等于是表明宇宙形成的那个大现象,“后天八卦”是说明宇宙以内的变化和运用的法则。\n一数坎兮二数坤,三震四巽数中分,五寄中宫六乾是,七兑八艮九离门。\n巽四 离九 坤二 震三 五中 兑七 艮八 坎一 乾六 监本《易经》 《易经》为什么不容易看懂?因为对象、数方面没有基本的认识,所以必须把《易经》的象认识清楚。宋以后的《易经》注解,多数是走物理的路线,就是用儒家的学术思想来解释《易经》,明以后《易经》监本,是明朝以后那些儒家采用了朱熹的思想编的。故《易经》的理不必太偏重它,但并不是不重视。譬如乾卦,朱熹认为是那样,我们亦可以认为是这样,各有各的理,正理只有一条,歪理可有千条。而《易》的象与数,却是科学的,没有办法讲歪的,就非要学会它的规矩、法则,才能懂得《易经》。\n六十四卦的来源 《易》的象、数,该如何开始学起?从中国学术史上看,唐宋以前,还没有分宫卦象次序,宋以后,才把这个次序列出来。这个次序的排列,是有一定道理的,是由每一卦变化出了八个卦,八个卦变成六十四卦。\n先天卦,有三爻,名三爻卦,是伏羲画的卦,亦是中国有文字的开始。后来人类社会越发展,人事越复杂,三爻卦已经不够用,就变成了六爻卦。后天卦统统是六爻的图案,这六爻卦是很精细的,亦是很科学的。\n为什么要用六爻?因为一直到现代的科学时代为止,宇宙间的事情、物理,没有超过六个阶段的。一切的变,只能变到第六个阶段,第七个变是另外一个局面开始。孔子在《系传》中说六爻的道理是:“六爻之动,三极之道也。”什么是“三极”?就是天地人三才。三才有阴阳相对,三二就得六,这是孔老夫子的心得报告,几千年来,没有脱离他的范围。\n要注意,画八卦是从下面画起。那《易经》的卦为什么要这样画?第一个道理,天下的事情发生变动,都是从下面开始变,换言之是从基层变起;第二个道理,《易经》的卦,原来只是三爻,后来变成六爻,名称上就有了分别:下面三爻的卦为 内卦 ,上面三爻的卦为 外卦 ,内外两卦相连起来。自下面开始画卦,亦说明了宇宙事物的变。一切东西都是从内变开始,所以画卦是由下往上,由内而外。\n物必自腐,然后虫生。\n#+caption: 分宫卦象次序\n乾为天 天风姤 天山遁 天地否 风地观 山地剥 火地晋 火天大有 坎为水 水泽节 水雷屯 水火既济 泽火革 雷火丰 地火明夷 地水师 艮为山 山火贲 山天大畜 山泽损 火泽睽 天泽履 风泽中孚 风山渐 震为雷 雷地豫 雷水解 雷风恒 地风升 水风井 泽风大过 泽雷随 巽为风 风天小畜 风火家人 风雷益 天雷无妄 火雷噬嗑 山雷颐 山风蛊 离为火 火山旅 火风鼎 火水未济 山水蒙 风水涣 天水讼 天火同人 坤为地 地雷复 地泽临 地天泰 雷天大壮 泽天夬 水天需 水地比 兑为泽 泽水困 泽地萃 泽山咸 水山蹇 地山谦 雷山小过 雷泽归妹 乾坎艮震为阳四宫,巽离坤兑为阴四宫,每宫阴阳八卦。\n乾宫的八个卦就是这样变得,简单地说,分宫卦象次序的变就是这样的:一、本体卦,二、初爻变,三、第二幺变,四、第三幺变,五、第四爻变,六、第五爻变,七、第四爻变回原爻(游魂),八、内卦变回本体卦(归魂)。\n错综复杂 “错综复杂”的语源,来自《易经》。不三不四也是根据《易经》而来,因为《易经》中的第三爻、第四爻最重要,这两爻在卦的正中间,亦是中心的位置,如果一个人不成样子,就被形容为“不三不四”。又如“乱七八糟”,即是从游魂卦、归魂卦来的,中国人处处都在引用《易经》的话,只是自己不知道而已。\n错综 \u0026ndash; 相对与反对 卦的错综复杂是什么意思?现在先说综卦,以乾卦为例来说明。乾卦的第一爻变为姤卦,如果把这个卦倒转过来看,就成了夬卦,这就是姤卦的综卦。\n综卦 是相对的,全部六十四卦,除了八个卦以外,没有不相对的,这综卦是象,而综卦的理,是告诉我们万事要客观,因为立场不同,观念就完全两样。另外有八个卦是绝对的,无论单方面看或相对地看,都是同一个样子,这八个卦就是 乾、坤、坎、离、大过、小过、颐、中孚 ,除此之外,其余五十六卦都是相对的,这表明宇宙间事物都是相对的,这就是综卦的道理。\n六十四卦,八绝(同)余相(综),皆相错之,三五为交,二四为互。\r错卦 是阴阳交错的意思,错卦的理是立场相同,目标一致,可是看问题的角度不同,所见也就不同了。\n天风姤卦,它的第一爻是阴爻,其余五爻都是阳爻,那么在阴阳交错之后,就变成了地雷复卦,所以天风姤卦的对错卦就是地雷复卦。六十四卦,每卦都有对错的。因此学了《易经》以后,以《易经》的道理去看人生,一举一动,都有相对、正反、交错,有得意就有失意,有人赞成就有人反对,人事物理都一定是这样的,离不开这个宇宙的大原则。\n综卦可以称之为反对的火相对的,错卦可称之为正对的。有人说《易经》动辄有黑格尔的辩证法的思想,他说的正、反、合,就是《易经》的原则,这是乱讲。他们说黑格尔的正、反、合是三段论法,我告诉他们《易经》是八段论法,比起来黑格尔就显得粗糙得很,又算得了什么。《易经》看东西是八面玲珑得。现在已经看了四面了,仍以天风姤卦来说,综卦是泽天夬,错卦是地雷复,而复卦亦有它的综卦,就是山地剥,这岂不是看了四面,所以《易经》的头脑,一件事初到手,处理起来,四面都要注意到,不但要注意四面,还要八面玲珑。\n复杂的道理 《易经》还有一个道理 \u0026ndash; 复杂,亦即等于 交互卦 的道理,我们都讲究互助,这个互象就是《易经》的图案,像同样的挂钩交相挂住,就是一个“互”字。什么是“交互”?就是六爻内部的变化,如第二爻上连到第四爻,下面挂到上面去为互,第五爻下连到第三爻,上面交至下面来为交,这就是交互的不同,每卦的纵深内在,发生了交互的变化,又产生了卦。换句话说,这是告诉我们看事情,不要看绝了,不要只看一面,一件事情正面看了,再看反面,反面看了,再把旁边看清楚,同时旁边亦要看反面,这样四面都注意到了,这还不算完备,因为内在还有变化,而内在的变化,又生出一个卦了。除了乾、坤两卦外,别的卦把重心拿出来交互,又变了一种现象。这现象的本身,又有综卦,又有错卦,这就是八面看东西,还要加上下一共十面。\n交互卦 现在谈交互卦,以火雷噬嗑为例说明如下:火雷噬嗑的第二爻、第三爻、第四爻卦配上去,便成为 ☶ 代表山的艮卦,这就是噬嗑卦的互卦;又把噬嗑卦的第三爻、第四爻、第五爻配上去,便成为 ☵ 卦,这就是噬嗑卦的交卦。再把噬嗑卦的交卦 ☵ 和互卦 ☶ 重叠起来,便成为水山骞卦,于是我们知道,噬嗑卦的交互卦就是蹇卦。\n至于复杂,复就和综卦一样,是重复的意思,杂是指彼此的相互关系,六十四卦可发展到无数的卦,每一卦牵一发而动全身,都有彼此相互的关系。\n周易六十四卦的排列,并不是照八宫卦象的次序。它的排列次序,是周文王研究《易经》所整理出来的一个学术思想系统,后人编之成歌 \u0026ndash; 《上下经卦名次序歌》。\n乾坤屯蒙需讼师,比小畜兮履泰否,\r同人大有谦豫随,蛊临观兮噬嗑贲,\r剥复无妄大畜颐,大过坎离三十备。\r咸恒遁兮及大壮,晋与明夷家人睽,\r蹇解损益夬姤萃,升困井革鼎震继,\r艮渐归妹丰旅巽,兑涣节兮中孚至,\r小过既济兼未济,是为下经三十四。 六十四卦的方圆图 这个方圆图,圆图是管宇宙的时间,代表宇宙的运行法则,亦可以说代表太阳系统时间运行的法则或原理,圆图中的方图管空间,代表方位方向,这就是前人的秘诀了。\n方图 #+caption: 六十四卦方图数字图\n8/8 7/8 6/8 5/8 4/8 3/8 2/8 1/8 坤 8 8/7 7/7 6/7 5/7 4/7 3/7 2/7 1/7 艮 7 8/6 7/6 6/6 5/6 4/6 3/6 2/6 1/6 坎 6 8/5 7/5 6/5 5/5 4/5 3/5 2/5 1/5 巽 5 8/4 7/4 6/4 5/4 4/4 3/4 2/4 1/4 震 4 8/3 7/3 6/3 5/3 4/3 3/3 2/3 1/3 离 3 8/2 7/2 6/2 5/2 4/2 3/2 2/2 1/2 兑 2 8/1 7/1 6/1 5/1 4/1 3/1 2/1 1/1 乾 1 坤 8 艮 7 坎 6 巽 5 震 4 离 3 兑 2 乾 1 ←/↑ 这六十四卦的方图,变化无穷。这个方图的数字,则是这样一纵一横,慢慢向上走的,构成了如此错综复杂的关系。可是亦是同时告诉我们,宇宙间的万事万物,看来是非常复杂,但懂了《易经》以后,从《易经》的观点,任何乱七八糟的事物,都有它的法则。在关键上轻轻一点,问题就解决了。\n圆图 围绕在这个方图外的圆图,亦是六十四卦,圆图是代表时间,和代表空间的方图配起来,某一空间在某一时间会起作用。那么这个圆图的六十四卦,是用什么方法排列起来的呢?\n圆图最上面左边第一个卦是乾卦,最下面右边第一个卦是坤卦,在这乾、坤之间有一条线,代表夜间天空中的银河,亦代表地球南极、北极的磁场,然后再来排列圆图。自下而上前四横,自右向左依次排列,直到坤;自上而下,后四横,自左向右依次排列,直到乾。\n学《易》之前,先善上事。\n附录+ 太极 阴爻 阳爻 太阳 少阴 少阳 太阴 ☯ ⚋ ⚊ ⚌ ⚍ ⚎ ⚏ \u0026gt; 太极、阴阳(两仪)、四象\n乾 兑 离 震 巽 坎 艮 坤 ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ \u0026gt; 八卦\n䷀ ䷁ ䷂ ䷃ ䷄ ䷅ ䷆ ䷇ 乾为天 坤为地 水雷屯 山水蒙 水天需 天水讼 地水师 水地比 ䷈ ䷉ ䷊ ䷋ ䷌ ䷍ ䷎ ䷏ 风天小畜 天泽履 地天泰 天地否 天火同人 火天大有 地山谦 雷地豫 ䷐ ䷑ ䷒ ䷓ ䷔ ䷕ ䷖ ䷗ 泽雷随 山风蛊 地泽临 风地观 火雷噬嗑 山火贲 山地剥 地雷复 ䷘ ䷙ ䷚ ䷛ ䷜ ䷝ ䷞ ䷟ 天雷无妄 山天大畜 山雷颐 泽风大过 坎为水 离为火 泽山咸 雷风恒 ䷠ ䷡ ䷢ ䷣ ䷤ ䷥ ䷦ ䷧ 天山遁 雷天大壮 火地晋 地火明夷 风火家人 火泽睽 水山蹇 雷水解 ䷨ ䷩ ䷪ ䷫ ䷬ ䷭ ䷮ ䷯ 山泽损 风雷益 泽天夬 天风姤 泽地萃 地风升 泽水困 水风井 ䷰ ䷱ ䷲ ䷳ ䷴ ䷵ ䷶ ䷷ 泽火革 火风鼎 震为雷 艮为山 风山渐 雷泽归妹 雷火丰 火山旅 ䷸ ䷹ ䷺ ䷻ ䷼ ䷽ ䷾ ䷿ 巽为风 兑为泽 风水涣 水泽节 风泽中孚 雷山小过 水火既济 火水未济 \u0026gt; 六十四卦\n","date":"2020-01-04","permalink":"https://loveminimal.github.io/posts/yi-jing/","summary":"\u003cp\u003e🔔 文中内容,多数摘录自南怀瑾先生的《易经杂说》。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e书不尽言,言不尽意。自觉圣智,完成人格。 \u0026ndash; 南怀瑾\u003c/p\u003e\n\u003c/blockquote\u003e","title":"易经"},]
✖