Git Product home page Git Product logo

blog's People

Contributors

qunzi0214 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

《你不知道的javascript(上)》附录以及个人理解

第一部分附录

动态作用域

function foo() {
  console.log(a); // 2
}

function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar();

如果javascript具有动态作用域,则应该在调用处获得变量 a 的值(3)。而javascript中的 this 的机制和动态作用域很像

Traceur编译器

实际上,谷歌维护的 Traceur 编译器会把块级作用域编译成 try...catch (在es5或以下环境)

显式 let 声明

非官方标准

let (a = 2) {
  console.log(a); // 2
}

console.log(a); // ReferenceError

第二部分附录

es6 class 解决的问题

class Person {
  constructor(sex) {
    this.sex = sex;
  }

  speak() {
    console.log(`hi, i'm a person`);
  }
}

class Rango extends Person {
  constructor(sex, age, name) {
    super(sex);
    this.age = age;
    this.name = name;
  }

  speak() {
    super.speak();
    console.log(`hi, i'm ${this.name}`);
  }
}

const r = new Rango('male', 18, 'Rango');

r.speak();
  1. 不会再引用杂乱的prototype
  2. 可以通过 super 来实现相对多态
  3. 可以通过 extends 很自然的扩展子类型,甚至是内置对象

es6 class 的陷阱

本质上是 prototype 机制的语法糖,子类仍然不是父类的复制,而是委托。同时 super 的绑定机制存在问题(待测试)。

一些个人理解

关于正确使用this机制

书中提倡正确包涵 this 机制(比如通过bind显式声明),而不是使用箭头函数等方式来混淆 this 绑定规则和词法作用域。个人觉得大同小异...

关于类风格和委托风格

书中对于强行将 javascript 的对象委托机制实现为类风格提出了批评。考虑到目前绝大多数前端框架仍然会使用类风格来编写程序,此处存疑,还需要后续知识储备更完善后重新判断

Typescript 4.2英文文档 - Everyday Types

string, number, boolean

在 Typescript 存在3个常用的基本类型:string , number , boolean 。它们的命名和 Javascript 中对一个变量使用 typeof 操作符获得的值相同

使用 string number boolean 来做类型声明而非通过 String Number Boolean,因为后三者是某些场景下特定的内置类型

Arrays

想要指定一个数组的元素类型(比如 [1, 2, 3]),可以使用以下两种语法:

  • number[]
  • Array<number>

需要注意,[number] 是完全不同的东西,这种语法是用来声明元组的(确定元素数量与类型的数组)

any

在 Typescript 中存在一个特殊类型:any ,一旦某个变量被声明为 any ,Typescript 会在编译阶段放弃对它进行类型检查

这同时意味着,可以读写这个变量的任意属性、函数调用该变量、将任何类型的值复制给该变量或将该变量赋值给任意类型的其他变量

let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed 
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

需要注意,一旦你没有指定某个变量的类型且 Typescript 无法通过上下文对其进行类型推论,编译器会默认该变量类型为 any。如果需要避免这种情况,可以设置 noImplicitAny 选项

Type Annotations on Variables

如果通过 var let const 来声明一个变量,那么类型注释是可选的:

  • 通过类型注释显式声明变量类型
let myName: string = "Alice";
  • Typescript 自动对变量进行类型推论
// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";

Functions

在 Typescript 中,允许你同时指定函数参数以及返回值的类型:

  • 参数类型注释
// Parameter type annotation
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

// Would be a runtime error if executed!
greet(42);
  • 函数返回值类型注释
function getFavoriteNumber(): number {
  return 26;
}

和变量类似,通常不需要对函数返回值进行类型注释,因为 Typescript 会根据 return 语句进行类型推论

匿名函数会有些不同,Typescript 会根据匿名函数如何被调用来决定此函数入参的类型:

// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];

// Contextual typing for function
names.forEach(function (s) {
  console.log(s.toUppercase());
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

// Contextual typing also applies to arrow functions
names.forEach((s) => {
  console.log(s.toUppercase());
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

Object Types

对象类型是除基本类型之外最常见的类型,定义一个对象类型,只需简单的罗列出它的属性和属性类型:

// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

定义对象类型,使用 ;, 分隔符都是合法的

可选属性:

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

在 Javascript 中,如果在对象上访问一个不存在的属性,会得到值 undefined 而不是一个运行时报错。鉴于此,在使用可选属性时,需要考虑到 undefined

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
  // Object is possibly 'undefined'.
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }

  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

Union Types

一种方式结合不同的类型是使用联合类型:

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
// Type '{ myID: number; }' is not assignable to type 'number'.

当在 Typesccript 中使用联合类型,只有当联合类型的每个成员都拥有某个方法和属性时,才允许访问:

function printId(id: number | string) {
  console.log(id.toUpperCase());
  // Property 'toUpperCase' does not exist on type 'string | number'.
  // Property 'toUpperCase' does not exist on type 'number'.
}

解决办法是通过代码 narrow(变窄) 联合类型。Narrowing 通常发生在 Typescript 能通过代码结构确定一个更特定的类型时:

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

另一个例子是 Array.isArray

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Here: 'x' is 'string[]'
    console.log("Hello, " + x.join(" and "));
  } else {
    // Here: 'x' is 'string'
    console.log("Welcome lone traveler " + x);
  }
}

如果联合类型中的成员同时具有某个方法或属性,不需要通过 Narrowing 也可以直接访问:

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

Type Aliases

类型别名仅仅是给希望多次使用的任意类型一个名字,语法如下:

type Point = {
  x: number;
  y: number;
};

// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({ x: 100, y: 100 });

可以给任意类型赋予一个类型别名,不仅仅是对象类型。比如给联合类型赋予别名:

type ID = number | string;

需要注意,别名仅仅是别名,无法通过别名给同样的类型定义一个不同的、有区别的版本。

Interfaces

另一种方式给一个对象类型起名是通过接口定义:

interface Point {
  x: number;
  y: number;
}

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({ x: 100, y: 100 });

Type AliasesInterfaces 大多数情况非常类似,主要区别是别名无法通过重复定义的方式来新增属性:

  • 扩展 interface
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey
  • 扩展 type (通过交集)
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: Boolean 
}

const bear = getBear();
bear.name;
bear.honey;
  • 给已存在的 interface 新增字段:
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
  • 给已存在的 type 新增字段:
type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

 // Error: Duplicate identifier 'Window'.

Type Assertions

某些情况下,你会比 Typescript 更了解某个值的类型信息。例如通过 document.getElementById ,Typescript 只知道会返回一个 HTMLElement 类型的值,但是你可能知道你的页面上会返回一个 HTMLCanvasElement 类型的值。

这种场景下,可以使用类型断言来指定一个更特定的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

和类型注释一样,类型断言会被编译器移除,不会影响代码运行时

另一种方式使用类型断言是通过尖括号语法( .tsx 文件不适用):

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

Typescript 仅允许通过类型断言将类型转换为更具体或更不具体的类型。该规则可防止出现“不可能”的强制转换:

const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

有时候,这条规则过于保守,禁止了一些更复杂但有效的强制转换。解决办法是通过两次类型断言(先断言为 anyunknown):

const a = (expr as any) as T;

Literal Types

除了基本类型 stringnumber,有时候需要指定某个更具体的字符串或数字

一种方式是 const

let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
// let changingString: string

const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
// const constantString: "Hello World"

鉴于通常一个可变的变量不会只持有一个值,以下这种方式不是很有价值:

let x: "hello" = "hello";
// OK
x = "hello";
// Type '"howdy"' is not assignable to type '"hello"'.
x = "howdy";

如果通过联合类型结合字面量类型,可以表达出一个非常有用的概念:例如函数只能接受某些确定的值

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

数字的字面量类型工作方式是一样的:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

当然,也可以结合字面量类型和非字面量类型:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

还有最后一种字面量类型:布尔值字面量类型,类型 boolean 本身就是布尔值字面量类型 true | false 的别名

在定义一个对象时,Typescript 不会将该对象的属性推论为字面量类型,例如以下例子中 counter 被推论为 number 类型而不是 0 类型:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

考虑如下场景,req.method 被推论为 string,在 req 被初始化,和 handleRequest 被调用之间,该值可能会被重新赋予一个字符串,Typescript会认为这是个错误:

function handleRequest(url: string, method: "GET" | "POST")

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

解决办法如下:

  1. 通过类型断言改变类型推论:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

Change 1 表示,req.method 会保持持有字面量类型 GET,此字段被重新赋值 GUESS 是不可能发生的(如果发生了编译会不通过)
Change 2 表示,我能确定因为某些原因,req.method 肯定持有值 GET

  1. 使用 as const 将整个对象所有属性转变为字面量类型:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

null and undefined

在 Javascript 中,有两个基本值 nullundefined 代表缺省或未初始化,Typescript 中同样有两个名字一致相应的类型,这两种类型如何表现取决于 strictNullChecks 选项是否打开。

  • strictNullChecks 关闭时:

nullundefined值可以正常访问,也可以用来赋予声明为任意类型的变量或属性。然而缺少了类型检查,这些值会导致大量bug,推荐打开 strictNullChecks 选项

  • strictNullChecks 打开时:

当一个值是 nullundefined,在使用这个值之前必须确保这个值拥有相应的属性或方法,类似于通过 Narrowing 来使用可选属性:

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

Non-null 类型断言操作符:

Typescript 中有一个特殊的语法,用来移除某个类型的 nullundefined,即表示,此值不可能为 nullundefined

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

和其他类型断言一样,这段代码不会对运行时产生影响,因此在使用 ! 操作符前,必须确保你知道这个值真的不可能是 nullundefined

Enums

枚举类型是对 Javascript 的一个扩展,允许给一组可以命名的值增加描述(太绕了,看例子),和其他 Typescript 特性不同,枚举类型是一个运行时的特性:

enum Color { Red, Blue, Green }

编译后的 Javascript :

var Color;
(function (Color) {
  Color[Color["Red"] = 0] = "Red";
  Color[Color["Blue"] = 1] = "Blue";
  Color[Color["Green"] = 2] = "Green";
})(Color || (Color = {}));

Less Common Primitives

bigint:ES2020之后,推出的一个新基本类型,用来存储一个非常大的整数

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);

// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

symbol:通过 Symbol() 函数创建一个全局独一无二的引用类型:

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
// This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
  // Can't ever happen
}

《你不知道的javascript(中)》1.1 类型

内置类型

  • 空值(null)
  • 未定义(undefined)
  • 布尔值(boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol)

复习下字面量和构造初始化区别:boolean、string、number字面量与对应的内置对象不等同,null和undefined没有构造形式,而Object、Array、Function和RegExp无论使用字面量还是构造形式来声明,他们都是对象。同时,javascript在操作基本类型的时候自动将之转化为内置对象

可以用 typeof 操作符来查看值的类型
typeofnull 的处理有问题,但是这个bug由来已久,今后也未必会修复

typeof undefined   // 'undefined'
typeof true   // 'boolean'
typeof 42   // 'number'
typeof '42'   // 'string'
typeof {}   // 'object'
typeof Symbol()   // 'symbol'
typeof null   // 'object'

函数和数组都是 object 的子类型。函数是可调用对象,不仅如此,函数同样可以拥有属性

typeof [1, 2, 3] // 'object'

typeof function a (b, c) {} // 'function'

a.length // 2 (参数的个数)

值和类型

javascript中变量是没有类型的,只有值才有。变量在未持有值的时候是 undefined 。此时 typeof 返回 "undefined" 。而没有在作用域中声明的变量,是 undeclared 的(运行时会报错)。同时,typeof 有一个特殊的安全防范机制,可以使其对 undeclared 的变量进行操作时同样返回 undefined 而不报错,可以利用这个机制来处理变量是否存在的问题

var helper = (typeof doSomething !== 'undefined')
  ? doSomething
  : function() { /* do something */ }

helper()

还有一种方式是,通过全局对象 window 属性访问的方式来避免报错,只不过这种方式不适用于多 javascript 环境,因为此时无法保证全局对象是 window

var helper = window.doSomething
  ? window.doSomething
  : function() { /* do something */ }

helper()

《你不知道的javascript(上)》第一部分 作用域和闭包

作用域是什么

var 关键字会导致变量提升的本质:

javascript是在运行前极短的时间内被编译的,var a = 2 实际上是两个动作:编译器会在当前作用域声明变量a(如果之前没被声明过),紧接着运行时引擎在作用域内寻找该变量,如果能找到就对它进行赋值。

LHSRHS

var a = 2 // 此时对变量a是LHS查询,即找到变量本身的容器
console.log(a) // 此时对变量a是RHS查询,即找到变量对应的值(引用)

关于左右侧查询的异常

function foo(a) {
  console.log(a + b); // ReferenceError: 对b进行RHS报错
  b = a; // 对b进行LHS会在全局创建一个b
}

foo(2);

// 严格模式下对未声明的变量进行 LHS 或 RHS 都会报错

ReferenceError 和作用域判别失败相关,而 TypeError 代表作用域判别成功了,但是对结果的操作是非法的

词法作用域

词法阶段

  1. 词法作用域查找从内向外,找到第一个匹配的标识符停止
  2. 可以通过 window.a 的方式访问那些被同名变量遮蔽的全局变量
  3. 函数的词法作用域只由函数被声明时的位置决定
  4. 词法作用域只会查找一级标识符,foo.bar.baz 只会查找到 foo ,后续会交给对象属性访问规则

欺骗词法(eval 和 with)

function foo(str, a) {
  eval(str); // 严格模式下有自己的词法作用域
  console.log(a, b);
}

var b = 2;

foo('var b = 3;', 1);
function foo(obj) {
  with(obj) {
    a = 2;
  }
}

var obj1 = { a: 1 };
foo(obj1);
console.log(obj1.a); // 2
console.log(a); // ReferenceError
var obj2 = { b: 3 };
foo(obj2);
console.log(obj2.a); // undefined
console.log(a); // 2

// with会基于传入的对象生成一个全新的词法作用域,当obj2作为作用域时,其中没有a标识符,因此进行了正常的LHS查找,最终在全局创建了一个变量a

词法分析阶段基本能够知道全部的标识符在什么位置,从而可以预测以及优化运行时对它们的查找。因此欺骗词法会导致性能问题

函数作用域、块作用域

函数声明与函数表达式

如果function是声明中的第一个词,那么就是一个函数声明,否则是一个函数表达式。它们的区别是函数声明会将函数绑定在所在作用域中,而函数表达式会将函数绑定在自身的函数中。

为什么不推荐匿名函数

  1. 在函数调用栈中没有函数名,会导致调试困难
  2. 当函数需要引用自身时,只能使用已经过期的 arguments.callee (递归或是事件监听器需要解绑)
  3. 代码失去可读性

块级作用域对垃圾回收机制的优化

function process(data) {
  // do something
}

{
  let bigData = {};
  process(bigData);
}

var btn = document.getElementById('btn');
btn.addEventListener('click', function click(e) {
  // do something
}, false);

回调函数 click 形成了一个覆盖整个作用域的闭包,javascript引擎极有可能在 process 函数执行后还保留着
bigData (?!)。如果使用块级作用域,可以告诉引擎不需要保留 bigData

作用域闭包

定义

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

闭包和模块模式运用

var foo = (function CoolModule(id) {
  function change() {
    publicAPI.identify = identify2;
  }

  function identify1() {
    console.log(id);
  }

  function identify2() {
    console.log(id.toUpperCase());
  }

  var publicAPI = {
    change: change,
    identify: identify1,
  };

  return publicAPI;
})('some string');

foo.identify(); // some string
foo.change();
foo.identify; // SOME STRING

requireJS原理

var MyModule = (function Manager() {
  var modules = {};

  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define: define,
    get: get,
  };
})();

MyModule.define('bar', [], function () {
  function hello(who) {
    return 'hello ' + who;
  }

  return {
    hello: hello,
  }
});

MyModule.define('foo', ['bar'], function (bar) {
  function awesome() {
    console.log(bar.hello('Rango').toUpperCase());
  }

  return {
    awesome: awesome,
  }
});

var foo = MyModule.get('foo');
foo.awesome(); // HELLO RANGO

LeetCode 279 完全平方数

题目如下:

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.
示例 2:

输入: n = 13
输出: 2
解释: 13 = 4 + 9.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路一(BFS):

  1. 找到小于 n 的所有平方数,存入数组 squaresNum
  2. 将组合 '0000...' (长度等于squaresNum长度) 入队;
  3. 出队;
  4. 如果当前出队组合计算后等于 n 终止循环;
  5. 找到所有当前出队组合任意位置 + 1后的新组合;
  6. 如果新组合已经存在 visited 中或计算后大于 n ,跳过。否则加入队列;
  7. 重复操作 3 - 6;
var numSquares = function(n) {
    
    const squaresNum = []; // 找出所有小于n的平方数
    
    let i = 1;
    while(i * i <= n) {
        if(i * i === n) {
            return 1;
        }
        
        squaresNum.push(i * i);
        i++;
    }
    
    const visited = new Set();
    const queue = [];
    let countStr = new Array(squaresNum.length).fill(0).join('');
    queue.push(countStr);
    visited.add(countStr);
    
    while(queue.length) {
        const currentStr = queue.shift();
        
        if(calculateSum(squaresNum, currentStr, n) === 0) {
            countStr = currentStr;
            break;
        }
        
        for(let i = 0; i < currentStr.length; i++) {
            const nextStr = getNextStr(currentStr, i);
            if(calculateSum(squaresNum, nextStr, n) <= 0 && (!visited.has(nextStr))) {
                queue.push(nextStr);
                visited.add(nextStr);
            }
        }
    }
    
    return countStr.split('').reduce((a, b) => parseInt(a) + parseInt(b));
};

// 计算总值和目标值关系 大于返回1, 等于返回0, 小于返回-1
function calculateSum(squaresNum, countStr, target) {
    let sum = 0;
    for(let i = 0; i < squaresNum.length; i++) {
        sum += squaresNum[i] * parseInt(countStr[i]);
    }
    return sum === target ? 0 : sum > target ? 1 : -1;
}

// 生成下一轮字符串
function getNextStr(currentStr, index) {
    const arr = currentStr.split('');
    arr[index] = parseInt(arr[index]) + 1 + '';
    return arr.join('');
}

问题:当传入n足够大时,每次出队组合对应的新组合太多,会超时


解题思路二(DP):

类似斐波那契数列优化后,用迭代代替递归计算第 n 位的数字
保存中间值以免反复计算

  1. 初始化 dp 为长度 n+1 的数组,并填充0;
  2. dp[0] = 0 对应了0是0个平方数的和,即0的最优解为0;
  3. 从第1位开始遍历数组;
  4. 遍历当前位置 i 时,先假设最优解 dp[i] = i (i个1的和)
  5. 找到所有小于 i 的平方数 kdp[i] = min(dp[i - k] + 1); ∀k∈小于等于i的平方数
  6. 更新 dp[i]

动态转移方程的理解:
对于正整数 i ,它的最优解肯定是之前某一整数(包括0)的最优解加上1(之前某一整数加一个平方数),遍历所有小于 i 的平方数 k,找到最小的那个 dp[i - k] ,那么加上1就是 i 的最优解

var numSquares = function(n) {
    if(n === 0) {
        return 0;
    }
    
    const dp = new Array(n + 1).fill(0);
    
    for(let i = 1; i <= n; i++) {
        dp[i] = i; // 最坏的情况 3 = 1 + 1 + 1
        for(let j = 1; i - j * j >= 0; j++) {
            dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
        }
    }
    return dp[n];
};

《鸟哥的Linux私房菜(基础篇)》第5章 Linux的文件权限与目录配置

用户组

用户的三种身份

  • 文件拥有者
  • 用户组
  • 其他人

文件权限概念

文件属性

使用命令 ls -al 查看当前目录所有文件以及详细属性

$ ls -al
drwxr-xr-x   8 rango  staff    256 11 18 14:36 .npm
文件权限 链接数 拥有者 用户组 大小(Bytes) 最后修改时间 文件名/目录名
drwxr-xr-x 8 rango staff 256 11 18 14:36 .npm

文件权限:

第一个字符代表文件类型:

  • d(directory) 目录
  • - 常规文件
  • l(link) 链接文件
  • c(character) 设备文件里面串行端口设备(键盘、鼠标)
  • b(block) 设备文件里面可供存储的周边设备(硬盘)
  • s(socket) 数据接口文件
  • p(pipe) 数据传送文件,FIFO(first in fisrt out)

接下来的字符三个为一组,切均为 rwx 三个字母的组合,代表可读(read)、可写(write)、可执行(execute),如果显示为 - ,则代表不具备此权限

  • 第一组为文件拥有者权限
  • 第二组为加入用户组后的权限
  • 第三组为其他人权限

权限对文件/目录的含义

权限 文件 目录
r(read) 可读取此文件的实际内容 可读取此目录结构列表(不包含查看目录下文件详细属性和内容)
w(write) 可以编辑文件内容(但不包含删除) 具有改动目录的权限:
建立新文件或目录
删除已经存在的文件或目录
将已经存在的目录或文件更名
移动该目录内文件和目录的位置
x(execute) 可以执行该文件 能否进入该目录

修改文件属性与权限

chgrp change group

修改文件所属用户组,要被修改的组名必须要在 /etc/group 中存在,-R 选项代表递归修改目录下子目录及文件的用户组

$ chgrp [-R] groupname filename/dirname

chown change owner

修改文件拥有者,要被修改的用户必须要在 /etc/passwd 中存在,-R 选项代表递归修改目录下子目录及文件的拥有者。这条命令还可以一次性同时修改拥有者和用户组

$ chown [-R] username filename/dirname
$ chown [-R] username:groupname filename/dirname

chmod 修改权限

使用数字的方式:

  • 权限对应数值:r 的值是4, w 的值是2, x 的值是1
  • 每一组权限最终的值,是3个权限的数值和
  • 例子: -rwxrw---x 对应的权限数字是 761(owner = 4 + 3 + 1 = 7, group = 4 + 2 = 6, other = 1)
$ chmod [-R] 777 filename/dirname    #将该文件/目录权限设置为 -rwxrwxrwx

使用符号的方式:

  • 权限组符号: u user, g group, o others, a all
  • 设置符号: + 加入权限, - 减去权限, = 设置权限
$ chmod u=rwx,go=rx filename/dirname    #-rwxr-xr-x
$ chmod a+w filename/dirname    #-rwxrwxrwx

Linux 目录配置

目录配置依据FHS(Filesystem Hierarchy Standard)

可分享(shareable) 不可分享(unshareable)
不变(static) /usr(软件存放处)
/opt(第三方辅助软件)
/etc(配置文件)
/boot(启动与内核文件)
可变动(variable) /val/mail(用户邮箱)
/var/spool/news(新闻组)
/var/run(程序相关)
/var/lock(程序相关)
  • 可分享:可以分享给网络上其他主机挂载用的目录
  • 不可分享:自己机器上面运行的设备文件或是与程序相关的socket文件等
  • 不变:函数库、文件说明、主机服务配置文件等不会经常变动的文件
  • 可变动:日志文件、一般用户可自行接收的新闻组等经常修改的数据

实际上,FHS针对目录仅定义出三层目录下面应该放置什么数据:

  • /(root,根目录) :与启动系统有关
  • /usr(unix software resource) :与软件安装/执行有关
  • /var(variable) :与系统的运行过程有关

/(root)

目录 应放置的文件内容
/bin 一般用户可以使用、普通的系统命令:cat、chmod、chown、chgrp、date、mv、mkdir、cp等
/sbin root权限可以使用,启动、修复、还原系统等基本的系统命令:shutdown、reboot等
/boot 启动时会用到的文件、Linux内核文件(vmlinuz)
/dev 设备、接口设备:/dev/null、/dev/zero、/dev/tty、/dev/sd*等
/etc 系统配置文件:/etc/modprobe.d、/etc/passwd、/etc/issue等
不应该放入可执行文件。另外FHS建议应该存在如下目录:
/etc/opt:第三方辅助软件 /opt 的配置文件
/etc/X11:与 X Window 有关的配置文件
/etc/sgml:与SGML格式有关的配置文件
/etc/xml:与XML格式有关的配置文件
/lib 启动时、/bin、/sbin下面的命令会调用的函数库,FHS要求 /lib/modules 必须存在以放置驱动文件
/media 可删除的设备,软盘、光盘等
/mnt 用于暂时挂载某些额外设备
/opt 第三方辅助软件目录,通常更加习惯放于 /usr/local 目录中
/run 早期的FHS规定系统启动后产生的信息应该放入 /var/run,新版改为 /run
/srv 网络服务启动之后需要使用的数据目录,如WWW、FTP等
/tmp 正在执行的程序暂时放置文件的地方,需要定期清理
/usr 软件执行、安装相关(第二层FHS)
/var 变动性数据(第二层FHS)
/home 默认的系统家目录
/lib 存放与 /lib 不同的二进制函数库
/root 系统管理员的家目录
/lost+found 使用ext2、ext3、ext4文件系统格式产生的目录,当文件系统发生错误时,将一些遗失的片段放入其中,使用xfs文件系统不会存在这个目录
/proc 这个目录本身是一个虚拟文件系统,放置内存中的系统内核、进程信息、外接设备状态、网络状态等数据
/sys 作用和 /proc 类似

/usr

目录 应放置的文件内容
/usr/bin/ CentOS 7 已经将全部的用户命令放置于此,/bin 链接此目录。另外,FHS要求此目录下不应该有子目录
/usr/lib/ /lib 链接此目录
/usr/local/ 系统管理员下载的软件建议安装在此目录,同时安装软件产生的执行文件会放入 /usr/local/sbin/
/usr/sbin/ 新版本 /sbin 链接此目录,老版本用于和 /sbin 做一个区分,放入服务器软件程序
/usr/share/ 只读的数据文件、可分享的文件
/usr/games/ 与游戏相关的数据
/usr/include/ c/c++ 等程序语言的头文件与包含文件,使用Tarball(*.tar.gz)方式安装某些程序时会用到里面许多文件
/usr/libexec/ 一般用户不常用的执行文件或脚本
/usr/lib/ /lib 链接此目录
/usr/src/ 源代码放置处

/var

目录 应放置的文件内容
/var/cache/ 缓存目录
/var/lib/ 程序执行中,需要使用的数据文件放置目录:MySQL的数据库放置于 /var/lib/mysql/
/var/lock/ 某些设备或文件同时只能被一个程序使用,因此需要上锁。目前此目录挪至 /run/lock/
/var/log/ 日志文件目录
/var/run/ 此目录链接 /run
/var/mail/ 放置个人电子邮箱的目录,通常和 /var/spool/mail 互为链接
/var/spool/ 队列数据,即排队等待使用的数据

LeetCode 680 验证回文字符串Ⅱ

题目如下:

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。

示例 1:

输入: "aba"
输出: True
示例 2:

输入: "abca"
输出: True
解释: 你可以删除c字符。
注意:

字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/valid-palindrome-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路一:

对于这个加强版验证回文,首先很容易想到的就是在判别普通回文方法的基础上升级。
当字符串不符合回文格式时,遍历每一个字符串删除后是否符合回文格式。

很显然的问题是,极大提高了时间复杂度,有没有更好的方式呢?


解题思路二:

设定左右两个游标,通过逐一比对的方式验证是否满足回文格式,同时如果不满足,左或者右游标可以跳过一次当前字符(即删除当前字符)。

同时考虑一下边界情况,如果left = right - 1 且 删除机会并没有使用,那么意味着前面的比对都通过了,且字符串长度为偶数,那么任意删除当前左右游标的字符,肯定能生成一个满足回文格式的字符串。代码如下

var validPalindrome = function(s) {
    let left = 0;
    let right = s.length - 1;
    let chance = 1;
    while(left < right - 1){
        if(s[left] === s[right]){
            left++;
            right--;
        }
        else if(s[left + 1] === s[right] && chance > 0){
            console.log('左边删除', s[left]);
            left++;
            chance--;
        }
        else if(s[right - 1] === s[left] && chance > 0){
            console.log('右边删除', s[right]);
            right--;
            chance--;
        }
        else {
            return false;
        }
    }
    return true;
};

然而事情并没有结束
其中一条没有通过的测试用例是这样的

"aguokepatgbnvfqmgmlcupuufxoohdfpgjdmysgvhmvffcnqxjjxqncffvmhvgsymdjgpfdhooxfuupuculmgmqfvnbgtapekouga"

提取出问题发生的核心位置,即:
"cuppucu"这个字符串。

很显然走我的判断函数,那么会删除掉左边的字符c,然后无法形成回文,但是如果删除右边的字符u,是可以生成回文的 (╬ ̄皿 ̄)

可以通过再次移动左右游标来排除这种情况,最终代码:

var validPalindrome = function(s) {
    let left = 0;
    let right = s.length - 1;
    let chance = 1;
    while(left < right - 1){
        if(s[left] === s[right]){
            left++;
            right--;
        }
        else if(s[left + 1] === s[right] && s[left + 2] === s[right - 1] && chance > 0){
            left++;
            chance--;
        }
        else if(s[right - 1] === s[left] && s[right - 2] === s[left + 1] && chance > 0){
            right--;
            chance--;
        }
        else {
            return false;
        }
    }
    return true;
};

附上结果:

image

Typescript 4.2英文文档 - Narrowing

想象下有个函数 padLeft,它要实现的功能是:如果参数 paddingnumber 类型,表示会在参数 input 前增加多少个空格。如果参数 paddingstring 类型,直接加在参数 input 前:

function padLeft(padding: number | string, input: string) {
  return new Array(padding + 1).join(" ") + input;
  // Operator '+' cannot be applied to types 'string | number' and 'number'.
}

Typescript 报错,并提示 number 类型和 number | string 类型相加可能不是我们想要的结果。因此需要在使用前显式的检测参数 padding 是不是 number 类型:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

Typescript 会分析运行时代码并和静态类型结合:if/else 结构的控制流、三目表达式、循环等等。在上面的例子中,Typescript 在 if 语句中看到 typeof padding === "number" 并了解到这是一种叫 type guard 的特殊形式代码,并根据程序可能的执行路径分析出一个更具体的类型。类似这种特殊的检查被称为 Narrowing

typeof type guards

在 Javascript 中,typeof 操作符会在运行时返回操作值的基本类型信息字符串:

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • object
  • function

Typescript 甚至兼容了许多 Javascript 的“怪癖”,例如对 null 使用 typeof 操作符会返回 object

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
      // Object is possibly 'null'.
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}

在这个例子中,strs 仅仅被 narrow 至类型 string[] | null 而不是 string[],因此 Typescript 会报错

Truthiness narrowing

在 Javascript 中,可以在条件判断语句( if && || ! )中使用任意表达式,例如 if 语句并不要求括号中必须是 boolean 类型,它会将括号中的值/语句强制类型转化为 boolean 类型的值去做判断。以下这些值会被转换为 false

  • 0
  • NaN
  • ""
  • 0n(the bigint version of zero)
  • null
  • undefined

而其他值都会被转换为 true。始终可以通过 Boolean 函数或者双重否定表达式 !! 来将某个值转化为 boolean 类型的值(需要注意的是,前者在 Typescript 中会被类型推论为 boolean,而后者会被推论为一个字面量布尔类型 true ?)

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world";        // type: true,    value: true

这里自己测试结果是都推论为布尔类型,双重否定并没有推论为字面量布尔类型

修改上面的 printAll 函数,加入 truthiness narrowing,会发现 Typescript 的报错消失了:

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

需要记住,对基本类型做真实性检查通常会引发错误,考虑以下代码:

function printAll(strs: string | string[] | null) {
  // !!!!!!!!!!!!!!!!
  //  DON'T DO THIS!
  //   KEEP READING
  // !!!!!!!!!!!!!!!!
  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}

这里用一个真实性检查包裹了整个函数体,但是有一个微妙的问题:忽略了处理空字符串场景

对于不熟悉 Javascript 的人来说,这是需要注意的情况。Typescript 可以帮助我们更早的捕获错误,但是如果我们对某个值不做任何操作,它仅能在不过度规定的情况下做这么多(没看懂,个人理解是处理不了这种情况)。如果需要,可以使用类似 linter 这样的工具

最后一点关于 Truthiness narrowing,布尔否定 ! 时,Typescript 会从否定分支进行 narrowing

function multiplyAll(
  values: number[] | undefined,
  factor: number
): number[] | undefined {
  if (!values) {
    return values;
  } else {
    return values.map((x) => x * factor);
  }
}

Equality narrowing

Typescript 同样会使用 switch 语句,相等检查例如 === !== == != 来进行 narrowing

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
    y.toLowerCase();
  } else {
    console.log(x);
    // (parameter) x: string | number
    console.log(y);
    // (parameter) y: string | boolean
  }
}

以上代码中,当我们确保 xy 值和类型都相同时,Typescript 会发现它们只有一个公用类型 string,所以在此分支中,可以直接使用任意字符串方法

检查是否特定类型的字面量也是生效的,考虑如下代码,通过特定检查,Typescript 正确的将 null 类型从 strs 上过滤掉了:

function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === "object") {
      for (const s of strs) {
        // (parameter) strs: string[]
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
      // (parameter) strs: string
    }
  }
}

Javascript 的宽松相等 == != 在 Typescript 中也可以正确的 narrowing。在宽松相等中,检查某个值是否 null 不仅仅是特定值 null 本身,同样也检查是否是 undefined,反之亦然(不管怎么说还是不要用宽松相等了...):

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {
  // Remove both 'null' and 'undefined' from the type.
  if (container.value != null) {
    console.log(container.value);
    // (property) Container.value: number

    // Now we can safely multiply 'container.value'.
    container.value *= factor;
  }
}

The in operator narrowing

在 Javascript 中,可以通过 in 操作符来判断某个属性是否存在一个对象中。Typescript 可以通过这种方式来 narrowing

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
    // (parameter) animal: Fish
  }

  return animal.fly();
  // (parameter) animal: Bird
}

需要注意的是,可选属性在 narrowing 时,会同时存在于两边:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = {  swim?: () => void, fly?: () => void };

function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) { 
    animal
    // (parameter) animal: Fish | Human
  } else {
    animal
    // (parameter) animal: Bird | Human
  }
}

instanceof narrowing

在 Javascript 中,x instanceof Foo 可以检查 Foo.prototype 是否存在于 x 的原型链中。同样的,Typescript 中可以通过 instanceofnarrowing

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
    // (parameter) x: Date
  } else {
    console.log(x.toUpperCase());
    // (parameter) x: string
  }
}

Assignments

当我们给一个变量赋值时,Typescript 会分析赋值表达式右侧,并 narrowing 合适的左侧类型:

let x = Math.random() < 0.5 ? 10 : "hello world!";
// let x: string | number

x = 1;
console.log(x);
// let x: number

x = "goodbye!";
console.log(x);
// let x: string

x = true
// Type 'boolean' is not assignable to type 'string | number'.

这里需要注意,尽管在 x = 1 这个赋值表达式之后,可以观察到 x 的类型为 number ,但仍旧可以将 string 类型的值赋予 x 。这是因为 x 的声明类型(即最初的类型)是 string | number

Control flow analysis

基于“流程上是否可到达”的分析也被称作控制流分析。Typescript 会根据控制流 narrowing 类型 — 当变量遇到类型守卫或赋值表达式。当一个变量被分析时,控制流可能一次又一次的拆分或重新合并,在每一个拆分点或合并点,变量都会被观测到不同的类型:

function example() {
  let x: string | number | boolean;
  x = Math.random() < 0.5;
  console.log(x);
  // let x: boolean

  if (Math.random() < 0.5) {
    x = "hello";
    console.log(x);
    // let x: string
  } else {
    x = 100;
    console.log(x);
    // let x: number
  }

  return x;
  // let x: string | number
}

Using type predicates

截至目前,我们已经通过 Javascript 的结构来处理 narrowing,但是有时候我们可能想要更直接的在代码中操作类型变化

想要定义一个自定义 type guard,仅需要简单的定义一个返回值是 type predicate 的函数:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

此例中,pet is Fish 就是 type predicate(类型谓语?) ,类型谓语总是 parameterName is Type 这种形式( parameterName 必须存在于当前函数签名中)

任何时候,当 isFish 被调用,Typescript 都可以进行 narrowing

let pet = getSmallPet();
// let pet: Fish | Bird

if (isFish(pet)) {
  pet.swim();
  // let pet: Fish
} else {
  pet.fly();
  // let pet: Bird
}

也可以通过类型守卫 isFish 来过滤一个 Fish | Bird 数组,获得一个 Fish 数组:

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];

// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

Discriminated unions

思考如下代码,定义一个 Shape 接口,kind 字段是一个联合字面量类型(防止拼写错误)。同时,当图形为圆时,我们关注的是 radius 字段,当图形为正方形时,则是 sideLength ,因此这两个字段是可选的:

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

当我们想要编写一个 getArea 函数(基于是圆形还是正方形计算面积):

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
  // Object is possibly 'undefined'.
}

如果设置了 strictNullChecks 配置项,会获得一个错误,radius 可能是没有定义的。如果我们尝试通过 kind 字段来优化这个问题呢?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
    // Object is possibly 'undefined'.
  }
}

Typescript 依旧不会理解这里到底要做什么。在此处,我们对于类型的了解比 Typescript 要多,可以尝试用 non-null 断言告诉 Typescript radius 一定存在:

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2;
  }
}

但是这种方式仍然不理想且容易出错。另外,如果不设置 strictNullChecks,虽然可以随意访问任何属性,但是也同样极容易导致错误(可选属性会被假设为始终存在)

问题在于类型检查器没有任何途径根据 kind 属性知道 radiussideLength 是否存在,考虑到这个问题,可以换一种方式来定义 Shape

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

这里,我们拆分了 Shape 变成两个类型,同时 radiussideLength 不再是可选属性,而是必选属性。此时如果访问 Shape 类型的 radius 属性:

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
  // Property 'radius' does not exist on type 'Shape'.
  // Property 'radius' does not exist on type 'Square'.
}

当我们使用可选属性时,Typescript 仅能警告这条属性可能不存在。当联合两个接口时,Typescript 会告诉我们 Shape 有可能是 Square,而 Square 上不存在 radius 属性!

与此同时,如果再次尝试检查 kind 属性:

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
    // (parameter) shape: Circle
  }
}

错误消失了!当一个联合类型中的所有成员都有同一个属性时(此属性必须是字面量类型),Typescript 会认为这是一个 discriminated union,并可以根据此属性来 narrow 联合类型的成员,此例中 kind 会被认为是 Shape 的“区别属性”。同样的,switch 语句也可以正常生效:

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
      // (parameter) shape: Circle
    case "square":
      return shape.sideLength ** 2;
      // (parameter) shape: Square
  }
}

The never type

如果我们通过 narrowing,在某个点排除了所有可能的类型,那么此时,Typescript 会使用 never 类型来表示这种不应该不存在的状态

Exhaustiveness checking

never 类型可以赋予任何类型,但是任何类型都不能赋予 never 类型(除了 never 本身),可以依靠这个特性,在 switch 语句中做一个是否穷尽判断:

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

如果在此时给联合类型新增一个成员,Typescript 会报错:

interface Triangle {
  kind: "triangle";
  sideLength: number;
}

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      // Type 'Triangle' is not assignable to type 'never'.
      return _exhaustiveCheck;
  }
}

《你不知道的javascript(中)》1.4 强制类型转换

值类型转换

应该将JavaScript中的类型转换区分为:显式强制类型转换(发生在编译阶段)、隐式强制类型转换(发生在运行时)

强制类型转换总是返回基本类型值,不会返回对象或函数。为基本类型值(除 object )封装一个相应类型的对象,并非严格意义上的强制类型转换

var a = 42
var b = a + '' // 隐式强制类型转换
var c = String(a) // 显式强制类型转换

除了字面意思意外,两种转换在行为特征上也有一定差别

抽象值操作

ToString

基本类型值的字符串化规则为:

  • null 转化为 'null'

  • undefined 转化为 'undefined'

  • true 转化为 'true'

  • 数字遵循通用规则,不过极大或极小的数字会使用指数形式

    var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000
    a.toString() // '1.07e+21'
  • 对于普通对象来说,除非自行定义,否则会调用 Object.prototype.toString() 返回内部属性 [[Class]] 的值

  • 数组的 toString() 方法重新定义过

    [1, 2, 3].toString() // '1,2,3'

工具函数 JSON.stringify() 在将JSON对象序列化为字符串时也会调用 toString() ,但是它并非是严格意义上的强制类型转换。对于大多数简单值来说,JSON.stringify()toString() 效果相同

JSON.stringify('aa') // ""aa""
JSON.stringify(42) // "42"
JSON.stringify(null) // "null"
JSON.stringify(true) // "true"

所有安全的JSON值(JSON-safe)都可以用 JSON.stringify() 字符串化。对于不安全的JSON值( undefined function symbol 循环引用的对象 ):

  • 在对象中遇到 undefined function symbol 会自动忽略,在数组中遇到则返回 null

    JSON.stringify(undefined) // undefined
    JSON.stringify(() => {}) // undefined
    JSON.stringify([1, undefined, () => {}, 4]) // "[1,null,null,4]"
    JSON.stringify({ a: 1, b: () => {}, c: undefined }) // "{"a":1}"
  • 对包含循环引用的对象,则会报错

  • 如果要对含有非法JSON值的对象做字符串化,或者对象中的某些值无法被序列化时,需要定义 toJSON() 方法来返回一个安全的JSON值,JSON.stringify() 会先调用该方法并用它的返回值做序列化操作(toJSON() 应该返回一个可以被字符串化的安全的JSON值,而不是返回一个字符串)

    var o = {}
    var a = {
      b: 42,
      c: o,
      d: () => {}
    }
    
    // 创建一个循环引用
    o.e = a
    
    JSON.stringify(a) // TypeError: Converting circular structure to JSON
    
    a.toJSON = function() {
      return { b: this.b }
    }
    
    JSON.stringify(a) // "{"b":42}"

JSON.stringify() 可以传递两个可选参数

  • replacer :如果是数组,必须是字符串数组,包含需要序列化的属性名,其余属性会被忽略;如果是函数,会对对象本身调用一次,然后对对象中的每个属性各调用一次。如果要忽略某个键就返回 undefined ,否则返回指定的值。同时,如果遇到键对应的值也是对象,则递归调用

    var  a = {
      b: 42,
      c: '42',
      d: [1, 2, 3],
      e: {
        b: 1,
        c: 2
      }
    }
    
    JSON.stringify(a, ['b', 'c']) // "{"b":42,"c":"42"}"
    JSON.stringify(a, function (k, v) {
      if (k !== 'c') return v
    }) // "{"b":42,"d":[1,2,3],"e":{"b":1}}"
  • space :用来指定缩进格式,为整数时指定每一级缩进的字符数,为字符串时前十个字符被用于每一级的缩进

    var  a = {
      b: 42,
      c: '42',
      d: [1, 2, 3],
    }
    
    JSON.stringify(a, null, 3)
    
    // "{
    //    "b": 42,
    //    "c": "42",
    //    "d": [
    //       1,
    //       2,
    //       3
    //    ]
    // }"
    
    JSON.stringify(a, null, '-----')
    
    // "{
    // -----"b": 42,
    // -----"c": "42",
    // -----"d": [
    // ----------1,
    // ----------2,
    // ----------3
    // -----]
    // }"

ToNumber

如果需要将非数字值当做数字使用,遵循以下规则:

  • true 转化为 1
  • false 转化为 0
  • undefined 转化为 NaN
  • null 转化为 0
  • 对于字符串,遵循数字常量的相关规则/语法,处理失败返回 NaN (报错?)
  • 对象会首先被转化为基本类型值,如果非数字,则按照以上规则转化

ToPrimitive

为了将值转化为相应的基本类型值,首先会去检查该值是否有 valueOf() 方法,如果有且返回基本类型值,会使用该基本类型值进行强制类型转换。如果没有则使用 toString() 的返回值来进行强制类型转换。如果这两种方式都不返回基本类型值,会产生 TypeError 错误

使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null ,不存在 valueOf()toString() 方法,因此无法进行强制类型转换

var a = {
  valueOf: () => '42',
}

var b = {
  toString: () => '42',
}

var c = [4, 2]
c.toString = function () {
  return this.join('')
}

Number(a) // 42
Number(b) // 42
Number(c) // 42
Number('') // 0
Number([]) // 0
Number(['abc']) // NaN

ToBoolean

Javascript规范具体定义了一小撮可以被强制类型转换为 false 的值,其余的则都是 true

假值列表:

  • undefined
  • null
  • false
  • +0 -0 NaN
  • ''

显式强制类型转换

字符串和数字

字符串和数字的互相转换是通过 String()Number() 两个内建函数来实现的

var a = 42
var b = String(a) // '42'

var c = '3.14'
var d = Number(c) // 3.14

// 注意这里没有new关键字,不会创建封装对象

一些其他的方式:

  • +

    var a = 42
    var b = a.toString() // '42'
    
    var c = '3.14'
    var d = +c // 3.14
    
    // 不推荐用一元运算符来显式强制类型转换,会导致代码难以理解
    // 同时 + 还可以将日期对象转换为时间戳,同样不推荐
    
    var e = new Date('Mon, 18 Aug 2014 08:53:06 CDT')
    +e // 1408369986000
  • ~:位操作符会通过抽象操作 ToInt32 强制操作数使用32位格式,位非 ~x 基本等于 -(x+1) 。对于正数,位反后拿到的是补码,需要转换为原码。对于负数,先取补码再位反。(MDN例子)

    补码的存在是为了正确计算负数的加法(借助溢出),以8位有符号位数字举例

    正数的补码是其本身,负数的补码符号位不变,其余按位取反并+1

    补码转换为原码同样是符号位不变,其余按位取反并+1

    原码 补码
    1 + (-2) 00000001
    +
    10000010
    1000011 = -3
    00000001
    +
    11111110
    11111111 = -1
    1 + (-1) 00000001
    +
    10000001
    1000010 = -2
    00000001
    +
    11111111
    00000000 = 0(溢出8位,从0开始)

    -(x+1) 中唯一能得到 0x 值是 -1,这是个哨位值,通常被赋予特殊含义,例如字符串 indexOf() 方法在没有找到相应字符串时返回 -1

    var a = 'Hello World'
    
    if (a.indexOf('lo') >= 0) {
      // 写法不好,抽象渗漏(暴露底层实现细节)
    }
    
    if (a.indexOf('lo') != -1) {
      // 写法不好,抽象渗漏(暴露底层实现细节)
    }
    
    if (~a.indexOf('lo')) {
      // 将结果强制类型转换为真假值!
    }

显式解析数字字符串

解析字符串中的数字,和将字符串强制类型转换为数字是有区别的:解析允许字符串中含有非数字字符,而转换不允许

var a = '42'
var b = '42px'

Number(a) // 42
parseInt(a) // 42
Number(b) // NaN
parseInt(b) // 42

ES5之前,parseInt() 如果不传递第二个参数指定进制,会自行根据字符串的第一个字符来决定进制。除此之外,不要传递非字符串的其他参数,如果传入参数非字符串,会先隐式转换为字符串

parseInt(1 / 0, 19) // parseInt('Infinity', 19) 第一个字符为'I',以19位基数时值为18,第二个字符不是有效数字,解析结束
parseInt(0.000008) // 0
parseInt(0.0000008) // 8 '8e-7'
parseInt(false, 16) // 250 'fa' = 250
parseInt(parseInt, 16) // 15 'f' 来自于function () {...}
parseInt('0x10') // 16
parseInit('103', 2) // 2 到3停止,因为3不是有效的二进制数字

显式转换为布尔值

Boolean() (不带new)是显式的 ToBoolean 强制类型转换

var a = '0'
var b = []
var c = {}

var d = ''
var e = 0
var f = null
var g

Boolean(a) // true
Boolean(b) // true
Boolean(c) // true

Boolean(d) // false
Boolean(e) // false
Boolean(f) // false
Boolean(g) // false

但这种方式并不常用,一元运算符 ! 也可以将值显式强制类型转换为布尔值,因为 ! 还会反转真假值,因此最常用的方式是 !! ,在 if 语句中,会自动隐式的进行 ToBoolean 转换,建议用显式的方式让代码更易理解

隐式强制类型转换

因人而异,对于自己来说不够明显的强制类型转换都可以称作隐式强制类型转换,会导致代码晦涩难懂。但从另一个角度来看,也可以减少代码的冗余,让代码更简洁

字符串和数字之间的隐式强制类型转换

根据ES5规范,如果 + 的某个操作数是字符串或者能够通过以下步骤转换为字符串,则进行拼接操作:

  • 对其进行 ToPrimitive 抽象操作
  • 调用 [[DefaultValue]]

否则执行数字加法

var a = '42'
var b = '0'

a + b // '420'

var c = 42
var d = 0

c + d // 42

var e = [1, 2]
var f = [3, 4]

e + f // '1,23,4' 对数组进行ToPrimitive抽象操作,发现valueOf()方法不能返回基本类型值,转而使用toString()

通过 + 空字符串来隐式强制类型转换数字为字符串的场景非常多见,但和显式强制类型转换略有不同:

  • a + '' 依据 ToPrimitive 抽象操作规则,先对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串
  • String(a) 则是直接调用 ToString 抽象操作
var a = {
  valueOf: () => 42,
  toString: () => 4
}

a + '' // '42'
String(a) // '4'

相应的,- * / 都是数字运算符,因此会将操作数强制类型转化为数字。

var a = 1
var b = '2'

b - a // 1
a * b // 2
b / a // 2

var c = [3]
var d = [1]

c - d // 2 c和d首先被转化为字符串,然后再转化为数字

布尔值到数字的隐式强制类型转换

将某些复杂的布尔逻辑转化为数字加法的时候可以派上大用场,但是情况非常罕见

function onlyOne() {
  var sum = 0
  for (var i = 0; i < arguments.length; i++) {
    if (arguments[i]) {
      sum += arguments[i]
    }
  }
  return sum == 1
}

var a = true
var b = false

onlyOne(b, a) // true
onlyOne(b, a, b, b, b, b) // true

隐式强制类型转换为布尔值

  1. if() 语句中的条件判断表达式
  2. for(..; ..; ..) 语句中的条件判断表达式(第二个)
  3. while()do..while() 循环中的条件判断表达式
  4. ?: 语句中的条件判断表达式
  5. 逻辑运算符 ||&& 的左操作数

对于javascript中的逻辑运算符(实际上更像是操作数选择器运算符),并不是返回一个布尔值,而是返回两个操作数中的一个。 ||&& 首先对第一个操作数进行 ToBoolean 抽象操作,然后再执行条件判断。对于 || ,如果第一个操作数判断结果为 true 则返回第一个操作数的值,为 false 则返回第二个操作数的值。而对于 && ,如果第一个操作数判断结果为 true 就返回第二个操作数的值,为 false 则返回第一个操作数的值

之所以我们在 if 或其他条件判断语句中可以用 &&||,是因为这些语句本身会做隐式强制类型转换

宽松相等和严格相等

常见的误区:“ == 检查值是否相等,=== 检查值和类型是否相等”。正确的解释是:“ == 允许在相等比较中进行强制类型转换,而 === 不允许。”延伸出来的性能结论:实际上 ===== 在比较过程中做的事情更多(相比误区说法),所以性能上确实会慢上一点点(可以忽略不计)

抽象相等

ES5规范如下:

  • 如果两个值类型相同,就仅比较他们是否相等(NaN 不等于 NaN+0 等于 -0
  • 对象指向同一个值时,视为相等,不发生强制类型转换
  • == 在比较两个不同类型的值时,会发生隐式强制类型转换,将其中之一或两者都转换为相同类型再比较,转换规则如下:
    • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果,反之亦然
    • 如果 Type(x) 是布尔值,则返回 ToNumber(x) == yType(y) 亦然),因此不要使用布尔值的宽松相等,会和预期完全不符
    • nullundefined 在比较中可以相互强制类型转换,两者等价
    • 如果 Type(x) 是字符串或数字,Type(y) 对象,则返回 x == ToPrimitive(y) 的结果,反之亦然

比较少见的情况

更改对象原型

Number.prototype.valueOf = () => 3
new Number(2) == 3 // true
// 因为数字对象拆封涉及到ToPrimitive操作

假值的相等比较

false == 0 // true
// false优先转换为0,即 0 == 0
false == '0' // true
// false优先转换为0,即 0 == '0',两个操作数分别是数字和字符串,转换为 0 == Number('0')
false == '' // true
// false优先转换为0,即 0 == '',两个操作数分别是数字和字符串,转换为 0 == Number('')
false == [] // true
// false优先转换为0,[]通过ToPrimitive转换为''
[] == ![] // true
// 右侧进行了布尔值的显式强制类型转换,即 [] == false

总结:

  • 如果比较的两个值存在布尔值,千万不要使用 ==
  • 如果比较的两个值存在 0 '' [] ,尽量不要使用 ==
  • 干脆就不要使用 == (个人理解)

抽象关系比较

根据ES5规范,首先对比较双方调用 ToPrimitive 操作

  • 如果结果出现非字符串,就根据 ToNumber 将比较双方强制类型转换为数字比较
  • 如果比较双方都是字符串,则按照字母顺序来比较

这里原本书中的例子有问题

var a = 42 
var b = '43'

a < b // true (42 < 43)

var a = ['42']
var b = ['043']

a < b // false ('42' < '043',在字母顺序上,0小于4)

需要注意,根据规范,a <= b 会被处理成 b < a 再将结果反转。所以实际上JavaScript中 <= 是不大于的意思(而不是通常理解的小于等于),>= 同理。这会产生一些悖论:

var a = {}
var b = {}

a < b // false 都是 '[object object]'
a == b // false 指向地址不是一致的!
a > b // false

a <= b // true
a >= b // true

相等比较有严格相等,但是关系比较却没有严格关系比较,想要避免在关系比较中出现隐式强制类型转换,只能确保比较两者类型相同,别无他法

《鸟哥的Linux私房菜(基础篇)》第2章 主机规划和磁盘分区

硬件

硬件支持

Red Hat 硬件支持
openSUSE 硬件支持
Linux 对笔记本电脑的支持

常见设备在Linux中的文件名

设备 在Linux中的文件名
SCSI、SATA、USB /dev/sd[a-p]
Virtio接口 /dev/vd[a-p](虚拟机内)
软盘驱动器 /dev/fd[0-7]
鼠标 /dev/input/mouse[0-15]
/dev/psaux(PS/2接口)
/dev/mouse(当前鼠标)
CD-ROM、DVD-ROM /dev/scd[0-1](通用)
/dev/sr[0-1](通用,CentOS常见)
/dev/cdrom(当前CD-ROM)
磁带机 /dev/ht0(IDE接口)
/dev/st0(SATA/SCSI接口)
/dev/tape(当前磁带)

虚拟机相关

VirtualBox官网
Fedora官网教程

磁盘分区

MBR(Master Boot Record)

早期的硬盘第一个扇区(512B)包涵启动引导程序(446B)和分区表(64B)
分区表记录整个硬盘分区状态,64B最多仅能有四组记录区,每组记录区记录了该区的起始与结束柱面号码

假设硬盘设备文件名为 /dev/sda 那么这4个分区文件名在Linux系统中如下:

  • P1: /dev/sda1
  • P2: /dev/sda2
  • P3: /dev/sda3
  • P4: /dev/sda4

最初的四组分区记录,可以设置为主要分区和扩展分区(最多一个),而扩展分区可以继续拆分为逻辑分区
扩展分区的目的是用额外的扇区来记录分区信息,本身并不能拿来格式化
假设初始分区分为:P1(Primary) P2(Extended),同时P2拆分为 L1、L2、L3(logical partition)
则在Linux系统中,设备文件名如下:(前面四个号码都是保留给主要分区和扩展分区使用的)

  • P1: /dev/sda1
  • P2: /dev/sda2
  • L1: /dev/sda5
  • L2: /dev/sda6
  • L3: /dev/sda7

总结:

  • 主要分区和扩展分区最多可以有4个;
  • 扩展分区最多有1个;
  • 逻辑分区是由扩展分区继续划分的分区;
  • 能够格式化后存储数据的是主要分区和逻辑分区;
  • 逻辑分区的数量限制依操作系统不同而不同
  • 无法使用2.2TB以上的磁盘容量;
  • MBR仅有一个扇区、若损坏,经常无法修复
  • MBR内存放的启动引导程序仅446B,无法存储较多的程序代码

GPT(GUID partition table)

目前已经有4K的扇区设计出现,为了兼容所有硬盘,在扇区的定义上,会使用逻辑区块地址(LBA, Logical Block Address)
GPT将磁盘所有区块以LBA来规划,使用了34个LBA区块来记录分区信息,同时将磁盘最后34个LBA区块用来做备份

  • LBA0(MBR兼容区块)存储了第一阶段的启动程序,用一个特殊标识符来表示此磁盘为GPT格式
  • LBA1(GPT表头记录)记录分区表本身的位置和大小、备份区块的位置、分区表的校验码(CRC32)
  • LBA2-33(实际记录的分区信息)每个LBA可以记录4组分区记录,所以默认总共可以有 4 * 32 = 128 组分区记录

BIOS和UEFI

BIOS搭配GPT/MBR启动流程:

  • BIOS:启动主动执行的固件,会认识第一个可启动的设备;
  • MBR:第一个可启动设备的第一个扇区内的主引导记录块,内涵启动引导代码;
  • 启动引导程序(boot loader):一个可读取内核文件来执行的软件;
  • 内核文件:开始启动操作系统

Boot loader的主要任务:

  • 提供选项:用户可以选择不同启动项,这也是多重引导的重要功能;
  • 加载内核文件:直接指向可执行的程序来启动操作系统;
  • 转交给其他启动引导程序(多系统):启动引导程序除了可以安装在MBR以外,还可以安装在每个分区的启动扇区;

UEFI BIOS(Unified Extensible Firmware Interface, 统一可扩展固件接口)搭配GPT/MBR启动流程:

相比于传统的BIOS,UEFI更像一个小型操作系统。某些时候,需要将UEFI的安全启动功能(secure boot)关闭,才能正常启动Linux系统。虽然UEFI可以直接获取GPT分区表,但是最好有BIOS boot分区。为了与windows兼容,并且提供其他第三方厂商使用的UEFI存储空间,需要格式化一个FAT格式的文件系统分区,通常512MB - 1GB

Linux安装时,磁盘分区的选择

文件系统和目录树的关系

将磁盘分区的数据防止在某个目录下,进入该目录就可以读取分区,这个操作被称为“挂载”。整个Linux系统最终要的就是根目录 root(/) ,因此根目录一定会挂载个某个分区,其他目录则可以挂载在不同的分区。可以通过对路径反向追踪来判断某个文件在哪个分区:即哪一级目录先被查到是挂载点,则该文件属于这个挂载点对应的分区

挂载点与磁盘分区的规划

懒人划分:

  • 划分“/”以及“交换分区”
  • 预留一些磁盘容量作为备用

麻烦一点的划分:根据主机服务来确定硬盘的规划

主机服务规划与硬件关系

  • NAT(IP分享器):Linux NAT可以额外安装一些分析软件,控制带宽或者流量。通常对网卡要求较高
  • SAMBA(网络邻居):没有客户端数量限制、适合用于小型环境的文件服务器。通常需要注意网卡和硬盘的性能,/home
    这个目录可以考虑独立出来挂载,并加大容量
  • Mail(邮件服务器):通常需要注意网卡和硬盘的性能,/var这个目录可以考虑独立出来挂载,并加大容量
  • Web(www服务器):通常需要考虑CPU和内存的性能
  • DHCP(提供客户端自动获取IP的功能):对硬件要求不高
  • FTP:通常需要注意网卡和硬盘的性能

《你不知道的javascript(中)》1.3 原生函数

内部属性[[Class]]

所有 typeof 返回为 'object' 的对象都包含一个内部属性 [[Class]] ,这个属性无法直接访问,一般通过 Object.prototype.toString 查看

多数情况下,内部 [[Class]] 属性和创建该对象的内建原生构造函数相对应

Object.prototype.toString.call([1]) // "[object Array]"
Object.prototype.toString.call(/^&/) // "[object RegExp]"

虽然 Null()Undefined() 这样的原生构造函数并不存在,但是内部的 [[Class]] 属性仍然是 'Null''Undefined'

Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"

其他基本类型值的情况有所不同,通常称为“包装”,即被各自的封装对象自动包装

Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(true) // "[object Boolean]"

封装对象包装

由于基本类型值既没有属性也没有方法,需要通过封装对象才能访问,例如 .length 或者 .toString() 。此时JavaScript会自动为基本类型值 wrap 一个封装对象,并在操作完成后重新拆封成基本类型

var a = 'abc'
a.length // 3
a.toUpperCase() // 'ABC'
a // 'abc'

通常不建议直接使用封装对象,浏览器已经为 .length 这样的常见情况作了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率

使用封装对象时还需特别注意假值情况,因为对象始终是真值

var a = new Boolean(false)
if(!a) {
  console.log('xxx') // 不会执行到这里
}

如果想要自行封装基本类型值,除了使用对应的构造函数,还可以通过 Object() (不带new关键字)

var a = 'abc'
var b = new String(a)
var c = Object(a)

typeof b // 'object'
typeof c // 'object'

b instanceof String // true
c instanceof String // true

Object.prototype.toString.call(b) // '[object String]'
Object.prototype.toString.call(c) // '[object String]'

拆封

可以使用 valueOf() 函数来获得封装对象中的基本类型值

var a = new String('abc')
var b = new Number(42)
var c = new Boolean(true)

a.valueOf() // 'abc'
b.valueOf() // 42
c.valueOf() // true

如果需要用到封装对象中的基本类型值,会发生隐式拆封(强制类型转换)

var a = new String('abc')
var b = a + 'd'

b // 'abcd'
typeof a // 'object'
typeof b // 'string'

原生函数作为构造函数

对于数组、对象、函数、正则表达式,使用字面量和使用构造函数来创建没有任何区别,除非十分必要,否则应该避免使用构造函数来创建,因为它们经常产生意想不到的结果

构造函数 Array() 不要求必须带 new 关键字,不带时,它会被自动补上。同时,如果只传入一个数字参数,该参数会被作为数组的预设长度。问题是,在JavaScript中数组没有预设长度这个概念。如若一个数组没有任何单元,但 length 属性却显示有单元数量,会导致一些怪异的行为(不同版本不同浏览器的控制台显示结果也不尽相同)

var a = Array(3)
var b = [undefined, undefined, undefined]
a // [empty × 3] 最新版chrome
b // [undefined, undefined, undefined]

a.join('-') // "--"
b.join('-') // "--"
a.map((v, i) => i) // [empty × 3]
b.map((v, i) => i) // [0, 1, 2]

a.map() 之所以执行失败,是因为数组中并不存在任何单元,而 a.join() 不同,它会根据 length 的值来遍历数组中的元素。总之,永远不要创建和使用“稀疏数组”(包含至少一个空单元的数组)。

可以通过如下方式来创建包含 undefined 单元的数组(而非稀疏数组)

var a = Array.apply(null, { length: 3 })
a // [undefined, undefined, undefined]

原理如下:

  1. Array.apply() 调用 Array() ,第一个参数作为上下文传入了 null 。第二个参数传入了一个类数组对象(array-like object){ length: 3 }
  2. 设想 apply() 在内部有一个 for 循环,从 0 开始循环到 length (即 0 1 2)
  3. 假设在 apply() 内部该数组参数名为 arrfor 循环就去参数中取 arr[0] arr[1] arr[2] 。然而,由于 { length: 3 } 中并没有数字键对应的值,得到的结果都是 undefined
  4. apply() 将循环结束后获得的参数传入 Array() 得到 Array(undefined, undefined, undefined)

对于 Object() Function() RegExp() 来说,仅有需要动态定义正则表达式时才需要用到构造方式来创建,其余场景无论是从可读可维护性或是性能上来看,构造方式创建都不如用字面量创建

var name = 'Kyle'
var namePattern = new RegExp('\\b' + name + '\\b', 'ig')

var txt = 'kyle xxx'
namePattern.test(txt) // true

相比于其他原生构造函数,Date() 和 Error() 的使用场景要多很多,因为它们没有对应的字面量形式。创建一个错误对象可以获得当前运行栈的上下文 .stack 属性,包含函数调用栈以及错误代码行号等重要信息

function foo() {
  bar()
}

function bar() {
  var err = new Error('error!')
  console.log(err.stack)
}

foo()
// Error: error!
//     at bar (<anonymous>:6:13)
//     at foo (<anonymous>:2:3)
//     at <anonymous>:10:1

在ES6中新加入了一个基本类型 Symbol(符号) ,符号是具有唯一性的特殊值(并非绝对),可以用它作为对象的属性键从而避免命名重复。ES6中有一些预定义符号,以 Symbol 的静态属性形式出现,如 Symbol.iterator ,它们作为键时有特殊用途

var myIterable = {}
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
}
// 给myIterable定义一个迭代器
[...myIterable] // [1, 2, 3]

可以使用原生构造函数来自定义符号,但是需要注意不能带 new

var mySymbol = Symbol('my symbol')
mySymbol // Symbol(my symbol)
mySymbol.toString() // 'Symbol(my symbol)'

var obj = {}
obj[mySymbol] = 'foo'
Object.getOwnPropertySymbols(obj) // [Symbol(my symbol)]

一些原生构造函数的原型并非普通对象

typeof Function.prototype // "function"
Function.prototype() // 空函数

RegExp.prototype.toString() // "/(?:)/" 空正则表达式

Array.isArray(Array.prototype) // true

可以考虑将原型作为声明变量时的默认值,这样的好处是,多处变量创建的默认值仅会被创建一次。但是需要注意,如果默认值随后会被更改,则不要使用这种方式,会引起一系列问题

var arr = arr || Array.prototype
arr.push(1)

Array.prototype // [1]

《你不知道的javascript(上)》第二部分 this和对象原型

关于this

定义

this 实际上是在函数被调用时动态发生的绑定,它指向什么完全取决于函数在哪里被调用

this指向优先级

  1. new 操作符(指向新创建对象)
  2. 显式绑定(call、apply、bind)
  3. 隐式绑定(foo.bar.methods() 指向bar)
  4. 默认绑定(指向window或undefined)

柯里化的问题

当显式绑定传入第一个参数为null或者undefined的时候,this会被绑定至全局

function foo() {
  console.log(this.a);
}

var a = 2;

foo.call(null); // 2

当使用硬绑定来实现柯里化的时候,被绑定函数内部如果使用this,会导致操作全局对象

function foo(a, b) {
  this.a = 1;
  console.log(a + b);
}

var bar = foo.bind(null, 2);

bar(3); //5

console.log(a); // 1

一个好的处理方式是:需要在显式绑定忽略this时,传入 DMZ

function foo(a, b) {
  this.a = 1;
  console.log(a + b);
}

var ø = Object.create(null); // 不会创建Object.prototype委托,比{}更空

var bar = foo.bind(ø, 2);

bar(3); //5

console.log(a); // ReferenceError

软绑定

  1. 避免函数引用时this会被绑定至全局或undefined
  2. 保留隐式和显式修改this的能力
if (!Function.prototype.softBind) {
  Function.prototype.softBind = function (obj) {
    var fn = this;
    var curried = [].slice.call(arguments, 1);
    var bound = function () {
      return fn.apply(
        (!this || this === (window || global)) ? obj : this,
        curried.concat.apply(curried, arguments)
      );
    };

    bound.prototype = Object.create(fn.prototype);

    return bound;
  }
}

function foo() {
  console.log(this.name);
}

var obj = { name: 'obj' },
    obj2 = { name: 'obj2' },
    obj3 = { name: 'obj3' };

var bar = foo.softBind(obj);

bar(); // obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // obj2
bar.call(obj3); // obj3
setTimeout(obj2.foo, 10); // obj

箭头函数

根据当前的词法作用域来决定this,继承外层函数调用的this指向
优先级高于new

function foo() {
  setTimeout(() => {
    console.log(this.a);
  }, 100);
}

foo.call({
  a: 1
}); // 1

对象

万物皆对象的说法是错误的

基本类型和对象子类型(或称内置对象,比如String)是不等同的,javascript会在操作基本类型的时候自动将之转化为内置对象

var str = 'abc';
var strObj = new String('abc');

console.log(str === strObj); // false
console.log(typeof str); // string
console.log(typeof strObj); // object
console.log(str instanceof String); // false
console.log(strObj instanceof String); // true

基本类型中:boolean、string、number字面量与对应的内置对象不等同,null和undefined没有构造形式,而Object、Array、Function和RegExp无论使用字面量还是构造形式来声明,他们都是对象。

对象内容的存储

对象内容并不是存储在对象内部,而是通过属性名(键)来指向内存某一位置的值

可计算属性名

最常用的场景是ES6的Symbol,因为Symbol不透明且无法预测

const key = Symbol('key');
const obj = {
  [key]: 1,
};

console.log(obj[key]);

Object.assign

Object.assign内部就是使用=操作符来赋值,以此实现浅拷贝。因此原对象属性的一些特性(writable等)不会被复制到目标对象

Object.defineProperty

var myObj = {};

Object.defineProperty(myObj, 'a', {
  value: 2,
  // 是否可写,如果改为false,修改值不生效,严格模式下报错 TypeError
  writable: true, 
  // 是否可配置,如果改为false,不可再通过Object.defineProperty来配置descriptor,也不能删除此属性。否则 TypeError
  configurable: true, 
  // 是否可枚举,如果改为false,不会出现在对象遍历中(如for...in)
  enumerable: true, 
});

如果目标对象引用了其他对象,这些属性特性的声明不会对子对象的内容产生作用

Object.preventExtensions()

var obj = {};

Object.preventExtensions(obj);

obj.a = 1; // 严格模式下报错
obj.a // undefined

Object.seal()

等于对现有对象调用Object.preventExtensions(),同时把所有现有属性设置为 configurable: false,即只能修改已有属性的值

Object.freeze()

等于对现有对象调用Object.seal(),同时把所有现有属性设置为 writable: false

getter 和 setter

当给一个属性定义getter,或setter或两者都有时,这个属性会被定义为”访问描述符“(区别于”数据描述符“),javascript会忽略它的value和writable特性,只关心set、get、configurable和enumerable(?实测会报错,不能同时设置get和value、writable)

var obj = {
  get a() {
    return 2;
  },
  _b_: 3,
};

Object.defineProperty(obj, 'b', {
  get() {
    return this._b_;
  },
  set(val) {
    this._b_ *= val;
  },
  enumerable: true,
});

console.log(obj.a); // 2
console.log(obj.b); // 3
obj.b = 3;
console.log(obj.b); // 9

in 和 Object.hasOwnProperty

in 操作符会检查属性名是否存在于对象或原型链中,而 Object.hasOwnProperty 只会检查属性是否存在对象中

propertyIsEnumerable

会检查属性名是否直接存在于对象上且可枚举

Object.keys

返回所有可枚举属性名的数组

Object.getOwnPropertyNames

返回一个数组,包含所有在对象上的属性,无论是否可枚举

iterator

var array = [1, 2, 3];
var it = array[Symbol.iterator]();

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

Symbol.iterator 会指向一个'返回迭代器对象'的函数

for...of

for...of 会找到对象的 iterator 来处理遍历(每次调用next方法)
数组天生自带 iterator ,所以可以被for...of遍历,但是普通对象不行,可以手动给想遍历的对象定义 iterator

var obj = {
  a: 1,
  b: 2,
  c: 3,
};

Object.defineProperty(obj, Symbol.iterator, {
  enumerable: false,
  writable: false,
  configurable: true,
  value: function () {
    var o = this;
    var index = 0;
    var keys = Object.keys(o);
    return {
      next: function () {
        return {
          value: o[keys[index++]],
          done: index > keys.length
        }
      }
    }
  }
});

for (var item of obj) {
  console.log(item); // 1, 2, 3
}

类和原型

类是一种设计模式

javascript的机制其实和类完全不同,虽然可以用一般意义上的类来理解(比如实例化,继承等),但是javascript所做的一切本质上只是在操作对象

属性设置和屏蔽

obj.foo = 'bar';
  1. 如果 obj 对象中包含名为 foo 的普通数据访问属性名,则这条赋值语句会修改已有属性值
  2. 如果 foo 存在原型链的上层,并且没有被标记为只读(writable: true),会在 obj 上新增一条屏蔽属性
  3. 如果 foo 存在原型链的上层,并且被设置为只读(writable: false),这条赋值语句会被忽略,严格模式下报错
  4. 如果 foo 存在原型链的上层,并且它是一个 setter ,则会调用这个 setter ,不会在 obj 上产生屏蔽属性,也不会重新定义 setter

如果希望在3、4也屏蔽上层foo,就不能使用=操作符来赋值,可以通过Object.defineProperty()来添加 foo 属性

javascript”实例化“本质

在面向类的语言中,类可以被复制多次(类似用模具做东西)。但是在javascript中,并没有类似的复制方式,不能创建一个类的多个实例,只是创建了多个对象。它们的 prototype 关联的是同一个对象(引用),在默认情况下并不会复制。因此,在javascript中,”实例化的本质“是 —— 让两个对象相关联

new 操作符实际上并没有直接创建关联,这个关联只是个副作用(?!)

prototype.constructor

鉴于上述javascript实例化的本质,因此不能信任这条属性,不应该把它理解为”由...构造“。

function Foo() {

}

Foo.prototype = {};

var obj = new Foo();

console.log(obj.constructor === Object); // true

寻找路径:
obj不存在 constructor -> Foo.prototype不存在 constructor -> Object.prototype的 constructor 指向 Object
修正这种行为,必须显式的定义Foo.prototype.constructor

关于继承

重新指定 prototype.constructor 时

Bar.prototype = Foo.prototype; 
// 直接引用而不是创建新对象,会导致修改父类原型
Bar.prototype = new Foo();
// 基本能满足要求,但是有一些副作用:丢弃原本原型,同时调用constructor,导致原型属性变成对象属性
Bar.prototype = Object.create(Foo.prototype); 
// 正确做法(非原型属性用 Foo.call(this) 来继承),唯一缺点是会丢弃原本的原型,需要重新指定constructor

// es6
Object.setPrototypeOf(Bar.prototype, Foo.prototype); // 不需要抛弃原本的原型对象

内省

a instanceof Foo // 在a的整条链中是否存在Foo.prototype,只能处理对象和函数的关系

Foo.prototype.isPrototypeOf(c); // 不用类的角度来解释,而是直接关注对象之间的关系
b.isPrototypeOf(c)

Object.getPrototypeOf(a); // 直接获取
function Foo() { }

function Bar() { }

function Baz() { }

Object.setPrototypeOf(Bar.prototype, Foo.prototype);
Object.setPrototypeOf(Baz.prototype, Bar.prototype);

const a = new Baz();

console.log(a instanceof Baz); // true
console.log(Baz.prototype.isPrototypeOf(a)); // true
console.log(Object.getPrototypeOf(a) === Baz.prototype); // true 
console.log(Object.getPrototypeOf(Object.getPrototypeOf(a)) === Bar.prototype); // true
console.log(a.__proto__.__proto__.__proto__ === Foo.prototype); // true es6之前非标准

__proto__实际上更像一个getter/setter,并不存在当前对象中,而是存在 Object.prototype 中。

// 模拟一个 .__proto__
Object.defineProperty(Object.prototype, '__proto__', {
  get: function () {
    return Object.getPrototypeOf(this);
  },
  set: function (o) {
    Object.setPrototypeOf(this, o);
    return o;
  }
});

委托

面向委托的设计模式

在javascript中,用委托代替继承来解释原型链更为恰当,因为本质上prototype的机制就是对象之间的关联关系

委托理论

var task = {
  setId: function (id) {
    this.id = id;
  },
  outputId: function () {
    console.log(this.id);
  }
};

var subTask = Object.create(task); // 让subTask委托task

console.log(Object.getPrototypeOf(subTask)); // task

subTask.prepareTask = function (id, label) {
  this.setId(id);
  this.label = label;
};

subTask.outputTask = function () {
  this.outputId();
  console.log(this.label);
};

subTask.prepareTask(1, 2);
subTask.outputTask(); // 1, 2

Object.create 会创建一个新对象,并把它的__proto__委托到我们指定的对象

原型风格和对象关联风格

原型风格

function Foo(who) {
  this.me = who;
}

Foo.prototype.identify = function () {
  return `i am ${this.me}`;
}

function Bar(who) {
  Foo.call(this, who);
}

Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function () {
  alert(`hello, ${this.identify()}`);
}

var b1 = new Bar('b1');
var b2 = new Bar('b2');

b1.speak(); // hello, i am b1
b2.speak(); // hello, i am b2

对象关联风格

const Foo = {
  init: function (who) {
    this.me = who;
  },
  identify: function () {
    return `i am ${this.me}`
  }
};

const Bar = Object.create(Foo);

Bar.speak = function () {
  alert(`hello, ${this.identify()}`);
}

var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');

b1.speak(); // hello, i am b1
b2.speak(); // hello, i am b2

关于对象关联风格的思考:乍一看 b1 的初始化变为了两个步骤,需要更多代码。但其实这也是一个优点,把构造和初始化分离,在某些情况下更灵活

《你不知道的javascript(中)》附录A

ECMAScript Annex B

ECMAScript规范包含了Annex B,其中介绍了浏览器兼容性与官方规范的差异,应该注意避免使用

主要如下:

  • 非严格模式下,允许八进制数值常量,如 0123(十进制83)
  • window.escape()window.unescape() 可以将字符串转义和回转成带有 % 的十六进制转义序列,如 window.escape('?') 的结果是 %3F,此特性已废弃
  • String.prototype.substr()String.prototype.substring() 十分相似,前者的第二个参数是截取长度,而后者是结束索引。前者没有被严格废弃,但是应该避免使用

同时,Web ECMAScript规范中介绍了目前浏览器因为兼容性考虑,实现的一些内容(并未包含在官方规范内),同样应该避免使用

  • <!-- --> 在浏览器中是合法的
  • String.prototype 中包含HTML格式字符串的附加方法,如 anchor()、big()... 等等
  • RegExp 扩展 RegExp.$1 .. RegExp.$9RegExp.lastMatch/RegExp["$&"] (匹配组和最近匹配),同样是非标准
  • Function.prototype.argumentsarguments.caller 均已废止,Function.caller 非标准

宿主对象

var a = document.createElement('div')
typeof a // 'object'
Object.prototype.toString.call(a) // '[object HTMLDivElement]'

上例中,a 不仅仅是一个对象,还是个特殊的宿主对象,其内部的 [[Class]] 值来自预定义的属性,不可更改。需要注意如下问题:

  • 一些宿主对象在强制转化为 boolean 时会意外的成为假值或真值,需要注意
  • 无法访问正常的 object 内建方法,比如 toString()
  • 无法写覆盖
  • 包含一些预定义属性
  • 包含无法将 this 重载为其他对象的方法
  • 其他...

除此之外, console 及其各种方法也是宿主环境提供

全局DOM变量

一个不太为人所知的现象:创建带有id的DOM元素时,也会创建同名的全局变量

<div id="app"></div>
console.log(app) // HTML元素

因此尽量不要使用全局变量

原生原型

除非必要,否则不要扩展原生原型。其次,扩展时必须加入判断条件:

if (!Array.prototype.foobar) {
  Array.prototype.foobar = function () {
    this.push('foo', 'bar')
  }
}

这种情况一般称之为 polyfill/shim

<script>

多个 <script> 标签中的JavaScript代码是如何运行的:

  • 共享 global 对象(浏览器中是 window
  • 全局变量作用域提升机制在这些边界中不适用
<script>foo()</script>
<script>
  function foo () {
    // 报错
  }
</script>

保留字

保留字分为四类:关键字( function, switch等 )、预留关键字( enum等 )、null 常量和 true/false 布尔常量

一首关于保留字有趣的小诗:

Let this long package float,
Goto private class if short.
While protected with debugger case,
Continue volatile interface.
Instanceof super synchronized throw,
Extends final export throws.

Try import double enum?
-False, boolean, abstract function,
Implements typeof transient break!
Void static, default do,
Switch int native new.
Else, delete null public var
In return for const, true, char
...Finally catch byte

这首诗中也包含了一些ES3中的保留字(byte、long)等,它们在ES5中已经不再是保留字

在ES5之前,保留字也不能用作对象属性名或键,目前已经没有这个限制

实现中的限制

JavaScript规范对于函数中参数个数,字符串常量的长度等没有限制,但是由于JavaScript引擎实现各异,会导致一些限制:

  • 字符串常量允许的最大字符数
  • 传入函数的参数数据大小(也称为栈大小,以字节为单位)
  • 函数声明中的参数个数
  • 未经优化的调用栈,即函数调用链的最大长度
  • JavaScript程序以阻塞方式在浏览器中运行的最长时间(秒)
  • 变量名的最大长度
function addAll () {
  var sum = 0
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i]
  }
}

var nums = []
for (var i = 1; i < 100000; i++) {
  nums.push(i)
}

addAll(2, 4, 6) // 12
addAll.apply(null, nums) // 499950000
// 在一些引擎中会报错

《鸟哥的Linux私房菜(基础篇)》第6章 Linux的文件与目录管理

目录的相关操作

特殊的目录

  • . :代表当前目录
  • .. :代表上一层目录,根目录的上层目录还是根目录
  • - :代表前一个工作目录
  • ~ :代表当前用户的家目录
  • ~account :代表用户account的家目录

cd(change directory, 切换目录)

可以使用绝对或相对路径

pwd(print working directory, 显示目前目录)

$ pwd [-P]    #参数 -P 可以显示链接文件的真正路径

mkdir(make directory, 新建目录)

$ mkdir [-mp] dirname    #参数 -m 可以额外设置目录权限,参数 -p 是指递归创建

rmdir(remove directory, 删除空目录)

$ rmdir [-p] dirname    #参数 -p 递归删除上层空目录

关于执行文件路径的变量:$PATH

当在Linux中执行命令的时候,系统会根据PATH的设置去每一个定义的目录下查找第一个可执行的命令。这个变量的内容是由一堆目录所组成,每个目录之间用 : 隔开

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

文件和目录管理

ls

$ ls [-aAdfFhilnrRSt] [--color={never,auto,always}] [--full-time] dirname/filename
  • -a :全部文件,包括隐藏文件
  • -A :全部文件,但不包括 ... 两个目录
  • -d :仅列出目录
  • -f :直接列出结果,不进行排序
  • -F :根据文件类型、目录附加额外的信息 * 可执行 / 目录 = socket文件 | FIFO文件
  • -h :格式化文件容量,带上单位
  • -i :列出inode号码
  • -l :详细信息,包含属性权限等
  • -n :列出UID与GID而非文件拥有者、用户组
  • -r :将排序结果反向输出
  • -R :递归列举子目录及文件
  • -S :以文件大小排序
  • -t :以时间排序
  • --color=never :不依据文件特性给与颜色
  • --color=always :依据文件特性显示颜色
  • --color=auto :让系统自行判断是否给与颜色
  • --full-time :以完整时间模式输出
  • --time={atime, stime} :输出access时间或改变权限属性的时间

cp

$ cp [-adfilprsu] source destination
$ cp [-adfilprsu] source1 source2 source3 ... directory
  • -a :相当于 -dr --preserve=all
  • -d :若源文件是链接文件,则复制链接文件属性而非文件本身
  • -f :若目标文件已经存在且无法开启,则删除后再试一次
  • -i :若目标文件已经存在,覆盖前会先询问
  • -l :建立硬链接(hard link),而非复制文件本身
  • -p :连同文件的属性一起复制,而非使用默认属性
  • -r :递归复制
  • -s :复制成符号链接文件(symbolic link),即快捷方式
  • -u :目标文件比源文件旧才更新、或目标文件不存在才复制
  • --preserve-all :除了 -p 功能外,还加入 SELinux 的属性,links、xattr也复制

rm

$ rm [-fir] filename/dirname
  • -f :强制删除,忽略不存在的文件,不会出现警告信息
  • -i :交互模式,删除前会询问使用者
  • -r :递归删除

mv

$ mv [-fiu] source destination
$ mv [-fiu] source1 source2 source3 ... directory
  • -f :强制移动,如果目标文件已经存在,不会询问直接覆盖
  • -i :交互模式,当目标文件存在时,会询问是否覆盖
  • -u :若目标文件存在且source交新,才会更新

basename/dirname

$ dirname ~/Documents/my-project/test-shell/test.txt
/Users/rango/Documents/my-project/test-shell
$ basename ~/Documents/my-project/test-shell/test.txt
test.txt

cat

$ cat [-AbEnTv] filename
  • -A :相当于 -vET
  • -b :列出行号,空白行不标注行号
  • -E :打印结尾换行符 $
  • -n :打印行号,包括空白行
  • -T :将 tab^i 打印出来
  • -v :列出一些看不出来的特殊字符

把时间花在更有意义的问题上:记录eslint开箱即用的规范和vscode配置

关于规范

是否在犹豫到底句尾加不加分号?
是否有过和同事争执哪种写法更优雅更易维护的经历?
是否争执过后痛不欲生的发现人和人之间无法互相理解?

参考下 standard 的哲学吧!

Just use standard and move on. There are actual real problems that you could spend your time solving! :P

确定一个开箱即用的规范然后就严格执行吧!(如果你有拍板能力的话...

Eslint基本配置和vscode插件配置

尽管在代码格式化上面有许多好用的命令行工具: prettierstandardtslint ...
但是考虑到不同技术栈对格式化要求不同、想要同时使用某几种风格、灵活性等情况,个人还是更加倾向基于 eslint 通过扩展和插件来配置项目代码规范的方式。目前各类热门的格式化工具或风格也都有 eslint 的扩展或者插件。

同时,基于 eslint 来扩展这种方式,无需为不同业务场景或项目安装额外的IDE插件或命令行工具

Eslint 基本配置

关于各种类型的 Eslint 配置文件如果同时出现,优先级如下:(通常不会这么做)

  1. .eslintrc.js
  2. .eslintrc.yaml
  3. .eslintrc.yml
  4. .eslintrc.json
  5. .eslintrc
  6. package.json

.eslintrc.json 为例,常见的核心配置项如下:

.eslintrc.json

{
  // 解析器 
  "parser": "esprima",
  // 定义一组预定义的全局变量
  "env": { "es6": true },
  // 解析器选项(不同的解析器选项可能会不同)
  "parserOptions": {
    // ECMAScript版本(只启用语法,不启用新全局变量,如Set)
    "ecmaVersion": 5,
    // 额外的语言特性
    "ecmaFeatures": {},
    // ECMAScript模块
    "sourceType": "module"
  },
  // 可共享的配置包
  "extends": [],
  // 插件提供额外的校验规则
  "plugins": [],
  // 具体的规则
  "rules": {
    // ...
  }
}

关于 extendsplugins
传入 plugins 选项的插件包会提供额外的规则实现,原理是在 eslint 将代码解析为AST后将上下文传入规则对应的回调函数,回调函数会进行一系列的校验操作。
传入 extends 选项的扩展包本身并不提供额外的规则实现,仅仅是提供一个预设的 .eslintrc.json 与本地的 .eslintrc.json 进行增量合并(本地的配置项优先级高于扩展包) 。如果传入多个扩展包的数组,相同的配置项后者会覆盖前者。

扩展包 eslint-config-standard 源码

{
  "parserOptions": {
    "ecmaVersion": 2021,
    "ecmaFeatures": {
      "jsx": true
    },
    "sourceType": "module"
  },

  "env": {
    "es2021": true,
    "node": true
  },

  "plugins": [
    "import",
    "node",
    "promise"
  ],

  "globals": {
    "document": "readonly",
    "navigator": "readonly",
    "window": "readonly"
  },

  "rules": {
    "no-var": "warn",

    // ...
  }
}

因此本地安装依赖后,只需要在配置文件中声明扩展,无需任何额外配置项。需要注意的是,扩展包一般会将依赖声明为 peerDependenciesnpm 不同版本对于 peerDependencies 处理方式不太一样。

本地 .eslintrc.json

{
  "extends": ["standard"] // eslint-config- 可省略
}

Vscode的插件配置

  1. 首先需要安装插件 ESLint
  2. 打开 setting.json ,添加如下配置
{
    "vetur.format.enable": false, // 关闭其他插件的format
    "editor.tabSize": 2,
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true // 保存的时候fix可以格式化的eslint问题
    },
    "editor.formatOnSave": true, // 开启保存格式化
    "eslint.format.enable": true, // 将eslint作为formatter
    "eslint.validate": [ // 哪些语言需要被eslint校验
        "javascript",
        "javascriptreact"
    ],
}

记录一下目前用到的一些风格方案

1. React + React Hooks + Standard

standard + standard-jsx

npm install --save-dev eslint-config-standard eslint-config-standard-jsx eslint-plugin-promise eslint-plugin-import eslint-plugin-node eslint-plugin-react

react-hooks

npm install --save-dev eslint-plugin-react-hooks

jsx-a11y

npm install --save-dev eslint-plugin-jsx-a11y

.eslintrc.json

{
  "extends": [
    "standard",
    "standard-jsx"
  ],
  "plugins": [
    "react-hooks",
    "jsx-a11y"
  ],
  "rules": {
    "no-console": "warn",
    "comma-dangle": [
      "error",
      "always-multiline"
    ]
  }
}

2. React + React Hooks + TS + Standard

standard-with-typescript

npm install --save-dev eslint@7 eslint-plugin-promise@4 eslint-plugin-import@2 eslint-plugin-node@11 @typescript-eslint/eslint-plugin@4 eslint-config-standard-with-typescript

react + react-hooks + jsx-a11y

npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "strict": true,
    "module": "esnext",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "jsx": "react",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "lib": [
      "es2015",
      "es2016",
      "es2017",
      "dom"
    ],
    "resolveJsonModule": true
  },
  "includes": [
    "./declare/externals.d.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

.eslintrc.json

{
  "extends": [
    "standard-with-typescript"
  ],
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "plugins": [
    "react",
    "react-hooks",
    "jsx-a11y"
  ],
  "rules": {
    "no-console": "warn",
    "comma-dangle": [
      "error",
      "always-multiline"
    ]
  }
}

3. Vue + Airbnb

babel-eslint + eslint-plugin-vue

npm install --save-dev eslint-plugin-vue babel-eslint

airbnb-base

npx install-peerdeps --dev eslint-config-airbnb-base

.eslintrc.js

module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ['plugin:vue/recommended', 'airbnb-base'],
  parserOptions: {
    parser: 'babel-eslint',
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'import/no-unresolved': 'off',
    'vue/max-attributes-per-line': ['error', {
      singleline: 3,
      multiline: {
        max: 1,
        allowFirstLine: false,
      },
    }],
  },
};

《你不知道的javascript(中)》1.2 值

数组

需要注意,空白单元可能会导致出人意料的结果,和将数组元素显式赋值为 undefined 是有所区别的(在不同浏览器上表现也不一致)

var a = []

a[0] = 1
a[2] = 3

a[1] // undefined
a.length // 3

由于数组也是对象,所以也可以包含字符串键,但是不会被计算在数组长度内。如果字符串键能被强制类型转换为十进制数字的话,它会被当做数字键处理

var a = []

a[0] = 1
a['foo'] = 'bar'

a.length // 1
a['foo'] // bar
a.foo // bar

a['1'] = 3
a.length // 2
a[1] // 3

一些DOM查询操作,或是 arguments 对象得到的都是类数组,可以将他们转化为真正的数组。

function foo(a, b) {
  var arr = Array.prototype.slice.call(arguments)
  // ES6: var arr = Array.from(arguments)
  arr.push('bam')
  console.log(arr)
}

foo('bar', 'baz') // ['bar', 'baz', 'bam']

字符串

javascript中字符串是不可变更的,即字符串的成员函数不会改变其原始值,而总是创建并返回一个新的字符串。同时字符串也是类数组,可以借用数组的非变更方法来处理字符串

var str = 'abc'
var arr = Array.prototype.slice.call(str)

arr // ['a', 'b', 'c']

var newStr = Array.prototype.join.call(str, '-')

newStr // 'a-b-c'

如果借用数组的变更方法来处理字符串(和书中结论不一致,推测是最新浏览器js引擎对字符串对象的键 setter 做了限制)

var str = Array.prototype.reverse.call('abc')
// Uncaught TypeError: Cannot assign to read only property '0' of object '[object String]'

数字

javascript只有一种数值类型:number,没有真正意义上的整数。javascript中的整数即没有小数的十进制数,所以 42.0 === 42

特别大和特别小的的数字默认用指数格式显示,与 toExponential() 函数输出的结果相同。

var a = 5e10
a // 50000000000
a.toExponential() // '5e+10'

toFixed() 方法可以指定小数部分的显示位数,但是需要注意,返回的是给定数字处理后的字符串

var a = 42.59

a.toFixed(0) // '42'
a.toFixed(1) // '42.6'
a.toFixed(2) // '42.59'
a.toFixed(3) // '42.590'
a.toFixed(4) // '42.5900'

toPrecision() 方法用来指定有效数位的显示位数,同样返回的是字符串

var a = 42.59

a.toPrecision(1) // '4e+1'
a.toPrecision(2) // '43'
a.toPrecision(3) // '42.6'
a.toPrecision(4) // '42.59'
a.toPrecision(5) // '42.590'
a.toPrecision(6) // '42.5900'

数字字面量也可以使用相应的方法(运算时字面量会被Number对象封装),但是需要注意 . 运算符,因为它是一个有效的数字字符

42.toFixed(3) // SyntaxError
(42).toFixed(3) // '42.000'
0.42.toFixed(3) // '0.420'
42..toFixed(3) // '42.000'
42 .toFixed(3) // '42.000'

数字字面量同样可以表示二进制、八进制、十六进制,推荐表示进制的字符统一采用小写

0xf3 // 243 十六进制
0o363 // 243 八进制
0b11110011 // 243 二进制
0363 // 243 八进制,但是严格模式下已经不支持,不建议使用

二进制浮点数(IEEE 754规范)最大的问题是,小数并不精确

0.1 + 0.2 === 0.3 // false

可以通过设置一个误差值的方式来处理小数,通常称为机器精度(machine epsilon)。在javascript中,这个值是 2^-52(2.220446049250313e-16) ,在ES6中,这个值被定义在 Number.EPSILON 中。

if(!Number.EPSILON) {
  Number.EPSILON = Math.pow(2, -52)
}

function numberEqual(n1, n2) {
  return Math.abs(n1 - n2) < Number.EPSILON
}

numberEqual(0.1 + 0.2, 0.3) // true

能够呈现的最大浮点数大约是 1.798e+308 ,它定义在 Number.MAX_VALUE 中。最小浮点数大约是 5e-324 ,它定义在 Number.MIN_VALUE 中。能够被安全呈现的最大整数是 2^53 - 1 ,在ES6中,被定义在 Number.MAX_SAFE_INTEGER ,同理最小整数是 Number.MIN_SAFE_INTEGER 。可以通过 Number.isIntegerNumber.isSafeInteger 来检测一个数是否是整数/安全整数。

需要注意的是,有些数字操作(如数位操作)只适用于32位数字,此时数字的安全范围为 Math.pow(-2, 31) 到 Math.pow(2, 31) - 1

特殊数值

在非严格模式下,可以为全局标识符 undefined 赋值(千万不要这么做!)

function foo() {
  undefined = 2
}

function bar() {
  'use strict'
  undefined = 2 // TypeError
}

运算符 void 并不改变表达式的结果,只是让表达式不返回值

var a = 42
console.log(void a, a) // undefined 42

void 使用场景:

function doSomething() {
  if(!APP.ready) {
    return void setTimeout(doSomething, 100) // 和下方注释代码等价
    // setTimeout(doSomething, 100)
    // return
  }
  
  // 返回处理完成后的结果
  var result
  return result
}

if(doSomething()) {
  // 继续做其他事情
}

NaN(not a number) 是唯一一个非自反的值,即 NaN !== NaN 。检查一个数值是否是 NaN 有两种方式,其中全局 isNaN 函数有个严重的缺陷,应该使用ES6提供的 Number.isNaN ,或者通过唯一一个非自反的值这个特性来判断

var a = 2 / 'foo'
var b = 'bar'

window.isNaN(a) // true
window.isNaN(b) // true
Number.isNaN(a) // true
Number.isNaN(b) // false

在JavaScript中,存在无穷数 Infinity(Number.POSITIVE_INFINITY) 和 -Infinity(Number.NEGATIVE_INFINITY) ,当运算结果溢出时,会在无穷数和最大数之间做一个“就近取整”。同时 Infinity / Infinity 是一个未定义操作,结果为 NaN

var a = Number.MAX_VALUE // 1.7976931348623157e+308
a + Math.pow(2, 970) // Infinity (距离Infinity更近)
a + Math.pow(2, 969) // 1.7976931348623157e+308 (距离Number.MAX_VALUE更近)

0 在JavaScript中是区分正负的。这在某些场景下是必要的,例如“动画帧的移动速度”,数字的符号位可以用来记录移动方向,假设某个时刻速度值为 0 ,那么保留正负号不会丢失移动方向。

var a = 0 / -3 // -0
var b = 0 // 0

a.toString() // '0'
a + '' // '0'
String(a) // '0'
JSON.stringify(a) // '0'

// 有意思的是,反向操作是正确的
+ '-0' // -0
Number('-0') // -0
JSON.parse('-0') // -0

// 比较操作
a == b // true
a === b // true
b > a // false

// 区分0和-0
function isNegZero(n) {
  return (n === 0) && (1 / n === -Infinity)
}
isNegZero(a) // true
isNegZero(b) // false

ES6中,可以通过 Object.is() 来判断两个值是否绝对相等,可以借用这个方法来判断特殊数值(性能比运算符差)

var a = 2 / 'foo'
var b = 0 / -1

Object.is(a, NaN) // true
Object.is(b, -0) // true

值和引用

JavaScript中没有指针,变量不可能成为另一个变量的引用,引用指向的是某一个具体的值。赋值和引用完全根据值的类型来决定:null, undefined, string, number, boolean, symbol 总是通过值复制的方式赋值/传递,object(以及其各种子类型 function, array, regexp) 总是通过引用复制的方式来赋值/传递。

由于引用指向的是值而非变量,所以一个引用无法更改另一个引用的指向

var a = [1]
var b = a
a // [1]
b // [1]

b = [2]
a // [1]
b // [2]

对于函数参数也是同理

function foo(x) {
  x.push(4)
  x // [1, 2, 3, 4]
  
  x = [4, 5, 6]
  x.push(7)
  x // [4, 5, 6, 7]
}

var a = [1, 2, 3]
foo(a)
a // [1, 2, 3, 4]

需要注意的是,因为基本类型值是不可更改的,所以对应对象子类型在引用时,如果遇到值发生变化的情景,会解除引用并将变化的值转化为基本类型(而非基本类型对应的对象子类型)

var a = new Number(1)
b = a 
a // Number{1}
b // Number{1}
typeof a // 'object'

b = b + 1
b // 2
a // Number{1}
typeof b // 'number'

LeetCode 1371 每个元音包含偶数次的最长子字符串

题目如下:

给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 'a','e','i','o','u' ,在子字符串中都恰好出现了偶数次。

示例 1:

输入:s = "eleetminicoworoep"
输出:13
解释:最长子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。
示例 2:

输入:s = "leetcodeisgreat"
输出:5
解释:最长子字符串是 "leetc" ,其中包含 2 个 e 。
示例 3:

输入:s = "bcbcbc"
输出:6
解释:这个示例中,字符串 "bcbcbc" 本身就是最长的,因为所有的元音 a,e,i,o,u 都出现了 0 次。

提示:

1 <= s.length <= 5 x 10^5
s 只包含小写英文字母。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-the-longest-substring-containing-vowels-in-even-counts
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路:

首先考虑暴力算法,单是找出字符串 s 的所有子串,即 [i, j] ,其中 0 <= i < j < s.length
因为前后两个变量,必然需要双层循环,在s.length <= 5*10^5 前提下,效率肯定不行。

一、考虑用前缀和来优化子串问题

以在 str = 'baaca' 这个字符串判断 'a' 在子串中数量举例
通过一次遍历可得:

  • pre[0] = 0 (前0个字符中,a的数量为0)
  • pre[1] = 0 (前1个字符中,a的数量为0)
  • pre[2] = 1 (前2个字符中,a的数量为1)
  • pre[3] = 2 (前3个字符中,a的数量为2)
  • pre[4] = 2 (前4个字符中,a的数量为2)
  • pre[5] = 3 (前5个字符中,a的数量为3)

假设想求得 [2, 4] 这个子串中 'a' 的数量
pre[5] - pre[2](前5个字符中a的数量 - 前2个字符中a的数量) 即可很方便得到数量为2

因此,如果在遍历时维护一个前缀和对应的各元音出现次数,可以直接计算出某一子串中各元音出现次数。

但是每得到一个前缀和,都需要和之前的前缀和比对,来不断更新最大长度

二、判断奇偶性并不需要得到具体数量

奇数 - 奇数 = 偶数 - 偶数 = 偶数
同样以上述 str = 'baaca' 举例

  • pre[0] = 偶 (前0个字符中,a的数量为偶)
  • pre[1] = 偶 (前1个字符中,a的数量为偶)
  • pre[2] = 奇 (前2个字符中,a的数量为奇)
  • pre[3] = 偶 (前3个字符中,a的数量为偶)
  • pre[4] = 偶 (前4个字符中,a的数量为偶)
  • pre[5] = 奇 (前5个字符中,a的数量为奇)

假设想求得 [2, 4] 这个子串中 'a' 的奇偶性
pre[5] 对比 pre[2](前5个字符中a的奇偶性 对比 前2个字符中a的奇偶性)
相同为偶,不相同为奇

推而广之,
假设 pre[i] 中奇偶性为 { 'a': 奇, 'e': 偶, 'i': 奇, 'o': 偶, 'u': 奇 }
并且 pre[j] 中奇偶性为 { 'a': 奇, 'e': 偶, 'i': 奇, 'o': 偶, 'u': 奇 } (0 <= i < j <= str.length)
那么可以得到 [i, j - 1] 这个子串中各元音出现次数均为偶数

转化下思路,
当一种奇偶组合第一次出现时,记录当前的前缀和个数 i
当相同组合再次出现时,当前的前缀和个数 j
此时可得到对应的子串为 [i, j - 1],长度为 j - i
如果长度比当前维护的最大长度大,则更新

{ 'a': 奇, 'e': 偶, 'i': 奇, 'o': 偶, 'u': 奇 } 这种存储结构比对起来很不方便,也没法作为键值,如何优化?

利用bitMap优化存储结构

对于n个key,且每个key只对应两种状态,很容易想到bitMap来存储
令 奇 = 1,偶 = 0

  • a 对应 00001
  • e 对应 00010
  • i 对应 00100
  • o 对应 01000
  • u 对应 10000

利用按位异或来不断切换对应的奇偶性,
且将第一次出现的奇偶组合、对应的当前前缀和个数作为 key: value 存储起来
当bitMap对应的值已经存在时,即当前奇偶组合已经出现过

最终代码:

var findTheLongestSubstring = function(s) {
    let res = 0;
    let bitMap = 0; // 当前前缀和的5个元音奇偶表
    const view = { // 对照表
        'a': 1 << 0,
        'e': 1 << 1,
        'i': 1 << 2,
        'o': 1 << 3,
        'u': 1 << 4,
    }
    const posMap = {}; // 记录所有奇偶可能性第一次出现的前缀和个数

    posMap[0] = 0; // 00000这种奇偶组合在0个前缀和中出现

    for(let i = 0; i < s.length; i++) {
        if(view[s[i]]) {
            bitMap ^= view[s[i]]; // 更新前缀和元音奇偶表
        }
        if(posMap[bitMap] >= 0) {
                // 如果posMap中已经出现过这种奇偶组合
                // 则当前前缀和个数 - posMap中记录的前缀和个数这一子串肯定符合条件,更新res
                res = Math.max(res, i + 1 - posMap[bitMap]);
        }
        else {
            // posMap中没出现过这种奇偶组合,则记录下当前前缀和个数
            posMap[bitMap] = i + 1;
        }
    }
    return res;
};

LeetCode 494 目标和

题目如下:

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。
注意:

数组非空,且长度不会超过20。
初始的数组的和不会超过1000。
保证返回的最终结果能被32位整数存下。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路一:递归(DFS)

  1. 从第0位开始递归,记当前总和为 sum = 0;
  2. 每次递归传入下一位的索引,和当前两种情况(+-)计算后的总和
  3. i === nums.length 时,对比和 target 是否相等
var findTargetSumWays = function(nums, S) {
    let count = 0;
    
    function calculateSum(nums, i, sum, target) {
        if(i === nums.length) {
            if(target === sum) {
                count++;
            }
        }
        else {
            calculateSum(nums, i + 1, sum + nums[i], target);
            calculateSum(nums, i + 1, sum - nums[i], target);
        }
    }
    
    calculateSum(nums, 0, 0, S);
    
    return count;
};

时间复杂度过高


解题思路二:DP

  1. dp[i][j] 表示数组中的前 i + 1 个元素,相加和为 j 的方案数;
  2. 因为可以添加正负两种符号,所以转移方程为:
dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
  1. 根据题设, -1000 <= j <= 1000 ,需要给 dp 第二维预先增加1000;
  2. 遍历完成后, dp[nums.length - 1][S + 1000] 即代表前 nums.length 个元素,相加和为 S 的方案数;
var findTargetSumWays = function(nums, S) {
    const dp = new Array(nums.length).fill(null).map(item => {
        return new Array(2001).fill(0);
    });
    dp[0][nums[0] + 1000] = 1;
    dp[0][-nums[0] + 1000] += 1;

    for(let i = 1; i < nums.length; i++) {
        for(let j = 0; j < 2001; j++) {
            if(dp[i - 1][j + nums[i]] > 0) {
                dp[i][j] += dp[i - 1][j + nums[i]];
            }
            if(dp[i - 1][j - nums[i]] > 0){
                dp[i][j] += dp[i - 1][j - nums[i]]
            }
        }
    }
    return dp[nums.length - 1][S + 1000] ? dp[nums.length - 1][S + 1000] : 0;
};

配置舒适的mac工作环境(轻微geek向)

系统设置

触控板设置:

  • 系统偏好 → 触控板:勾选 轻点来点按 ;将 跟踪速度 设置为最大
  • 系统偏好 → 辅助功能 → 触控板选项:勾选 三指拖移

触发角:

  • 系统偏好 → 调度中心 → 触发角:将右下角设置为 桌面 左下角设置为 通知中心

程序坞:

  • 系统偏好 → 程序坞:调整合适程序坞 大小,并关闭 放大 动画

命令行

大部分命令行工具安装需要使用外网环境,需要科学上网或者切换国内镜像

Command Line Tools

从 MacOS High Sierra,Sierra 开始,无需安装整个 Xcode 软件包,就可以单独安装 Command Line Tools

  1. 打开终端运行如下命令:
xcode-select --install
  1. 弹窗点确定安装
  2. 确认安装成功
git version

zsh

macOS 自带 zsh,但是老版本系统可能默认使用的是 bash

查看当前使用的 shell

echo $SHELL
# /bin/bash 默认使用的是bash
# /bin/zsh 默认使用的是zsh

修改默认 shellzsh

chsh -s /bin/zsh

当然也可以修改回 bash

chsh -s /bin/bash

zsh 对应的配置文件路径:~/.zshrc

oh my zsh

oh my zsh 是对 zsh 的一个增强,可以配置许多插件、主题,并且内置了许多有用的函数(没有用到)

oh my zsh 官网

通过 curl 安装:

sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

通过 wget 安装:

sh -c "$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)"

增加语法高亮和自动补全(ctrl + F 采纳)插件:

cd ~/.oh-my-zsh/custom/plugins
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git
git clone https://github.com/zsh-users/zsh-autosuggestions.git

编辑 .zshrc,添加如下内容:

plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
)

激活配置:

source ~/.zshrc

iTerm2

使用iTerm2的原因主要是可以配置一个比较舒服的命令行界面 iTerm2下载地址

安装PowerFonts:

cd ~/Documents && mkdir itermConfig && cd itermConfig # 创建一个iTerm2配置的目录
git clone https://github.com/powerline/fonts.git --depth=1 # 下载字体
cd fonts
./install.sh # 安装

设置字体:

iTerm2 → Preferences → Profiles → Text,在Font区域下拉菜单中选中 Meslo LG S for Powerline

安装配色方案(最新的iTerm2貌似自带):

cd ~/Documents/itermConfig && git clone https://github.com/altercation/solarized
cd solarized/iterm2-colors-solarized/
open . # 有明暗两种配色方案的 .itermcolors 文件,双击即可安装

设置配色:

iTerm2 → Preferences → Profiles → Colors → Color Presets,选中 Solarized Dark

安装主题:

cd ~/Documents/itermConfig && git clone https://github.com/fcamblor/oh-my-zsh-agnoster-fcamblor.git
cd oh-my-zsh-agnoster-fcamblor/
./install

设置主题:

编辑 .zshrc,修改 ZSH_THEME 配置项为 agnoster

激活配置:

source ~/.zshrc

另外可以根据个人习惯设置背景透明度或背景图片:

iTerm2 → Preferences → Profiles → Window

最终效果图:

image

git

配置多 ssh 环境:

cd ~ && mkdir .ssh # 如果不存在的话创建.ssh目录
ssh-keygen -t rsa -C "[email protected]" # 根据工作邮箱创建工作仓库秘钥对(github同理)
Enter file in which to save the key (/Users/maoqunzhi/.ssh/id_rsa): /Users/maoqunzhi/.ssh/id_rsa_work
Enter passphrase (empty for no passphrase):
Enter same passphrase again:

操作完成后 .ssh 目录下有两对秘钥(将公钥上传至对应的git仓库):

id_rsa_work
id_rsa_work.pub
id_rsa_github
id_rsa_github.pub

配置 ssh

cd ~/.ssh && touch config # 创建配置文件

添加如下内容:

个人建议保持host和host别名一致,因为目前无论是github还是gitlab都提供复制仓库地址的功能,使用别名反而增加了操作复杂度

Host github.com
HostName github.com
User qunzi0214
IdentityFile ~/.ssh/id_rsa_github


Host company.net
HostName company.net
User maoqunzhi
IdentityFile ~/.ssh/id_rsa_work

测试是否配置成功:

ssh -T [email protected]
Hi qunzi0214! You've successfully authenticated, but GitHub does not provide shell access.

编辑 .zshrc,加入两条快捷配置项目 git 用户名邮箱的 alias

alias git-config-work="git config user.name \"maoqunzhi\" && git config user.email \"[email protected]\" && echo \"Change git config work success\""
alias git-config-github="git config user.name \"qunzi0214\" && git config user.email \"[email protected]\" && echo \"Change git config github success\""

激活配置:

source ~/.zshrc

新建当前用户全局 git 配置文件:

cd ~ && touch .gitconfig

增加如下配置:

[alias]
  br = branch
  co = checkout
  ci = commit 
  st = status
  df = diff --stat
  lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

homebrew

homebrew 是一个 macOS 或 Linux 下的命令行工具管理器:

homebrew官网

homebrew github

homebrew 安装至当前用户根目录下:

cd ~ && mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew

编辑 ~/.zshrc,添加 homebrew 环境变量(修改为你自己的路径):

export PATH=/Users/maoqunzhi/homebrew/bin:$PATH

激活设置:

source ~/.zshrc

tmux

tmux 是一个终端增强工具,支持窗口与会话分离以及窗口水平垂直切割

阮一峰的tmux教程

通过 homebrew 安装:

brew install tmux

下载 tmux 插件管理器:

git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm # 插件管理器

创建配置文件 .tmux.conf

cd ~ && touch .tmux.conf

添加如下配置:

set -g prefix C-a
unbind C-b
set -g mouse on

# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-yank'

# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'

激活配置:

tmux source-file ~/.tmux.conf

修改 iTerm2 配置:

iTerm2 → Preferences → General → Selection:勾选 Applications in terminal may access clipboard

进入 tmux ,使用快捷键 prefix(ctrl + A) + I 安装配置文件中的插件

tree

tree 命令可以在撰写文档的时候快速生成目录树结构

通过 homebrew 安装:

brew install tree

配置 .zshrc ,添加如下 alias

alias show-dir="tree -L 3 -a -I (node_modules|dist)"

使用效果:

show-dir
.
├── package.json
├── src
│   ├── index.js
│   └── index.ts
└── yarn.lock

fuck

承包了我一上午笑点的毛子哥项目,遇事不决请 fuck 一下~

The Fuck github

通过 homebrew 安装:

brew install thefuck

前端开发环境

nvm

nvm 是一个 node 版本管理工具,可以切换不同版本的 nodejs,经手的项目时间跨度很大时,非常有用

nvm github

通过 homebrew 下载:

brew install nvm

编辑 .zshrc,添加如下内容(修改为你自己的路径):

# nvm config
export NVM_DIR="$HOME/.nvm"
. "/Users/maoqunzhi/homebrew/opt/nvm/nvm.sh"
#export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node

node

墙裂推荐通过 nvm 下载 node !!!

nvm install x.y.z

nrm

nrm 是一个 npm 镜像源管理器,不过如果项目是多人合作或要走流水线上生产环境,推荐使用 .npmrc.yarnrc 文件来统一项目依赖的 npm 站点

nrm github

通过 npm 安装:

npm install -g nrm

yarn

使用方式和 npm 大同小异,执行速度略快

yarn官网

通过 npm 安装:

npm install -g yarn

vscode

设置通过 code 从命令行打开 vscode

编辑 ~/.zshrc,添加 vscode 环境变量(修改为你自己的安装路径):

export PATH=/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin:$PATH

激活设置:

source ~/.zshrc

安装开发常用插件:

  1. auto-close-tag,自动闭合标签
  2. auto-rename-tag,自动重命名标签
  3. bracket-pair-colorizer,括号颜色配对
  4. eslint,eslint插件
  5. gitlens,git插件
  6. highlight-matching-tag,高亮当前焦点标签
  7. html-snippets,通过短语快速生成html代码
  8. image-preview,小图预览图片资源
  9. import-cost,标记引入模块大小
  10. vetur,vue插件,涉及到的功能非常多
  11. vscode-icons,目录及文件icon
  12. vue-vscode-snippets,通过短语快速生成vue代码

settings.json 添加如下配置:

{
  "vetur.format.enable": false,
  "editor.tabSize": 2,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "eslint.format.enable": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact"
  ],
  "explorer.autoReveal": false,
  "diffEditor.ignoreTrimWhitespace": false,
  "editor.bracketPairColorization.enabled": true,
  "editor.guides.bracketPairs": "active",
  "editor.unicodeHighlight.ambiguousCharacters": false,
  "editor.inlineSuggest.enabled": true,
  "github.copilot.enable": {
    "*": true,
    "yaml": false,
    "plaintext": true,
    "markdown": true,
    "rust": false
  },
  "window.zoomLevel": 1,
  "git.mergeEditor": false
}

chrome

快速打开一个禁用同源策略、开启控制台的标签页:

编辑 ~/.zshrc,添加 alias (需要在相应位置创建 MyChromeDevUserData 目录):

alias chrome-disabled="open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --auto-open-devtools-for-tabs --user-data-dir=/Users/maoqunzhi/MyChromeDevUserData/"

chrome 插件:

  1. JSONView
  2. ModHeader
  3. Octotree - GitHub code tree
  4. React Developer Tools
  5. Redux DevTools
  6. Vue.js devtools
  7. Quick QR
  8. 沙拉查词

必备软件

Spotify

LyricsX

Charles

Typora

SwitchHost!

Typescript 4.2英文文档 - More on Functions

Function Type Expressions

描述一个函数最简单的方式是用 function type expression,语法上和箭头函数非常像:

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

语法 (a: string) => void 表示一个“第一个入参为字符串类型,且没有返回值”的函数。如果参数没有指定类型,那么它会隐式推断为 any

注意,(string) => void 是指一个“第一个入参为任意类型,且没有返回值”的函数

可以通过类型别名来给一个函数类型命名:

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

Call Signatures

在 Javascript 中,函数除了可以被调用,也可以拥有属性。但是函数类型表达式并不能声明函数的属性。如果想要处理这种情况,可以使用 call signature

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

Construct Signatures

Javascript 中的函数,可以通过 new 操作符当做构造函数使用。Typescript中,可以通过在调用签名前增加 new 关键字,声明一个 construct signature

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Javascript 某些对象的构造函数也可以支持不通过 new 关键字调用(例如 Date 对象)。Typescript 中,可以给一个函数类型结合调用签名和构造签名:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: number): number;
}

Generic Functions

很多场景下,需要关联函数的输入值输出值类型,或者以某种方式关联两个输入值的类型。考虑如下函数,返回输入数组(不确定数组元素类型)的第一个元素:

function firstElement(arr: any[]) {
  return arr[0];
}

问题在于,返回值被推论为 any 类型。最好是能够让返回值类型和数组元素类型关联起来

在 Typescript 中,可以通过 generics 关联两个值的类型,仅需要定义一个 type parameter

function firstElement<Type>(arr: Type[]): Type {
  return arr[0];
}

定义一个类型参数 Type,并且在两个位置使用这个参数,会创建一个输入值和输出值的链接,此时在调用该函数时,可以获得一个更具体的类型:

// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);

注意这里,在调用时并不需要指定类型参数 Type 的具体值,Typescript 会自动类型推论。

当然也可以同时使用多个类型参数,例如,一个独立的 map 函数可能长这样:

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}

// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

泛型函数可以让我们操作任意类型的值。但是某些场景下,我们希望关联两个值,同时希望它们有确定的子集,这时,需要用到 constraint 来限制泛型可以接受的类型

思考如下函数,接受两个任意类型的值,返回 length 更大的那个值。想要做到这点,可以使用 extends 语法:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'string'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

使用泛型函数时,很容易发生如下错误:

function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj;
  } else {
    return { length: minimum };
    // Type '{ length: number; }' is not assignable to type 'Type'.
    // '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
  }
}

看起来好像是没什么问题的:Type 被约束为必须满足 { length: number } ,同时此函数只会返回 Type 类型或是某个匹配约束条件的值。

问题在于,函数承诺返回值类型和第一个参数值类型是相同的,而非仅仅是某个“能匹配约束条件”的值。哪怕上述代码是合法的,在运行时调用也会出现问题:

// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));

Typescript 通常可以在泛型函数中推断出合适的参数类型,但有些情况下不行,思考如下代码:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

const arr = combine([1, 2, 3], ["hello"]);
// Type 'string' is not assignable to type 'number'.

此时只能显式的指定 Type

const arr = combine<string | number>([1, 2, 3], ["hello"]);

编写合理优雅的泛型函数,需要遵循以下最佳实践:

  1. 尽可能使用类型参数本身而不是使用类型约束
function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}

function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

以上两种方式看似相同,但实际上 firstElement1 才是正确的,因为此时返回值类型被合理推断为 Type 。在 firstElement2 中,Typescript 会根据类型约束将返回值类型推断为 any

  1. 使用尽可能少的类型参数
function filter1<Type>(
  arr: Type[], 
  func: (arg: Type) => boolean
): Type[] {
  return arr.filter(func);
}

function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

filter2 中,类型参数 Func 并没有关联两个不同的值。同时,调用者在想要显式指定类型参数时,必须要多额外指定一个(尽管 Func 毫无作用),这导致代码难以阅读和理解

  1. 如果一个类型参数只在某处出现一次,需要重新考虑是否需要泛型函数
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

greet("world");

为什么不直接使用更简单的方式:

function greet(s: string) {
  console.log("Hello, " + s);
}

Optional Parameters

在 Javascript 中,函数可以接受不确定数量的参数。Typescript 中可以通过标记可选参数来模拟:

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

虽然此例中 x 被指定为 number 类型,但实际上它是 number | undefined 类型。因为在 Javascript 中,没有传入的形参会接收到值 undefined

当然也可以指定默认参数:

function f(x: number = 10) {
  // ...
}

现在在函数体内,x 的类型为 number 。因为任何 undefined 参数都会被替换为 10

当结合可选参数和函数类型表达式,很容易写出以下错误:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

此例中回调函数接受一个 index 可选参数,通常这么写是因为希望以下两种调用方式都成立:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

但实际上,这么声明的结果是,Typescript 会认为此回调函数只接收一个参数,导致调用时报错:

myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
  // Object is possibly 'undefined'.
});

在 Javascript 中,调用一个函数时可以传递实参少于函数形参,多余的参数会被忽略。Typescript 同样如此,只要参数类型相同,可以为形参更少的函数声明形参更多的函数类型:(此处有待研究,应该是只有回调函数支持这种特性)

function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

// both ok
myForEach([1, 2, 3], (a) => {
  console.log(a);
});
myForEach([1, 2, 3], (a, i) => {
  console.log(a, i);
});

基于以上特点,当给一个回调函数编写函数类型时,永远不要使用可选参数。

Function Overloads

许多 Javascript 函数可以被多种方式调用(动态的参数数量以及类型)。例如可以编写一个函数,此函数可以通过一个时间戳参数或是年/月/日(三个参数)来生成 Date 对象。在 Typescript 中,可以通过编写 overload signatures 让一个函数可以被多种方式调用:

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

《鸟哥的Linux私房菜(基础篇)》第4章 首次登陆与在线求助

X Window的使用

装机后实测:TODO

关闭界面切换的动画

终端输入 gsettings set org.gnome.desktop.interface enable-animations false

X Window 与命令行切换

Linux默认的情况下会提供六个操作接口环境(tty1 ~ tty6)供用户登录,切换:Ctrl + Alt + F1 ~ F6
如果在 tty3 上登录了系统,同时运行 startx 则图形界面会产生在 tty3 上
startx 生效条件:

  • 没有其他 X Window 被启用;
  • 安装了 X Window ,并且 X Server 能顺利启动;
  • 最好有窗口管理器,如 GNOME、KDE、TWM 等

命令行

命令的基本构成

$ command [-options] parameter1 parameter2 ...

命令组成:

  • 第一部分是命令或可执行文件
  • 通常简写选项是 -h ,完整选项是 --help
  • 中间的空格不论多少,都视为一格
  • 可以用 \ 来转义紧接着的特殊字符,如回车

从一些简单命令开始

$ date
2020年11月18日 星期三 10时46分07秒 CST

$ date +%Y/%m/%d
2020/11/18

某些选项前面是 - ,特殊情况下,也有些选项的前面是 +

$ cal [month] [year]

可以快速查看日历

$ bc

启动一个计算器环境,输入 scale=3 即保留小数点后3位,输入 quit 离开

重要热键

  • tab:在一串命令第一个字段后面是命令补全,非第一个字段后面是文件补全。安装了 Bash-completion 后,某些命令后面可以进行选项和参数补全
  • Ctrl + c:中断当前程序或输入
  • Ctrl + d:通常代表EOF(End Of File),也可以当做 exit 使用
  • shift + PageUP/PageDown:往前往后翻页

求助

--help:命令的使用方法(zsh上并不支持)
man [command]:打开该命令的 manual page
info [command]:打开该命令的 info page

man/info 中命令代号 含义
1 用户在shell环境可以操作的命令或可执行文件
2 系统内核可调用的函数与工具
3 常用的函数或函数库,大部分为C的函数库
4 设备文件的说明,通常在 /dev 下的文件
5 配置文件或者是某些文件的格式
6 游戏
7 惯例和协议,例如Linux文件系统,网络协议
8 系统管理员可以使用的命令
9 跟内核有关的文件
按键 manual page info page
空格 向下翻页 向下翻页
Page Down 向下翻页 向下翻页
Page Up 向上翻页 向上翻页
Home 去第一页
End 去最后一页
/string 向下查找string这个字符串 查找字符串
?string 向上查找string这个字符串
n 查找下一个字符串 前往下一个节点
N 查找上一个字符串
q 退出 退出
Tab 在节点之间移动
Enter 在节点上时,进入节点
b 移动光标到节点顶部
e 移动光标到节点底部
p 前往上一个节点
u 向上移动一级
s 查找字符串
h,? 显示帮助选项

总结:

  • 如果知道某个命令,忘记相关选项和参数,可以使用 --help
  • 有任何不知道的命令或文件格式,考虑用 maninfo 来查询
  • /usr/share/doc 下面有一些服务或软件的说明文件

关机

与关机有关的常用操作:

  • who:可以查看当前有谁在线
  • netstat -a:可以查看网络联机状态
  • ps -aux:可以查看目前主机使用状态
  • sync:将数据同步写入硬盘
  • shutdown:最常用关机命令
  • reboot/halt/poweroff:重新启动、关机等

《鸟哥的Linux私房菜(基础篇)》第1章 Linux是什么与如何学习

Linux是什么

基于x86硬件架构的操作系统

历史小梳理:Multics (1965, Bell MIT GE) —> Unics (1969, Ken Thompson) —> UNIX (1973, Dennis Ritchie) —> Minix (1986, Andrew S.Tanenbaum) —> Linux (1991, Linus Torvalds)

Linux内核版本

  • 根据主线版本开发新功能
  • EOL(End of Live) 不再继续维护
  • Longterm 长期维护版本
  • 通过 $ uname -r 查看内核版本,参考Linux官网

Linux发行版

RPM软件管理 DPKG软件管理 其他
商业公司 RHEL(Red Hat)、SUSE(Micro Focus) Ubuntu(Canonical Ltd.)
网络社区 Fedora、CentOS、openSUSE Debian、B2D Gentoo

GPL License

  • 可以自由复制
  • 可以对源代码进行修改
  • 可以将修改过的程序再次发行
  • 需要将修改过的代码回馈社区
  • 不可修改GPL授权
  • 不能单纯销售

一些学习资料网站

Netman: 计算机基础
Netman: 网络基础

如何解决问题

Linux自己的文件数据

/usr/share/doc

The Linux Documentation Project

Linux文档

网络服务问题

/var/log/ 查询 log file

《你不知道的javascript(中)》1.5 语法

语句和表达式

在JavaScript中,需要区分语句和表达式,因为它们存在一些重要差别。语句相当于句子,表达式相当于短语,运算符则相当于标点符号和连接词。在JavaScript中,表达式会返回一个结果值

var a = 3 * 6
var b = a
// 3 * 6是一个表达式,第二行的a也是一个表达式
// 这两行都是包含表达式的声明语句

var a
var b = 18
a = b
// 这里第三行只是赋值表达式,结果值是b的值18

语句的结果值

有如下两种情况:

  1. var let const 语句的结果值是 undefined

  2. 代码块 {..} 的结果值是其最后一个表达式/语句的结果值

var b
if (true) {
  b = 4 + 38
}
// chrome 控制台会输出 42

var a, b
a = if (true) {
  b = 4 + 38
}
// 无法运行!

var a, b
a = eval('if (true) { b = 4 + 38 }')
a
// 新版浏览器会报错:Uncaught EvalError,与书中不符。不过一般也没人这么写

表达式的副作用

最常见有副作用的表达式是函数调用(函数调用也是一个表达式,结果值是该函数的返回值):

function foo () {
  a = a + 1
}

var a = 1
foo() // 结果值undefined,副作用:a的值被改变

递增和递减运算符的副作用:

var a = 42
var b = a++

a // 43
b // 42
// 副作用将a自增1

++ or -- 在前时,副作用产生在表达式返回结果前,在后时,副作用产生在表达式返回结果之后

可以使用语句系列逗号运算符,将多个独立的表达式语句串联成一个语句:

var a = 42, b
b = ( a++, a )
a // 43
b // 43
// 使用括号包起来是因为','运算符优先级比'='运算符优先级低

delete 运算符的副作用:

var obj = {
  a: 42
}

obj.a // 42
delete obj.a // true
obj.a // undefined
// 表达式本身返回true或false来表示是否删除成功,副作用是删除了obj的a属性

= 运算符的副作用:

var a;
a = 42 // 42
a // 42
// 表达式本身返回42,副作用将42赋值给a

利用赋值表达式的副作用合并 if 条件

function vowels (str) {
  var matches
  
  if (str) {
    matches = str.match(/[aeiou]/g)
    
    if (matches) {
      return matches
    }
  }
}

// 优化后
function vowels (str) {
  var matches
  
  if (str && (matches = str.match(/[aeiou]/g))) {
    return matches
  }
}

上下文规则

在不同的上下文中 {..} 的含义不尽相同

  1. 对象常量

    var a = {
      foo: bar()
    }
  2. 代码块

    { // 这里并非对象常量,而是一个代码块
      foo: bar() // 合法,标签语句(不建议使用)
    }

    标签语句最大的用处在于,双层循环的时候可以从内循环控制外循环

    foo: for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        if (j === i) {
          continue foo // 跳转到foo的下一个循环
        }
        console.log(i, j)
      }
    }
    
    // 1 0
    // 2 0
    // 2 1
    // 3 0
    // 3 1
    // 3 2
    
    foo: for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        if (j * i >= 3) {
          break foo // 终止外层循环
        }
        console.log(i, j)
      }
    }
    // 0 0
    // 0 1
    // 0 2
    // 0 3
    // 1 0
    // 1 1
    // 1 2

    一个常见的坑,原因如下:

    • 第一行中 {} 被算作出现在 + 运算符表达式中,[] 会被强制类型转换为 '' ,而 {} 被强制类型转换为 '[object object]'
    • 第二行中,{} 被当做一个空代码块,因此 +[] 被强制类型转换为 0
    [] + {} // '[object object]'
    {} + [] // 0
  3. 对象解构

function getData () {
  return {
    a: 42,
    b: 'foo'
  }
}
var { a, b } = getData()
console.log(a, b) // 42 'foo'

function foo ({ a, b, c }) {
  console.log(a, b, c)
}
foo({
  c: [1, 2, 3],
  a: 42,
  b: 'foo'
}) // 42 'foo' [1, 2, 3]
  1. else if 和可选代码块

实际上JavaScript中并没有 else if 语法,只不过 ifelse 只包含单条语句的时候可以省略代码块的大括号

if (a) {
  // ...
} else if (b) {
  // ...
} else {
  // ...
}

// 实际上是
if (a) {
  // ...
} else {
  if (b) {
    // ...
  } else {
    // ...
  }
}

运算符优先级

MDN运算符优先级列表

需要注意:除了优先级,操作符的左关联 or 右关联也非常重要

自动分号

JavaScript会自动为代码补上缺失的分号,(Automatic Semicolon Insertion, ASI)

ASI只在换行符处起作用,且这个行为只有在代码行末尾与换行符中间除了空格和注释没有其他内容,才会产生

var a = 42, b
c
// 这里c是一条单独的表达式语句

var a = 42, b,
c
// 这里ASI不会生效,c会被当做第一行声明语句的一部分

书中建议在所有需要的地方加上分号,对ASI依赖的程度降到最低,仅为了追求“代码的美观”不值得。然而这件事完全可以交给webpack在打包/编译时处理。时代变了啊大人: )

函数参数

ES6中,参数可以设置默认值,但是需要注意暂时性死区(Temporal Dead Zone, TDZ)

function foo (a = 42, b = a + b + 5) {
  // ...
}
// 报错!b的默认值在b声明前就调用了b!但是访问a没有问题

有参数默认值的情况下,参数被省略或被赋值为 undefined 参数取值是一样的,但是 arguments 会产生如下问题

function foo (a = 42, b = a + 1) {
  console.log(
  	arguments.length,
    a,
    b,
    arguments[0],
    arguments[1]
  )
}

foo() // 0 42 43 undefined undefined
foo(10) // 1 10 11 10 undefined
foo(10, undefined) // 2 10 11 10 undefined
foo(10, null) // 2 10 null 10 null

除此之外,向函数传递参数时,arguments 数组中的对应单元会和命名参数建立关联(严格模式下不关联)

function foo (a) {
  a = 42
  console.log(arguments[0])
}
foo(2) // 42 关联了
foo() // undefined 未关联

function foo (a) {
  'use strict'
  a = 42
  console.log(arguments[0])
}
foo(2) // 2
foo() // undefined

因此,尽量不去使用 arguments ,本身它也是JavaScript语言底层实现的一个抽象泄露。ES6之后,完全可以用剩余参数语法来代替

try .. finally

关于执行顺序,这里 try 先执行,将函数的返回值设置为42,接着执行 finally 。最后函数执行完毕

function foo () {
  try {
    return 42
  } finally {
    console.log('hi')
  }
  console.log('never run')
}
console.log(foo())
// hi
// 42

如果包含 throw 也是如此

function foo () {
  try {
    throw 42
  } finally {
    console.log('hi')
  }
}
console.log(foo())
// hi
// Uncaught Exception: 42

但是当 finally 抛出异常,则函数执行会终止,try 中的返回值会被丢弃

function foo () {
  try {
    return 42
  } finally {
    throw 'oops'
  }
}
console.log(foo())
// Uncaught Exception: 'oops'

finally 存在返回值时,会覆盖 trycatch 中的返回值

function foo () {
  try {
    return 42
  } finally {
    return 43
  }
}
console.log(foo())
// 43

甚至还可以将 finally 和带标签的 break 语句一起使用(千万不要)

function foo () {
  bar: {
    try {
      return 42
    } finally {
      break bar
    }
  }
  console.log('crazy')
  return 'hi'
}
console.log(foo())
// 'crazy'
// 'hi'

switch

switch 语句中,变量会逐一与 case 表达式比较(严格相等)。需要注意的是,即使 case 表达式中使用了宽松相等,但是在 switch 内部仍然是严格相等的比较。

var a = 'hi'
var b = 10

switch (true) {
  case (a || b == 10):
    // 不会执行到这里,因为 a || b == 10 的结果为'hi',真值与true不严格相等
    break;
  default:
    console.log('oops')
}
// 'oops'

《鸟哥的Linux私房菜(基础篇)》第0章 计算机概论

**处理器(Central Processing Unit, CPU)架构

精简指令集(Reduced Instruction Set Computer, RISC)

指令精简,单个指令运行时间短,复杂指令需要多个指令来完成(Oracle的SPARC、IBM的Power Architecture、ARM的 ARM CPU)。PS3是Power Architecture,而手机一般是ARM CPU

复杂指令集(Complex Instruction Set Computer, CISC)

指令数目多且复杂,可以处理的工作丰富(AMD,Intel,VIA等x86)。叫x86的原因是最早Intel研发的CPU代号8086,64位的个人电脑CPU又称为x86-64架构。位(bit)是指CPU一次可以读写的数据量

常用的计算单位

容量

1TB = 1024GB = 1024 ^ 2 MB = 1024 ^ 3 KB = 1024 ^ 4 Byte = 8 * 1024 ^ 4 bit

速度

CPU频率:1GHz = 1000 ^ 3 Hz
网络传输:20M/5M 其实是 20Mbit/s 5Mbit/s,理论上下载 2.5MB/s 上传 625KB/s

总线带宽

内存频率的限制主要来自CPU中的内存控制器,假设一个x86-64的CPU对内存的工作频率是 1600MHz,
那么理论上总线带宽 = 1600MHz * 64bit = 12.8GB/s

内存

动态随机存取内存(Dynamic Random Access Memory, DRAM)

SDRAM/DDR 型号 数据位宽(bit) 内部频率(MHz) 频率倍数 带宽
SCRAM PC133 64 133 *1 1064MB/s
DDR DDR-266 64 133 *2 2.1GB/s
DDR DDR-400 64 200 *2 3.2GB/s
DDR DDR2-800 64 200 *4 6.4GB/s
DDR DDR3-1600 64 200 *8 12.8GB/s

同理,目前已经很流行的DDR4频率为3200MHz,带宽可以达到 33.6GB/s(Double Data Rate, DDR)

静态随机存取内存(Static Random Access Memory, SRAM)

使用SRAM来做成CPU内部的二级缓存(L2 Cache),把常用的程序或数据放入缓存中,可以大大提升性能

只读存储器(Read Only Memory, ROM)

通常用来写入固件,曾经BIOS(Basic Input Output System)也是写入ROM中的。特点是没有通电时也能记录数据。

显卡

主要用来处理图像,显存容量会影响到屏幕分辨率和颜色深度。同时,有专门的图像处理芯片,用于加速3D运算需求。目前显卡的插槽基本都是PCIe(PCI-Express)

规格 1x带宽 16x带宽
PCIe 1.0 250MB/s 4GB/s
PCIe 2.0 500MB/s 8GB/s
PCIe 3.0 1GB/s 16GB/s
PCIe 4.0 2GB/s 32GB/s

硬盘

机械硬盘(Hard Disk Drive, HDD)

一般由主轴马达、机械手臂、磁片柱和磁头组成。数据存储在磁片中,读写数据需要转动磁片,因此存在读写速度存在物理限制

SATA接口

版本 带宽(Gbit/s) 速度(MB/s)
SATA 1.0 1.5 150
SATA 2.0 3 300
SATA 3.0 6 600

因为 SATA 接口传输数据时,10bit 中仅 8bit 是数据,其余 2bit 作为校验用,因此换算(B转bit)是 1:10 而不是 1:8

USB接口

和SATA接口一样,仅仅是理论值

版本 带宽(Mbit/s) 速度(MB/s)
USB 1.0 12 1.5
USB 2.0 480 60
USB 3.0 5 * 1024 500
USB 3.1 10 * 1024 1000

固态硬盘(Solid state Disk, SSD)

最大的好处是,通过闪存直接读写的特性,不需要马达,无延时速度快且省电

操作系统

早期想让电脑执行程序需要参考一堆硬件功能函数,学习机械语言,并且程序迭代困难。操作系统的出现解决了这一问题:操作系统是一组程序,重点在于管理电脑的所有活动以及驱动系统中的所有硬件。

内核(kernel)

对硬件的所有操作都由内核完成,内核程序通常在电脑启动后就常驻内存中,且在内存中的区块是受保护的

系统调用(System Call)

操作系统通常提供一套应用程序编程接口(Application Programming Interface, API),否则开发人员仅仅从参考硬件功能函数编程参考内核功能

兼容

综上所述,硬件规格 -> 系统内核 -> 操作系统 -> 系统调用 -> 应用程序,所以应用程序必须指定某一版本的操作系统

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.