普通视图

发现新文章,点击刷新页面。
昨天以前黄琦雲的博客

TypeScript 之泛型

2021年4月18日 08:00

背景

泛型用于创建可复用的支持多种类型的组件,比如不仅能支持当前的类型,还能支持未来的类型,为大型系统的构建提供一定灵活性,有广泛、多种的意思,即泛型可实现对多种类型的支持;泛型是一种已有的概念,除了 TypeScript,同样也存在于其他多种语言中;

先举一个基本的例子,ts 中实现一个加法运算的函数,可以是这样的:

function addFn(arg1: number, arg2: number): number {
    return arg1 + arg2;
}
addFn(1, 2);

如果后期功能拓展,需要上述函数也具备拼接字符串的功能,即:

function addFn(arg1: string, arg2: string): string {
    return arg1 + arg2;
}
addFn('a', 'b');

但是这样的申明并不与上面已有的申明兼容,即使使用联合类型处理也比较复杂,但是它们的处理逻辑却是一样的,只是类型值换了,重写一个新函数也不符合拓展的初衷,所以需要寻求其他方法;

函数重载

在 ts 中重载是为一个函数提供多个类型定义的操作,使得函数可以根据传参的不同而拥有不同的返回类型;这样,我们就能轻松实现之前例子的拓展需求:

function addFn(arg1: string, arg2: string): string;
function addFn(arg1: number, arg2: number): number;
function addFn(arg1: any, arg2: any): any {
  // 上面一行只是函数的实现签名,为了兼容上面两个重载签名,不能被直接调用,
  // 同时它也并不算作一个重载,真正的重载签名只有最上面的两个
  return arg1 + arg2;
}

// 以下代码都能通过类型检查
addFn(1, 2);
addFn('a', 'b');

// 而没在重载定义中的类型会报错
// addFn(true, true); // Error

这个例子中,该申明函数与正常函数的区别是:

  • 在函数申明的上方又叠加了几个申明表达式,包裹参数类型和返回类型,末尾以分号结束,每一个申明便是一个重载;
  • 然后是在函数区块中写处理逻辑,同时包含进上面的几种参数与返回类型的情况;
  • 最后调用时就可以传入不同重载所对应的传参类型,并且能通过类型检查,而不在重载定义中的类型则不会通过类型检查;
  • 函数在调用时,ts 会在申明的函数重载中自上而下查找第一个匹配的重载签名,最后一个函数签名称为“实现签名”,并不会被调用;

使用重载虽然实现了同时能计算数字和拼接字符串的需求,但是这种写法还是有些复杂,因为参数类型与返回类型具有一定规律性;因此还可以继续寻求更简便的方式;

类型变量

在 ts 中,可以使用泛型来解决上述需求,具体的方式是使用类型变量,顾名思义,ts 包含一个庞大的类型处理系统,有各种使用类型的情景,为了应对一些场景,就需要类型值有像变量一样的变化性,支持赋值与取值操作;

先看一下使用类型变量的具体写法:

function addFn<T>(arg1: T, arg2: T): T {
    return arg1 + arg2;
}
addFn<number>(1, 2);
addFn<string>('1', '2');
addFn('a', 'b'); // 调用时也可以省略类型赋值

这里的写法就比写重载的形式简便了许多;示例中出现了 <T> 这个标识,其中 T 表示类型变量<> 表示对类型变量的申明,即申明时使用 <> 设置变量,调用时再使用 <> 进行赋值,这样所有用到变量 T 的地方都会被替换为传入的类型值;这里可以发现,泛型就像类型系统中一个针对类型的函数,类型参数就是形参

调用时可以省略类型赋值的操作是因为上面的场景中 ts 可以利用类型推断机制自动判断出 T 的实际类型值(number);

由于 T 表示任意类型,所以不能直接访问某些属性或方法:

function fn<T>(arg: T): T {
  // return arg.toString(); // Error,因为并不是所有类型都有该方法
  return arg;
}

如果是复合类型,则可以使用某些固有属性:

function fn<T>(arg: T[]): string {
  return arg.toString(); // 普通数组类型都具有该方法
}

类型变量也可以使用其他字母或者单词(通常使用 T),并且可以同时定义多个变量:

function fn<M, My, other>(arg: M): M {
  let one: My;
  let two: other;

  return arg;
}

// 存在多个类型变量时,需要依次赋值类型
fn<string, number, boolean>('abc');

泛型接口

除了函数,泛型也可以在接口中使用,例如:

interface IGeneric {
  <T>(arg: T): T;
}

let fn: IGeneric = function(arg) {
  return arg;

  // 和接口申明不一致会报错
  // return arg + '';
}

或者是针对整个接口的泛型:

interface IGeneric<T> {
  a: T;
  b: T[];
  c(arg: T): T;
}

const obj: IGeneric<number> = {
  a: 123,
  b: [1, 2, 3],
  c: (arg) => arg + 1
};

泛型类

针对类定义类型变量时,类的所有非静态成员都可以使用该变量:

class CS<T> {
  constructor(public attr: T) {}

  fn(): T {
    return this.attr;
  }

  // 静态成员不能使用泛型类型
  // static a: T = ''; // Error
}

// 实例化时传入类型值
const cs = new CS<number>(123);

cs.fn(); // 123

泛型约束

在一些场景中,可能并不期望类型变量的值太,而是需要具有某些属性或方法,这时就可以使用 extends 关键字对类型加一些约束,这和类中的继承用途也有些类似;例如:

interface IObj {
  length: number;
}

// 将参数约束为具有 length 属性的任意类型
function fn<T extends IObj>(arg: T): number {
  return arg.length;
}

fn('abc'); // 3

// 报错,因为数字没有 length 属性
// fn(123); // Error

结合其他 ts 特性,也能表示一些特殊情形,比如下例表示函数第二个参数,需要是第一个参数对象的属性名之一,keyof 关键字为索引查询操作符,keyof T 表示 T 的所有属性构成的联合类型:

function fn<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

fn({ a: 1 }, 'a');

// 报错,因为第一个参数对象中并没有一个叫 'b' 的属性
// fn({ a: 1 }, 'b'); // Error

Redux 主要知识学习总结

2020年11月29日 08:00

概念

Redux 作为一个状态管理器,可以应用于多种 web 技术或框架中,React 只是其中之一;Redux 的特点在于,多个页面或组件使用同一个状态(store,用于管理应用的 state),可以实现各模块或组件之间的数据共享,应用的任何部分都能进行状态修改,避免了传统的组件间深层次传值问题;

使用

创建状态(store)

Redux.createStore() 方法用于创建一个 store,其接收 reducer 作为第一个参数;

reducer 为一个自定义函数,接收 state 作为第一个参数,同时返回一个值作为新的 state

reduce 有缩减,减少的意思,可以理解为一个缩减器,不断将新得到的状态覆盖原状态,以实现 store 的单一状态更新,其名字也是根据 Array.reduce() 方法而来的;

import Redux from 'redux';

// 为 state 设置默认值
const reducer = (state = 1, action) => {
    return state;
}
const store = Redux.createStore(reducer);

createStore() 方法还接收第二个参数 initialState,作为 state 的初始化值,即下面两种写法效果相同:

import Redux from 'redux';

const store1 = Redux.createStore((state = 1) => {
    return state;
});
const store2 = Redux.createStore((state) => {
    return state;
}, 1);

const state1 = store1.getState();
const state2 = store2.getState();

console.log(state1); // 1
console.log(state2); // 1

读取状态

getState(),是所创建的 store 对象的一个方法,用于获取创建的状态;

const store = Redux.createStore(
    (state = 1) => state;
);
const state = store.getState();

console.log(state); // 1

改变状态(action)

触发更新

state 的更新需要通过触发 action 来实现,actoin 是前面的 reducer 函数接收的第二个参数,一个 action 是一个包含操作信息的对象,同时也可以携带要传递的额外数据;

触发 action 使用 dispatch 实现,dispatchstore 对象的一个方法,其接收参数为 action 对象,是更新状态的唯一途径;

这里之所以多加一层 action,而不直接修改状态,是为了追踪某一状态为何更新,或者调试时进行操作复现等目的,而 action 中的 type 就相当于为了被追踪而留下的痕迹;

const store = Redux.createStore(
    (state=1, action) => {
        if (action.type === 'myAction') {
            return action.myData;
        } else {
            return state;
        }
    }
);

const action = {
    type: 'myAction', // type 属性为必填项
    myData: 'myContent.', // 自定义携带数据
}

store.dispatch(action);
console.log(store.getState()); // "myContent."

在模块较多的复杂应用中,为了辨识操作,方便理解,通常 type 的格式会定义为 模块/操作 的形式,模块一般和对应的 reducer 相关,例如:

const todoReducer = (state, action) => state;
const userReducer = (state, action) => state;

const addTodo = { type: 'todo/add' };
const renameUser = { type: 'user/rename' };

响应更新

更新 stateaction 被触发了,还需要定义一些操作对其进行响应,在 action 触发时执行,即指定如何更新 state

这里更新 state 的逻辑写在之前创建 store 时传入的 reducer 函数中,由于 Redux 中的 state 是只读的(并未强制,但需自行在代码中遵守),所以 reducer 每次返回的 state 都是新的;

const myState = {
    num: 0
}
const myReducer = (state=myState, action) => {
    if (action.type === 'add') {
        return {
            num: state.num + 1
        };
    } else {
        return state; // 非指定状态需要考虑返回原状态
    }
}
const store = Redux.createStore(myReducer);
const myAction = {
    type: 'add'
}

store.dispatch(myAction);

console.log(store.getState().num); // 1

Redux 并未强制 reducer 中的 state 为只读的,其实是可以对其进行修改,例如:

const defaultState = { num: 0 };
const store = Redux.createStore(
    (state=defaultState, action) => {
        if (action.type === 'add') {
            state.num += 1;
            return state;
        } else {
            return state;
        }
    }
);

console.log(store.getState()); // { num: 0 }

store.dispatch({ type: 'add'});

console.log(store.getState()); // { num: 1 }

但官方并不建议这么做,这有可能导致页面数据得不到及时更新的 bug,所以需要开发者考虑自行维护其不可变性(Immutability),这也能实现更好的状态追踪,问题追溯等开发体验,如官网提到的一项叫 time traveling debugging 技术;并且,Redux 官网对该框架的介绍也是 Redux is a predictable state container,即具有预见性的状态管理器;

订阅状态

subscribe() 是 store 对象的方法之一,它接收一个函数作为参数,用于设置监听器以订阅状态的更新,即指定 state 更新时应该做什么;

const store = Redux.createStore((state=0, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
});

store.subscribe(() => {
    // 指定每次更新状态就打印当前值
    console.log('dispatch', store.getState());
})
store.dispatch({ type: 'add' }); // 'dispatch' 1
store.dispatch({ type: 'add' }); // 'dispatch' 2

拓展

状态合成

虽然 Redux 为了管理方便而设置单一的 store 对所有 state 进行统一管理,但是状态量的增长会使得书写变得复杂,所以 Redux 对象提供了一个 combineReducers() 方法,将所有声明的分工不同(不同组件、页面或子应用)的 reducer 合并为一个总的 reducer

该方法接收一个对象作为参数,不同的属性名用于标识不同作用的 reducer,以及状态更新后从 store 中取回状态值,属性值为声明的 reducer 函数;

const calcReducer = (state=1, action) => {
    switch (action.type) {
        case 'add':
            return state + 1;
        case 'minus':
            return state - 1;
        default:
            return state;
    }
}
const countReducer = (state=0, action) => {
    if (action.type === 'add') return state + 1;
    else return state;
}
const rootReducer = Redux.combineReducers({
    calc: calcReducer,
    count: countReducer,
});
const store = Redux.createStore(rootReducer);


console.log(store.getState()); // { calc: 1, count: 0, }

store.dispatch({ type: 'add' });

console.log(store.getState()); // { calc: 2, count: 1 }

combineReducers() 参数对象中指定的属性名用于存储该 reducer 的所有状态值;

Enhancer

Redux.createStore() 方法还可以接收第三个参数 enhancer,用于自定义 store 的功能或强化其能力(例如魔改),比如改变 dispatch(), getState(), subscribe() 等方法的默认行为;

enhancer 参数为一个自定义函数,其接收 Redux.createStore 这个方法作为参数,并返回一个新的 createStore 方法;

下面是一个为 dispatch 添加功能的简单示例:

const myReducer = (state=1, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
}
// enhancer 接收一个参数,即 Redux.createStore 这个方法,
// 用于执行创建 store 的默认行为;
const myEnhancer = (createStore) => {
    // enhancer 需要返回一个函数,其参数与 Redux.createStore 的相同,
    // 可以理解为返回另一个新的 createStore 函数;
    return (reducer, initialState, enhancer) => {
        // 需要执行一次 Redux.createStore 的默认行为,并获取 store
        const store = createStore(reducer, initialState, enhancer);

        // 修改 store 中的默认 dispatch 方法
        store.dispatch = (action) => {
            // 新加的功能
            console.log('dispatched.');
            // 最后仍需执行一次 dispatch 的默认行为
            return store.dispatch(action);
        }
        
        // 修改默认的 getState 方法
        store.getState = () => {
            return store.getState() + 1;
        }

        // 返回的新 createStore 方法还需要返回一个对象,即整个 store 对象;
        return store; 
        }
    }
}

const store = Redux.createStore(myReducer, undefined, myEnhancer);

store.dispatch({ type: 'add' }); // "dispatched."

console.log(store.getState()); // 3

需要同时使用多个 enhancer 时,需要进行合成,可以使用 Redux.compose() 方法:

const enhancers = Redux.compose(enhancer1, enhancer2); // 可以传入多个参数作 enhancer
const store = Redux.createStore(myReducer, undefined, enhancers);

Middleware

大多数时候,我们只希望自定义 dispatcch 方法的逻辑,所以官方专门提供了一个叫 middleware 的特性,翻译过来就是中间件,即在触发 action 和调用 reducer 执行响应之间,给用户提供一个可操作空间,如用于日志记录,问题报告,或者处理异步操作等;

middleware 是一个自定义函数,其接收一个对象作为参数,该参数对象有两个方法,分别是 dispatchgetState,逻辑都与 store 对象中的两个同名方法相同;

middleware 函数还需要返回另一个函数作为包装(自定义)后的 dispatch 方法,由于逻辑层次较多,下面会通过代码说明;

Redux 中内置了一个叫做 applyMiddlewareenhancer 方法,用于添加 middleware,它可以接收多个参数以传入多个 middleware

具体实现通过举例说明:

const myReducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else {
        return state;
    }
}

// 自定义的中间件函数
const myMiddleware1 = ({ dispatch, getState }) => {
    // 中间件需要返回一个函数,即新的 dispatch 逻辑,
    // 该函数又接收一个参数 next,用于执行下一个 middleware,
    // 当然如果有下一个中间件就执行,没有了就执行原始的 dispatch,
    // 其实这个参数 next 也就是原始的那个 store.dispatch 方法;
    return (next) => {
        // 该函数也需要返回一个函数,用于处理 action,
        // 接收一个 acction 作参数,即 store 触发的 action;
        return (action) => {
            // 自定义逻辑
            console.log('mid 1', getState());

            // 这个函数还需要返回一个函数,即用之前的 next 方法
            // 将 action 传递给下一个 middleware 继续处理;
            return next(action);
        }
    }
}

// 也可以使用简写方式
const myMiddleware2 = ({ getState }) => next => action => {
    console.log('mid 2', getState());
    const result = next(action);
    console.log('mid 2 new', getState());

    return result;
}

// 使用中间件
const myEnhancer = Redux.applyMiddleware(myMiddleware1, myMiddleware2);

const store = Redux.createStore(myReducer, 1, myEnhancer);

store.dispatch({ type: 'add' });
// "mid 1" 1
// "mid 2" 1
// "mid 2 new" 2

console.log(store.getState());
// 2

总结一下整个执行过程就是:

  • 用户调用了 store.dispatch() 触发 action
  • Redux 按 applyMiddleware() 方法中参数的传入顺序,挨个执行自定义的 middleware 逻辑;
  • 然后再调用原始的 store.dispatch() 方法触发 action
  • 最终执行 reducer 中的逻辑;

整个过程有些类似函数的链式调用:

dispatch -> middleware1 -> middleware2 ... -> dispatch -> reducer

此外,由于 middleware 的执行逻辑,其特性还包括对 action 中数据的修改、中断甚至彻底停止 action,的触发,例如上例中最后不返回 next(action),那么整个过程执行完第一个 middleware 就结束了,state 也不会发生预期的改变;

处理异步逻辑

Redux 内部并不知道如何处理异步逻辑,只会同步的触发 action,然后调用 reducer 更新 state,所以任何异步逻辑需要我们在外部自己实现;而 Redux 的宗旨是 recuder 不要有任何副作用,最好是一个纯函数,即不要有多余的外部联系,如控制台打印,异步请求等;

middleware 就是 Redux 专为副作用逻辑需求而设计的,这里以异步操作为例用代码进行简单实现:

const reducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else if (action.type === 'asyncAdd') {
        return action.data;
    } else {
        return state;
    }
}
const asyncMiddleware = _store => next => action => {
    if (action.type === 'asyncAdd') {
        setTimeout(() => {
            action.data = 'some data.';
            next(action);
        }, 2000);
    } else {
        next(action);
    }
}
const enhancer = Redux.applyMiddleware(asyncMiddleware);
const store = Redux.createStore(reducer, 0, enhancer);


store.dispatch({ type: 'add' });
console.log(store.getState()); // 1

store.dispatch({ type: 'asyncAdd' });
console.log(store.getState()); // 1

setTimeout(() => {
    console.log(store.getState());
}, 2000);
// 2 秒后输出:
// some data.

结果显示异步操作获取的数据,可以成功被 reducer 拿到并实现相应的逻辑,所以把 setTimeout 换成 Ajax 请求也同样可以从服务器获取到数据,然后传递给 Redux 进行下一步处理;

由于上面的异步逻辑的原生写法不太方便,Redux 官方就提供了一款 redux-thunk 工具,封装好了一个 middleware,应用之后就可以将 action 声明为一个函数(以前是一个对象),其接收 dispatchgetState 两个参数;具体用法如下:

import Redux from 'redux';
import ReduxThunk from 'redux-thunk';

const reducer = (state, action) => {
    if (action.type === 'add') {
        return state + 1;
    } else if (action.type === 'asyncAdd') {
        return action.data;
    } else {
        return state;
    }
}

// 直接应用该工具
const middlewareEnhancer = Redux.applyMiddleware(ReduxThunk.default);

const store = Redux.createStore(reducer, 1, middlewareEnhancer);

// 这里 action 声明为函数,处理异步逻辑
const asyncAction = (dispatch, getState) => {
    console.log('old state:', getState());
    setTimeout(() => {
        dispatch({ type: 'asyncAdd', data: 'some data.' });
    }, 2000);
}

store.dispatch(asyncAction);
// "old state:" 1

setTimeout(() => {
    console.log(store.getState());
}, 2000);
// 2 秒后输出:
// "some data."

需要注意的是,一些教程上(包括 Redux 官网)介绍 Redux Thunk 的用法时,仍然使用的 Redux.applyMiddleware(ReduxThunk) 写法,这是该工具 1.x 版本的写法,现在 2.x 版本需要加上 .default,即 Redux.applyMiddleware(ReduxThunk.default),不然程序会出现问题;

记一次 React 组件无法更新状态值的问题分析与解决

2020年9月10日 08:00

问题

React 组件中通过直接声明的元素变量(jsx 写法),在访问 state 中指定的状态值时,如果状态发生改变,使用状态值的元素内容无法得到相应更新;

下面的例子中,直接在 class 组件中声明元素变量 myDiv,并且需要访问 this.state 中的数据,最终对状态值进行展示,按钮用于改变状态值:

import React from 'react';

class App extends React.Component {
  state = {
    msg: 'hello',
  };
  
  myDiv = <div>{this.state.msg}</div>;
  
  handleClick() {
    this.setState({
      msg: 'world',
    })
  }
  
  render() {
    return (
      <>
        <div>{this.myDiv}</div>
        <button onClick={() => this.handleClick()}>
          change
        </button>
      </>
    );
  }
}

export default App;

按理说点击按钮后,状态发生改变(this.state.msg),页面的值会发生相应更新,但是页面内容并未发生相应改变,这其实是一个微小的细节问题,下面对其进行展开分析;

分析

上例中,在组件中直接声明了值为元素的一个变量 myDiv,并且其内容调用了状态值(this.state.msg),其实该变量在声明时状态内容直接被赋予了 this.state.msg当前值,并非想象中的引用值,然后状态改变(this.setState())时,React 会重新调用组件的 render() 方法,重新渲染组件内容,但是此时该变量中的状态值仍是之前被赋予的字面值,不会再访问一次当前的 state,所以其值最终也就不会发生相应的改变;

并且在一般的写法中,涉及访问状态的逻辑(如 {this.state.msg})一般都写在整个 render() 方法之中,这样每次状态的改变导致 render 方法重新执行,使得重新执行获取状态的逻辑,就能每次都访问到最新的状态值了;但有时又很难避免在复用组件时在 render 方法以外的地方访问 state,并期望数据被动态改变,这里就需要寻求其他解决方案;

解决方案

方法一

我们可以使用 getter 方式来声明变量,getter/setter 方法是声明对象属性的一种方式,可以实现该对象指定属性的属性值的访问控制(getter)以及修改控制(setter),下面是一个简单的使用示例:

var obj = {
    num: 1,
    get a() {
        return this.num;
    }
    set b(n) {
        this.num = n;
    }
}

console.log(obj.a); // 1

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

getter 声明的属性的特点是,每次调用对象的该属性,都会执行一次 getter 函数内的逻辑,然后返回 return 的值;所以如果把之前组件中的 myDiv 属性以 get 方式进行声明,这样每一次状态改变后,render() 方法重新执行,然后就会涉及对该变量的访问,导致重新执行 getter 方法中的逻辑,最后就能访问到改变后的状态值(this.state.msg),页面也就相应地更新了;

改造后的组件代码:

import React from 'react';

class App extends React.Component {
  state = {
    msg: 'hello',
  };
  
  get myDiv() {
    return <div>{this.state.msg}</div>;  
  } 
  
  handleClick() {
    this.setState({
      msg: 'world',
    })
  }
  
  render() {
    return (
      <>
        <div>{this.myDiv}</div>
        <button onClick={() => this.handleClick()}>
          change
        </button>
      </>
    );
  }
}

export default App;

方法二

类似使用 getter 的思路,为了让每次状态改变,用到状态的变量也发生相应的更新,另一种方法就是将变量 myDiv 声明为函数类型,同样也能使每次获取变量时都重新执行一次获取状态的逻辑,以获取最新状态值,改造后代码如下:

import React from 'react';

class App extends React.Component {
  state = {
    msg: 'hello',
  };
  
  myDiv = () => <div>{this.state.msg}</div>;
  // 或者是:
  // myDiv() { return <div>{this.state.msg}</div> };
  
  handleClick() {
    this.setState({
      msg: 'world',
    })
  }
  
  render() {
    return (
      <>
        <div>{this.myDiv()}</div>
        <button onClick={() => this.handleClick()}>
          change
        </button>
      </>
    );
  }
}

export default App;

不同之处就是每次调用变量 myDiv 时需要使用函数式调用(后面加一对括号),为了方便多处调用,显然方法一更有优势;

React 组件间传值的几种情形

2020年9月3日 08:00

父级传向子级

这应该是最常见的一种场景,通过在子组件上写 props,将数据从父组件中传递到子组件,子组件再从 this.props 中获取相应的值,这样可以根据传入值的不同返回不同的状态,即实现组件的复用;例如:

import React from 'react';

// 父组件
class Parent extends React.Component {
    message = 'Hello world!';

    render() {
        return (
            <Child myProp={this.message} />
        );
    }
}

// 子组件
class Child extends React.Component {
    message = this.props.myProp;

    render() {
        // Hello world!
        return (
            <div>{this.message}</div>
        );
    }
}

export default Parent;

多层传值

上述方法只用于单层数据传递,即父级传向子级,如果子级又存在子级,甚至向下递推,那么父组件要传值给后代组件,就要逐层向下传递,类似下面的情况:

import React from 'react';

class Parent extends React.Component {
    render() {
        return <Child myProp="hello" />
    }
}

class Child extends React.Component {
    render() {
        return <Grandchild myProp1={this.props.myProp} />
    }
}

class Grandchild extends React.Component {
    render() {
        // hello
        return <div>{this.props.myProp1}</div>
    }
}

export default Parent;

如果层数再多一些就是书写噩梦了,所以 React 提供了 context 机制,解决了深层传值的问题,现在来改造上面的代码:

import Reac from 'react';

// 首先需要创建一个自定义 context
// 该方法接收一个参数作为 context 的默认值
const myContext = React.createContext();
// 获取包裹组件,用于包裹需要应用 context 的组件
const { Provider } = myContext;

class Parent extends React.Component {
    value = {
        message: 'hello',
    };
    
    render() {
        // 通过包裹器的 value 属性向下传递指定值
        return (
            <Provider value={this.value}>
                <Child />
            </Provider>
        );
    }
}

class Child extends React.Component {
    render() {
        return <Grandchild />
    }
}

class Grandchild extends React.Component {
    // 在需要获取祖代传递的 context 值的后代组件中,
    // 声明 contextType 静态属性,值为之前创建的 context;
    static contextType = myContext;
    
    render() {
        // 最后使用 this.context 就能获取到之前
        // 在 Provider 中传入的 value 值;
        return <div>{this.context.message}</div>
        // hello
    }
}

export default Parent;

需要注意的是,由于提供方和调用方需要使用同一个使用 React.createContext() 创建的 context,所以如果父组件和要调用 context 的子组件不在同一个文件中的话,则需要考虑通过 exportimport 来实现引用,但是这样组件间的耦合度又增加了一层,React 官方建议使用 组合 方式取代上述的 继承 方式,下面再次对上述代码进行改造:

import React from 'react';

class Parent extends React.Component {
    value = {
        message: 'hello',
    };

    render() {
        return (
            <Child>
                {this.value.message}
            </Child>
        );
    }
}

class Child extends React.Component {
    render() {
        // this.props.children 会指向父组件在子组件中嵌入的数据或组件
        return (
            <div>{this.props.children}</div>
        );
    }
}

export default Parent;

子级传向父级

React 中似乎没有提供子级向父级直接传值,类似 props 的方法或途径,可以通过一些间接手段实现,开发中常见的处理方式就是子组件调用父组件通过 props 传入的处理函数,对需要传递的值进行处理;例如:

import React from 'react';

class Parent extends React.Component {
    state = {
        message: '',
    };
    
    handleMsg(msg) {
        this.setState({
            message: msg,
        });
    }
    
    render() {
        // Hello
        return (
            <>
                {this.state.message}
                <Child 
                    onMsg={(msg) => this.handleMsg(msg)}
                />
            </>
        );
    }
}

class Child extends React.Component {
    handleClick() {
        this.props.onMsg('Hello');
    }

    render() {
        return (
            <button
                onClick={() => this.handleClick()}
            >Clike me</button>
        );
    }
}

export default Parent;

简单梳理一下流程:

  • 父组件提前声明数据处理逻辑,该方法接收传入的值,然后进行相应处理;
  • 父组件将该方法通过 props 传递给子组件;
  • 子组件触发一些行为,得到了将要传递给父组件的值;
  • 子组件通过 this.props 调用父组件传入的处理函数,并将要传递的值作为该函数的参数;
  • 处理函数开始执行,由于其是在父组件的作用域中声明的,所以也能访问父组件中的一些数据,比如 state,相当于在父组件中处理子组件传入的数据;
  • 处理函数更新 state 状态值,随后其他访问该 state 的地方也会随即更新;

同级间传递

状态提升

这是 React 官网提到的一个概念,即多个组件都在重复使用同一个状态值,可以将这个值 提升 至父组件中保存,相当于数据复用;当然如果想实现一个子组件向另一个子组件传值,也可以通过父组件这层“媒介”;下面举例说明:

import React from 'react';

class Parent extends React.Component {
    state = {
        msg: 'hello',
    }
    
    handleChangeMsg(msg) {
        this.setState({
            msg: msg,
        });
    }
    
    render() {
        return (<>
            <ChildLabel msg={this.state.msg} />
            <ChildButton
                onChangeMsg={(msg) => this.handleChangeMsg(msg)}
            />
        </>);
    }
}

class ChildLabel extends React.Component {
    render() {
        return <span>{this.props.msg}</span>
    }
}

class ChildButton extends React.Component {
    render() {
        return (
            <button
                onClick={() => this.props.onChangeMsg('world')}
            >Change</button>
        )
    }
}

export default Parent;

这样在页面中点击子组件 <ChildButton /> 中的按钮时,子组件 <ChildLabel /> 中的文本便会发生相应变化,成功获取传入的值;这里的操作相当于子组件先向父组件传值,使得父组件状态变化,然后使用该状态的另一子组件就会相应地变化;

Refs

Refs 是 React 提供的另一种机制,React 中典型数据流是通过 props 传递,而 refs 则相当于提供了直接操纵组件或 DOM 元素的一种途径,强制修改元素;因此官网也建议避免过度使用 refs,防止应用变得难以理解或“失控”;下面通过举例简单说明其用法:

import React from 'react';

class Parent extends React.Component {
    // 首先需要声明 ref
    myRef = React.createRef();
    
    handleClick(msg) {
        // 通过 ref 的 current 属性实现对该元素的引用,
        // 然后就能想操作正常 DOM 一样实现控制;
        this.myRef.current.innerText = msg;
    }
    
    render() {
        // 对需要被引用的元素使用 ref 属性,值为之前所创建的 ref
        return (<>
            <span ref={this.myRef}>hello</span>
            <button
                onClick={() => this.handleClick('world')}
            >Change</button>
        </>);
    }
}

export default Parent;

在页面上点击按钮后,前面的文本同样会发生改变,即 DOM 元素的元素属性 innerText 值被成功修改,如需使用其他原生属性或方法同理;

Refs 转发

虽然 Refs 提供了直接访问组件或元素的途径,但是它却访问不了组件中的组件,这是 React 层故意为之,隐藏组件实现细节与渲染结果,防止组件的 DOM 结构被过度依赖;但有一些特殊情况下确实需要访问组件内部的组件的话,React 也提供了另外一种机制,即 Refs 转发(Refs Forwarding);顾名思义,组件 A 不能直接使用 ref 访问组件 B 中的组件 C,但是可以通过组件 B 转发 ref 给组件 C,这里改造一下上面 Refs 中的例子,我们在 <Parent /><span> 之间再加一层组件,再实现对其的操作:

class Parent extends React.Component {
    myRef = React.createRef();
    
    handleClick(msg) {
        this.myRef.current.innerText = msg;
    }
    
    render() {
        // 对子组件正常使用 ref
        return (
            <Child
                ref={this.myRef}
                handleClick={this.handleClick.bind(this)}
            />
        );
    }
}

// 使用 React.forwardRef 方法转发 ref 给下一层组件
const Child = React.forwardRef((props, ref) => {
    return (<>
        <span ref={ref}>hello</span>
        <button
            onClick={() => props.handleClick('world')}
        >Change</button>
    </>);
});

Refs 转发需要使用 React.forwardRef() 方法创造组件,该方法接收一个回调函数做为参数,该回调函数接收两个入参,第一个是传进组件的 props,第二个是传进组件的的 ref,通过内部逻辑决定 ref 再转发给谁,回调函数的返回值是最终生成的组件;页面加载组件后,点击按钮,就能像直接使用 ref 一样改变展示的文本值了;

记一次Windows电脑开机登录后黑屏的问题分析与排查

2020年5月20日 08:00

起因

一阳光明媚的晌午,本人心情愉悦地翻开笔记本,一如既往地摁下开机键后,略过了主板开机动画,熬过了 Windows 登录(win10 系统)的魔力转圈圈,最终却没能等来那昔日熟悉的桌面与亲切的图标们,直接映入眼帘的是下图:

black.png

嗯,就这样盯着它,10s…30s…1min…时间安静的流淌,内心也慢慢掀起了波澜,身经百战的心灵意识到不好的事情要发生了;Nice,人在家中坐,bug 天上来,不过黑屏给了我黑色的眼,我将用它来寻找问题。

问题探索

首先,调整好心态,冷静就有希望,慌乱就会败北(或者是像本人一样曾被无数 bug 折磨后的生死看淡?)问题总有会一些办法可以进行解决;然后就是寻找突破口了,这时下意识的晃了晃鼠标,然后熟悉的小光标出现了!但是还是背景一片黑,不过在这无边的黑暗中,这光标也算闪烁着唯一又弱小的希望的光芒;然后又是试探性的按了一下键盘的 windows 键,然后画风一变:

black-win-a.png

win+a也有反应,打开了侧边栏,证明系统已经加载完毕,按键都有作用,只是无法显示,于是一顿操作打开了个应用(盲开),等待数秒后没有反应,仍是一片黑,再次按下 win 键又确实看见了它已被打开,鼠标挪到任务栏位置看一下:

black-mouse-to-taskbar-window.png

再开个应用,尝试使用 alt+tab 组合键切换应用:

black-switch-window.png

看来能够正常启动应用,然后尝试点开了任务栏一个应用(资源管理器),把鼠标挪到应用的任务栏缩略图后,出现了下面一幕:

black-mouse-to-taskbar-window.png

咦这不是我那亲切的桌面嘛,居然以这种方式出现了,果然有戏,接下来再进一步发掘;然而 就在这时,桌面奇迹般的亮了,一切恢复如初,就像风不曾吹过,雨不曾下过,似一切都未曾发生过,难道是这般执著感化了 CPU ?开个玩笑,刚才没有执行特使的操作,应该是某种超时时间过了,桌面出现响应,不过看了看时间,算一下时间差大概有 3 分钟左右,果然这就是神奇的相对论,转瞬的时间有时可以变得很漫长;

不过事情不会这样结束,接下来又是习惯性地重启了电脑,看一下问题是否会再现,一顿操作和等待后,电脑开机…登录…转圈圈…然后果然又是黑屏!无边的黑暗再次席卷覆盖整个显示屏,不过这一次就要想办法将其撕破了;

问题分析

根据前面的经历,这里黑屏应该也要持续 3 分钟左右,甚至更多,那么就不能干等着,于是开始盯着深邃的屏幕陷入沉思:问题出在 Windows 系统登录后(该系统设置了开机自动登录 Windows 账户),就是系统的 BOOT 引导已经结束,这样就排除了常见的开机黑屏现象,即按下 电源键后一直黑没多余反应那种,这就通常是硬件方面的问题,比如内存条接触不良等原因,目前就基本排除了这些原因;既然是系统启动后,并且执行完了登录操作,而没能正常显示桌面,那么问题就缩小(好像也不怎么小哈..)到了软件层面,比如系统服务,驱动,启动项等等;

等等,启动项和桌面,好像想到了什么,因为一直在用一款桌面整理软件,从而避免脏乱差的视觉环境,同时就是设置开机启动,最近软件也更新了一下,难道是这个原因造成的桌面显示 bug ?不知不觉间桌面已经恢复显示了,于是按下 Ctrl+Shift+Esc 组合键调出任务管理器,点下 启动 栏,然后禁用该程序开机启动:

taskmgr-startup-disable.png

随后马不停蹄地重启了电脑,然后,事实证明事情似乎没有想象中的简单,依然是熟悉的黑屏,到这里也没有特别的好招了,因为一般给别人解决问题时首先就是问最近干了啥,可能会发现线索,不过本人最近用计算机干的事情似乎有点多,系统到用户层面的各种,服务器、虚拟机、数据库等等,一时也想不出什么线索(甚至觉得盯着屏幕呼吸都是一种错 -_-),所以准备向搜索引擎寻求帮助或者找找启发;

问题排查

搜索解决方案

一搜还确实有不少小伙伴有类似的经历,排除硬件故障无法开机的,有说更新驱动的,还不少,这种回答就一笑略过吧,这种方案很普遍也是有原因的,排除不愿相信硬件损坏的显示,就可以把大部分问题推到在软硬件之间打交道的驱动程序上了,其在过去确实能解决大部分问题,不过各厂商也都更新了这么多年了,驱动层面的问题现在应该很少了,而且本人电脑里的各个驱动都一直保持在最新状态,这个也排除了;

另外也有提到取消 Windows 的快速启动功能的,也就是下面的步骤:

control-powercfg.png

control-power-unlock.png

control-power-uncheck.png

不过经测试无效,所以排除;

继续浏览,不出所料,处理和解决 Windows 大部分故障或问题的场景,几乎都能见到 注册表 的身影,不过确实注册表这东西和 Windows 系统关系相当紧密,你在 Windows 中执行的大部分可见甚至不可见的操作,几乎都一项注册表项值与之关联;关于注册表的解决方案中基本都提到修改同一个地方(大部分是让下载或者新建一个注册表文件,然后双击导入系统,其实大可不必这样复杂):

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0001

即在这两项下都增加(不存在的话)一个名为 EnableULPS 的键,值为 0,类型为 REG_DWORD,就是如下图这样:

regedit-enableULPS.png

不明觉厉,先重启试试……还真解决了!!但是由于职业精神,问题被莫名其妙的解决了还是有些不甘心,于是就继续深入分析下,上面的注册表操作其实就是把 EnableULPS 这个熟悉赋值为了 0,根据字面意思全部翻译过来就是禁用了 ULPS 这个功能,再搜索得知,其全称 Ultra Low Power State(超低功率状态),这个似乎是 AMD 中的一个功能,下面是引用片段:

AMD 显卡为了防止因为频率太高导致系统不稳定。所以在 AMD 显卡上推出了一个 ULPS 功能,就是用户无操作的时候自动降频,休眠,然后用于节电。想法是好的,但是有人用了导致黑屏。所以出了一个关闭此功能的工具,它可以用于检测这个功能的开关状态,并直接关掉。

不过这个问题能在我的 Intel 中出现也是很迷;另外文章还有提到:

  • ULPS是休眠状态 ,降低非主卡的频率和电压的以节省电能,缺点就是可能会导致性能的损失和一些交火不稳定。
  • 经常用电池的不建议关闭ULPS,因为关闭后显卡一直工作在独显状态。

细想以前似乎从未动过这个功能,这么冒然改好像有点简单粗暴,之后还可能会得不偿失,所以这个方案暂时存着,先找找其它方面的问题;

自行排查

系统服务

就像之前说的,搜索问题有时候并不能得到有效的解决方案,但是某些回复的解决手段或者思路是可以起到一定程度的启发作用的,比如某一条大致说的是排查系统服务的问题,确实,之前分析时把问题定位在系统层面,排查过了启动项,但是 服务 这一块还没测试,所以先打开 msconfigWin+R 后输入:

msconfig-open.png

然后进入服务模块:

msconfig-service.png

这里列出的就是系统中的所有服务项,前面打勾代表已启用,否则是禁用,这里的思路就是先都禁用了,然后重启如果正常则挨个启用排查是哪一项服务的问题,当然这样工作量有点大,全部禁用也可能会出现额外的问题,所以可以先试试系统自带的诊断启动,会加载一些基本服务和设备,就是点击上图顶部最左侧的 常规 模块,然后选择 诊断启动

msconfig-diagnose-boot.png

点确定或应用后重启系统,这次就愉快又快速的进入系统桌面了(证明禁用不必要的服务确实能提高开机速度),不过也会发现某些模块无法使用,比如喇叭和屏幕亮度,甚至提示某些系统错误,很正常,因为只启用了基本服务,其它的系统服务和模块就没有加载,不过不影响问题排查就行了;

系统日志

当然,排查问题怎么少的了日志分析,可以起到一定辅助作用,于是这时就想起了 Windows 自家的法宝 事件查看器(由于平时也基本也怎么用过),平时用惯了 Linux 命令行分析日志,突然一切可视化了还不太习惯,先打开熟悉一下操作再说:

event-win-search.png

点开 Windows 日志:

eventlog-windows-log.png

再看看包含事件的几项:

eventlog-app.png

eventlog-security.png

这里由于分析问题可能是出在系统层面上,所以先关注 Windows 相关的事件,应用程序的暂且不管(其实也是因为点开它后发现应用数量有些庞大,不好找落脚点 ╮(╯▽╰)╭ ),然后就是挨个进到每一项中,点击右侧操作栏的清除日志按钮把日志分别清空:

eventlog-clear.png

这样如果后续操作时问题复现了,就可以较精确的定位了;

服务排查

然后可以把事件查看器关闭,再次打开 msconfig,选择诊断启动,再切到服务模块,可以看到大部分服务都没有被勾选了,然后我们点一下“服务”这个表头,让项目按名称顺序排列,方便后续操作:

msconfig-sort-by-name.png

然后就是重点的排除环节了,这里大致数了一下,有 400 个左右的服务项,如果挨个勾选再重启检查的话,可能也就写不出这篇文章了,所以需要找一个高效的办法,之前搜索问题时也受到一位小伙伴的启发,可以使用 二分查找 法进行排除,这本来是算法中的一种解决方案,没想到被这样给实际应用了(~ ̄▽ ̄)~,这里通俗讲就是先勾选一半的服务项目,比如从第一条开始,一直勾选直到右侧滚动条运动到大概中点的位置(好像工作量也不小,看手速咯),前面已经对服务名称进行过排序,所以这里前半部分服务大致是字母开头是 A - P 的服务项:

msconfig-check-a-p.png

重启系统后正常进入桌面,证明问题不在勾选的前半部分服务项中,可以排除掉,接下来我们再把剩下的没有勾选的服务项,勾选它们的前半部分,也就是说现在还有总量的最后四分之一部分没有被勾选,这样排除确实挺快,然后就是清除全部 Windows 日志,重启,再重复这些工作,直到问题复现(登录黑屏);

于是乎在进行到 W 字母开头的服务项排查时,登录终于黑屏了,虽然有些幸灾乐祸,但是却代表定位到问题了;然后就是继续二分,缩小范围,最终定位如下图所示:

msconfig-uncheck-web-account.png

也就是说罪魁祸首是这个名为“web 账户管理器”的服务项,看制造商应该是一项系统服务,并且之前搜索时看到有几位小伙伴定位的服务项是“App Readiness”,所以这个会因不同系统环境而不同,不应该一概而论冒然禁用;当然把它禁用后问题就解决了,没有像之前一样修改注册表,但是再次本着职业精神(no zuo no die),就继续分析一下问题的具体原因;

日志分析

日志概览

每一次统计的系统日志就在这时候发挥作用了,因为每一次重启前都清除了日志,所以每次记录的也就是当前排查项的事件,下面看一下记录的日志情况:

eventlog-app-apperr.png

black-sys-errlog.png

分别查看不同事件,可以 显示详细信息:

black-sys-err-dhcp.png

black-sys-err-scm-ops.png

日志筛选

可以看到即使单次记录的日志量也是很庞大的,所以现在可以使用事件查看器的日志筛选功能了,即点击右侧操作栏的筛选当前日志按钮,会弹出筛选设置窗口:

eventlog-filter.png

首先是记录时间,即指定事件的起始和结束时间点,可以在开机和桌面显示后分别记录一个时间,然后选择这个时间区间就能进一步缩小范围;

eventlog-filter-time.png

然后是时间组别,浏览也会发现事件主要分为信息、警告和错误,这里我们只用关心错误类型的事件,勾上后下面的项目暂时不用关,点确定;

eventlog-filter-config.png

下面就是筛选结果,可以看到错误信息还挺多,

eventlog-filter-done.png

对比黑屏时产生的错误日志,可以发现“应用程序”项的错误在正常进入桌面时也有发生,所以可以暂时排除这一项,而“安全”这一项,都是信息类,并没有错误类事件,所以也排除,最后就只剩“系统”这一项中的错误日志存在差异,存在差异的事件包括名为 Service Control ManagerDistributedCOM 的事件“来源”中;

eventlog-app-err-of-black.png

eventlog-sys-err-of-black.png

对比分析

那么我们就来对比一下“系统”中产生的错误日志的差异,只是事件查看器似乎没有内置日志对比的功能,所以只能使用较为原始的办法,先选中想要分析的事件:

eventlog-select-event.png

再点击右侧的保存选择的事件按钮,保存事件日志文件到任意位置:

eventlog-save-selected-log.png

像这样分别记录和保存发生黑屏问题和未发生问题时的事件,然后点击“打开保存的日志”,就能导入两个日志文件就行下一步分析了:

eventlog-open-saved-log.png

另外发现每个事件似乎都对应着一个唯一的 事件 ID 值,可以通过这个把两个日志文件重复的地方剔除,这就要使用筛选功能里的事件 ID 排除选项了:

Inkedeventlog-filter-exclude-id.png

填入重复的事件 ID,用逗号隔开,前面加负号 - 表示排除该 ID 的事件,不加表示包括,筛选结果如下:

eventlog-filter-compare-result.png

两个错误事件相同,从下方信息栏中没有发现特别有用的信息,只有一行主要信息:

服务器 {784E29F4-5EBE-4279-9948-1E8FE941646D} 没有在要求的超时时间内向 DCOM 注册。

那么接下来分析一下这串注册值,Win+R 输入 wmic 运行,进入 wmic 管理界面,然后运行:

dcomapp where "appid<='{79' and appid>='{74'" get appid,name

以上命令是查询开头类似 {784E29F4-5EBE-4279-9948-1E8FE941646D} 的 DCOM 服务,得到结果如下:

wmic-dcomapp-query.png

浏览后发现里面没有和上面相同的 ID 值,所以这条线索断了,试试其它的;

进程追踪

点一下“详细信息”,再向下浏览,发现了触发该事件的进程信息,其中比较重要的就是进程 ID(ProcessID),也就是常说的 PID,这里为 1140,先记下来;

eventlog-sys-compared-err-pid.png

然后 Ctrl+Shift+Esc 打开任务管理器,点一下 PID 栏(没有就在表头右键单击,然后勾选上),让它按数字升序排列,找到之前记录的 pid 值(1140):

eventlog-err-pid-with-tasklist.png

这时就能看到运行该进程的命令行信息了(同样要是没有这一列就右键点击勾选),发现运行的程序是 C:\Windows\system32\svchost.exe,这是一个系统程序,很多服务都会调用它,需要关注的是后面的参数,出现了 RPCSS 这个关键字,看着很熟悉,好像是和远程相关的,搜索后网上说这是一个与 135 端口相关的服务,那么我们就 Win+R 输入 cmd 打开命令提示符,查看一下这个端口信息:

cmd-netstat-rpcss.png

果然存在关联,那么这个 RPCSS 应该是一个服务,所以接下来用 sc 命令查询一下这个服务:

cmd-sc-rpcss.png

确实是一个服务,这里主要是获取 DISPLAY_NAME 这个值,即 Remote Procedure Call,然后打开服务管理工具(Win+R 后输入 services.msc),找到这个服务项:

service-rpcss.png

双击进去,看一下依赖关系,确实是一项系统基础服务,许多重要的服务和模块都依赖于它,还不能直接冒然禁用:

service-rpcss-depend.png

到这里所有分析工作就结束了。

后记

下面是之后重新收集的黑屏时的错误事件(启用全部服务),这次就只剩一处错误日志了,也与上面分析筛选结果一致:

eventlog-in-black-less-err-warn.png

然而,当再次禁用之前的问题服务项时,信息量就剧增了:

eventlog-no-black-more-err-warn.png

原因也很明显,禁用一项比较关键的服务项,并且其依赖项还比较多时,就难免发生连环事故,虽然暂时解决了目前的问题,但是对于轻微强迫症的作者来说,多少看着还是有些不安(饮鸩止渴?)但是后面有趣的事情又发生了,在某一次启用全部服务(包括之前确定为问题源的“web 账户管理器”服务)重启进入系统后,竟然意外地没有黑屏,而是和平时一样正常进入桌面,后面又试了几次都正常……难道是这一天的 n 顿操作猛如虎和无数次重启再次感化 CPU?看来 Windows 系统永远是个谜,bug 轻轻走了又正如它轻轻的来,不带走一片云彩,算了不玩了,收工。

关于浮点数的剪不断理还乱

2020年3月21日 08:00

二进制小数

谈浮点数前,先了解一些基础知识;对于整数的十进制与二进制转换不难,原理也简单就不再赘述,假如现在要进行转换的十进制数字是带有小数点的,转换方法就会稍微有点不一样了;为了使说明更浅显易懂,先来探究一下十进制小数中的奥秘;

以十进制数 3.125 为例,小数点以左为整数部分,右为小数部分,它也可以被更具体的拆解为以下形式:

3x10^0 + 1x10^-1 + 2x10^-2 + 5x10^-3

这也是十进制数的特点,基数为 10,所以每一位都是与 10 的相应次方的乘积,指数也是有规律的,可以看成以小数点为对称轴,向左就是 10 的 0、1、2、3……次方递增,向右则是 10 的 -1、-2、-3……次方递减,如果换一种形式,那么在运算方面也具有对称性(乘法与除法互为逆运算),就是下面的形式:

3x10^0 + 1/10 + 2/10^2 + 5/10^3

这样,理解二进制的小数部分就容易了,已经知道整数十进制转二进制其实就是连续除以 2 取余数,那么根据上面的规律,小数部分看出逆运算,就可以总结为 连续乘 2 取整数,举例说明:

3.125(十进制)

3     / 2 = 1    --> 1
         余 1    --> 1
0.125 x 2 = 0.25 --> 0
0.25  x 2 = 0.5  --> 0
0.5   x 2 = 1    --> 1

==> 11.001(二进制)

11.001 就是 3.125 的二进制小数形式,同时它也可以做如下拆解:

1x2^1 + 1x2^0 + 0x2^-1 + 0x2^-2 + 0x2^-3

= 2 + 1 + 0 + 0 + 0.125
= 3.125

可以看到也是与十进制形式相对应的,只是基数从 10 换成了 2 而已;

定点数

当然,都知道机器码都是一堆 01 组成的数据,并没有小数点这个专门的符号,要表示数字 3125 还好说,但是 3.125 中间多出的这一点要怎么解决呢……一个很直接的方法就是把小数点应该在的位置焊死 -_-,比如 32 位的系统,规定小数点在 16 位和 17 位之间,那么根据 3.125 的二进制是 11.001,在 32 中的表现就是:

0000 0000 0000 0011 0010 0000 0000 0000

这就是 定点数 的规则干的事情,看着挺好,要是多用几次系统空间可能就 hold 不住了,不好之处明显就是花掉当前一半的数量级取表述小数部分,当然可以少划分几位给小数,但对小数好像不太公平,万一精度要求高呢,反过来呢对整数又不公平了,手心手背都是肉,得想办法解决才行;

浮点数

于是乎,浮点数横空出世;其实上述问题平时肯定都遇到过,比如记个帐,昨日净收入 31200000000 元(@_@),肯定不想写这一堆 0 对不对(如果是真的咱愿意写 100 遍……),一般都会写 312 亿或者 3.12x10^10 元,重点就在后面这种科学计数表示法,人为了省力省纸省笔墨弄了个科学计数法,处理器也自然用上了浮点数,省空间;具体原理与表示方法后面会讲;

以 C语言为代表,其中就有浮点这一数据类型,比如单精度浮点 float(32 位),双精度浮点 double(64 位)等,可能平时也就直接声明和使用较多,没太关注底层实现的算法,其实也不太复杂,套用一种规则而已,下面开始介绍;

标准

目前关于浮点运算与表示,使用广泛的应该就是 IEEE 754(二进制浮点数算术标准)了,其主要内容如下:

v = (-1)^s * 2^e * m

v: 浮点数具体值
s: 符号位,即正负号,0 为正,1 为负
m: 有效数,也叫尾数,可以类比科学计数法前面的有效数字
   另外还有一个小数位 f, m = 1 + f
e: 指数位,即 2 的多少次方

该标准包含了多中位数,以 32 位为例:

(1)   (8)          (23)          : 位数
0 00000011 0010000000000000000000
s e        f

总结就是:

  • 第一位为符号位 s;
  • 后 8 位为指数位 e;
  • 最后 23 位为小数位 f;

64 位的规则是:

  • 第一位是符号位 s;
  • 后 11 位是指数 e;
  • 最后 52 位是有效数字 m;

知道了这些还不能立刻套用上面的公式经行转换,还需要了解接下来的一些规定;

指数偏移值

浮点的二进制表示中指数位 e 的计算值(即转换成十进制后的值),要在实际指数值(十进制)的基础上加上一个 偏移值,标准中规定偏移值为 2^(e-1) - 1,如 32 为中 e 是 8 位,所以偏移值为 127,64 位的就为 1023;所以 32 位指数实际值的范围为 -126 ~ 127

举个例,指数 e 的实际值为 -3,那么 32 位中加上偏移值就是 -3 + 127 = 124,换算成二进制的 8 位指数位就是 01111100;

这种指数位偏移后的指数值,又叫做 阶码,因为科学计数法中的指数是有正负之分的,所以实际指数值加上一个正的适中偏移值,就可以使得浮点表示法中的指数位为无符号的整型(就是变成正整数),利于浮点数的比较大小,就是可以直接从浮点的二进制表示中,由高位向低位逐位进行比较(如果是负数二进制比较大小要复杂一点)。

表示方式

具体的表示方式会根据不同的情况而不同,主要有以下几种情况;

规约形式

即 e 的 8 位数字不是全部为 0 或者 1,此时 m = 1 + f,由于小数部分 f 的值在 0 到 1 之间,所以有效数 m 的值在 1 到 2 之间;

非规约形式

这种形式 e 的 8 位全部为 0,小数位 f 值不为 0,用于表示非常接近 0 的数,此时不再是 m = 1 + f,而是 m = f,即 m 值在 0 到 1 之间;实际上所有非规约的浮点数比规约浮点数更接近于 0;

零值

指数位 e 全为 0 的同时,小数部分(f)为 0,用来表示 ±0(正负取决于 s 位的值);且规定最小指数位编码(e = 0 时)的实际值应该取 -126(本来应该是 0 - 127 = -127);

无穷大

如果 e 全为 1,且 m 全为 0,则表示无穷大(Infinity,正负取决于 s 位的值);

NaN

如果 e 全为 1,且 m 不全为 0,则表示 NaN(Not a Number,非数值类型);

综合举例

后面的例子都以 32 位为例,其它位数根据标准类推;

先来看一个十进制转浮点,规约形式的例子,比如用之前的十进制数 3.125,转换为 32 位浮点二进制格式(0b 开头的表示二进制数据):

3.125
= 0b11.001
= 0b1.1001 x 2^1

s = 0

e = 1
  => 1 + 127
  =  128
  =  0b10000000
  
m = ob1.1001
f = m - 1
  = 0b0.1001
  => 0b10010000000000000000000
  
==>
0 10000000 10010000000000000000000

转换的大致流程总结如下:

十进制小数 –> 二进制小数 –> 浮点表示法 –> 二进制浮点

再举一个二进制浮点转十进制小数例子:

1 01111111 00100000000000000000000

s = 1
  => -1
  
e = 0b01111111
  = 127
  => 127 - 127
  = 0
  
f = 0b0.001
m = 1 + f
  = 0b1.001
  
v = -1 x 0b1.001 x 2^0
  = 0b-1.001
  = -1.125

==> -1.125

精度

以 32 位单精度浮点数为例,由于分配给有效数字的位数是 23 位,而整数部分默认是 1,它的位置就不用留了,所以小数部分就可以独占 23 位,在加上默认的一个整数位就是 24 位了,同理,64 位双精度浮点数的有效数就是 53 位(52+1),再进行一下算术运算:

log(2^24) = 7.22
log(2^53) = 15.95

上面的算式表示二进制下的这么多位数的实际值,对应到十进制中有多少位;结果表明,单精度浮点可以保证 7 位十进制的有效数字,双精度的则可以保证 15 位;

“浮”的原因

取名为浮点,那么到底“浮”在了什么地方,与定点数相比的优势又是什么?总的看来,其实其转换操作很类似于十进制中的科学计数法,而科学计数法的出现,就是为了实现能简短地书写较大的数,比如写作 1.0x10^20,就可以避免在 1 后面写那令人抓狂的 20 个 0 了;

同理,二进制中如果用定点数表示小数,那么 32 位的话就最多到 32 位有效数字,而用这种类似科学技术的浮点表示法的话,指数能表示到 100 以上,也就是 100 多位了,相信现在的 100 位系统也是稀有物种了吧;因此,浮点数有效的扩大了能表示的数据范围,科学计数法减少了书写量,浮点表示则是节省了存储空间;

另外也是由于科学计数本身的特性,以及指数偏移值,也就是 阶码 的应用,小数点也就不再像之前一样固定,具体位置会根据指数的大小最终“漂浮”到不同的位置,甚至到那遥远的地方……

JavaScript之注释规范化(JSDoc)

2020年3月13日 08:00

前言

俗话说,无规矩不成方圆;虽说代码敲出来都是交给编译器解释执行的,只要不存在语法格式错误,排版无论多么反人类都是没有问题的,但是代码除了执行外的另一个广泛用途就是阅读了,翻阅自己过去的代码、理解别人的源码,等等;所以出现了代码风格化,美化外观的同时便于阅读,这就是目前 JSLint 等工具的作用;

当然,除了代码本身外,阅读更多的可能就是代码注释了,注释本身是不会被编译器编译执行的,其作用也是为了留下一些信息,方便更好的理解代码本身;所以,注释的规范化也是一个值得思考的问题;而接下来即将介绍的 JSDoc 就是这样的一款工具;

JSDoc

根据其官网(https://jsdoc.app/index.html)的介绍,JSDoc 是一个针对 JavaScript 的 API 文档生成器,类似于 Java 中的 Javadoc 或者 PHP 中的 phpDocumentor;在源代码中添加指定格式的注释,JSDoc 工具便会自动扫描你的代码并生成一个 API 文档网站(在指定目录下生成相关的网页文件);

生成 API 文档只是一方面,其更主要的贡献在于对代码注释格式进行了规范化,你可能没用过,但多半曾经在某个地方的源码中见过类似于下面的注释格式:

/**
 * Returns the sum of a and b
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function sum(a, b) {
    return a + b;
}

使用

工具的使用很简单,首先安装它:

npm install -g jsdoc

其次假设在一个名为 doc.js 的文件中书写以下代码:

/**
 * Returns the sum of a and b
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function sum(a, b) {
    return a + b;
}
/**
 * Return the diff fo a and b
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function diff(a, b) {
    return a - b;
}

然后就是在当前目录执行以下命令:

jsdoc doc.js

最后就会在当前目录下生成一个名为 out 的目录(也可以另外指定),当前目录内容就会变成像下面这样:

├── doc.js
└── out
    ├── index.html
    ├── doc.js.html
    ├── global.html
    ├── fonts
    │   ├── OpenSans-BoldItalic-webfont.eot 
    │   ├── OpenSans-BoldItalic-webfont.svg 
    │   ├── OpenSans-BoldItalic-webfont.woff
    │   ├── OpenSans-Bold-webfont.eot       
    │   ├── OpenSans-Bold-webfont.svg       
    │   ├── OpenSans-Bold-webfont.woff      
    │   ├── OpenSans-Italic-webfont.eot     
    │   ├── OpenSans-Italic-webfont.svg     
    │   ├── OpenSans-Italic-webfont.woff    
    │   ├── OpenSans-LightItalic-webfont.eot
    │   ├── OpenSans-LightItalic-webfont.svg
    │   ├── OpenSans-LightItalic-webfont.woff
    │   ├── OpenSans-Light-webfont.eot
    │   ├── OpenSans-Light-webfont.svg
    │   ├── OpenSans-Light-webfont.woff
    │   ├── OpenSans-Regular-webfont.eot
    │   ├── OpenSans-Regular-webfont.svg
    │   └── OpenSans-Regular-webfont.woff
    ├── scripts
    │   ├── linenumber.js
    │   └── prettify
    │       ├── Apache-License-2.0.txt
    │       ├── lang-css.js
    │       └── prettify.js
    └── styles
        ├── jsdoc-default.css
        ├── prettify-jsdoc.css
        └── prettify-tomorrow.css

通过浏览器访问这个 out 目录中的相关网页,就会展示类似于下面的页面内容;

主页:

jsdoc-home.png

指定函数页:

jsdoc-func.png

网页样式模板也可以更换,根据命令行参数修改即可,这里不再探究,下面主要来学习一下它的注释格式;

注释格式

完整的格式介绍请参考官网(https://jsdoc.app/index.html),目前版本是 JSDoc 3,下面只介绍几种常用的标签并配合举例;当然如果嫌手写一堆标签麻烦,现在许多编辑器(比如 VS Code)都提供了相关的插件下载,直接在插件中搜索关键词 jsdoc 就会出现许多,都是带提示或者自动识别当前代码生成的,很方便;

注释符

JSDoc 使用以下格式的注释符来对要添加的标签进行块级包裹:

/**
 * 
 * 
 */

即星号列垂直对其,第一行使用两个星号,每个星号后要添加一个空格再写内容,比如:

/**
 * 前面留一个空格,再写描述
 * 或者多行描述
 * @param {number} 关于该参数的描述
 */

行内包裹:

/** @function */

@description

也可写作 @desc,描述当前注释对象的详细信息;

/**
 * @function
 * @description 关于该函数的介绍内容
 */
function myFn() {}

/**
 * 也能在这里直接写介绍内容
 * @function
 * @description 如果这里又继续使用标签添加内容,则会覆盖第一行的介绍内容
 */
function myFn() {}

@file

注释写在文件开头,用于描述当前文件的相关信息;例如:

/**
 * @file 这是一个用于...的文件,包含了...功能
 */
 
// 然后是代码正文...

@author

描述当前文件或者代码的作者的相关信息;

/**
 * @author Jack <jack@example.com>
 */

@copyright

描述当前文件的版权相关信息

/**
 * @copyright Jack 2020
 */

@license

描述当前文件许可证相关信息;

/**
 * @license MIT
 */

或者是:

/**
 * @license
 * Copyright (c) 2015 Example Corporation Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * ...
 */

@version

描述当前项目的版本号;

/**
 * 这个版本修复了...问题
 * @version 1.2.3
 */

@since

描述某个功能是从哪个版本开始引入的;

/**
 * 提供了...功能
 * @since 1.2.1
 */
function newFn() {}

@see

类似于“另见”、“详见”的意思,引导至其他位置,也可以使用 @link 引导至某一网络地址;

/**
 * @see fn2
 */
function fn1() {}

/**
 * @see {@link http://example.com|some text}
 */
function fn2() {}

@todo

描述接下来准备做的事情;

/**
 * @todo 添加...功能
 * @todo 修复...bug
 */
function myFn() {}

@function

@func, @method 含义相同,描述一个函数;

/** @function */
var myFn = function() {}

@type

描述一个变量的类型;

/**
 * 一个对象类型的变量
 * @type {object}
 */
var val1 = {};

/**
 * 一个字符或者数字类型的变量
 * @type {(string|number)}
 */
var val2;

/**
 * 类型为数字或为空
 * @type {?number}
 */
var val3;

/**
 * 类型为数字或且不能为空
 * @type {!number}
 */
var val4;

/**
 * 一个 MyClass 类的实例数组
 * @type {Array.<MyClass>}
 */
var arr = new MyClass();

/**
 * 一个字符串的数组
 * @type {string[]}
 */
var arr2 = ['a', 'b', 'c'];

/**
 * 一个包含一个字符串和一个数字类型的对象
 * @type {object.<string, number>}
 */
var obj1 = {a: 'one', b: 2}

/**
 * 指定具体键和类型的对象
 * @type {{a: string, b: number}}
 */
var obj2 = {a: 'one', b: 2}

/**
 * 指定具体键和类型的命名对象
 * @type {object} obj3
 * @type {string} obj3.a
 * @type {number} obj3.b
 */
var obj3 = {a: 'one', b: 2}

@param

@arg, @argument 含义相同,描述一个函数的参数信息;

/**
 * 标签后跟参数类型,然后是参数名,最后是参数描述
 * @param {number} a 这里写变量的描述
 * @param {string} b - 或者加上连字符便于阅读
 * @param {string} c - 又或者这个参数有一个很长很长很长
 * 很长很长很长很长很长非常长的描述,可以这样占用多行
 */
function myFn(a, b, c) {}

/**
 * 传入的参数是个对象
 * @param {object} option - 传入的对象参数
 * @param {string} option.name - 对象的 name 属性
 * @param {number} option.age - 对象的 age 属性
 */
function myFn(option) {
    var name = option.name;
    var age = option.age;
}

/**
 * 传入的参数是个字符串组成的数组
 * @param {string[]} arr - 传入的对象参数
 */
function myFn(arr) {
    var name = option.name;
    var age = option.age;
}

/**
 * 表示某个参数是可选的
 * @param {number} a - 这是必填参数
 * @param {number} [b] - 这是可选参数
 * @param {number=} c - 可选参数的另一种表示
 */
function myFn(a, b, c) {}

/**
 * 表示可选参数的默认值
 * @param {number} a
 * @param {number} [b=3] - 默认值为 3
 */
function myFn(a, b) {}

/**
 * 参数类型的各种表示
 * @param {number} a - 类型为数字
 * @param {number|string} b - 类型为数字或字符串
 * @param {?number} c - 类型为数字或者为空(null)
 * @param {!number} d - 类型为数字且不为空
 * @param {*} e - 类型不做限制,即可以为任意类型
 */
function myFn(a, b, c, d, e) {}

/**
 * 表示具有任意多个参数的函数
 * 下面的函数返回所有传入参数的和
 * @param {...number} num - 参数个数任意,但是都是数字类型
 */
function sum(num) {
    var len = arguments.length;
    var result = 0;
    
    for (let i = 0; i < len; i++) {
        result += arguments[i];
    }
    return result;
}

@typedef

用于描述自定义的变量类型;

/**
 * 关于自定义类型的描述
 * @typedef {(string|number)} myType
 */

/**
 * 关于自定义类型的描述
 * @type {myType} val - 使用自定义的类型
 */
function myFn(val) {}

@callback

描述指定函数中作为回调函数的参数信息;

/**
 * 这是关于回调函数的描述
 * @callback myCallback
 * @param {string} aa - 回调函数接受的参数
 * @param {number} [bb] - 回调函数接受的另一个可选参数
 */
 
/**
 * 这是关于函数本身的描述
 * @param {string} a
 * @param {myCallback} callback - 回调函数
 */
function myFn(a, callback) {}

@returns

或者写作 @return,描述函数的返回值的信息;

/**
 * @param {number} a
 * @returns {number} 关于返回值的描述
 */
function myFn(a) {
    return a + 1;
}

/**
 * @param {number} a
 * @returns {(number|string)} 返回值可能是数字或字符类型
 */
function myFn2(a) {
    if (a > 1) {
        return 1;
    } else {
        return 'no.';
    }
}

@example

描述指定代码的使用示例;

/**
 * 添加示例代码(格式会被高亮展示)
 * @param {string} a
 * @param {string} b
 * @returns {string} return a concat b.
 *
 * @example
 * console.log(myFn('hello ', 'world!'));
 * // "hello world!"
 */
function myFn(a, b) {
    return a + b;
}

@class

描述一个 class 类;

/**
 * 关于该类的描述
 * @class
 */
class MyClass {}

/**
 * 或者是一个构造函数
 * @class
 */
function MyClass() {}
var ins = new MyClass();

@namespace

描述一个命名空间;

/**
 * 指定一个对象对命名空间
 * @namespace
 */
var MyNamespace = {
    /**
     * 表示为 MyNamespace.fn
     * @returns {*}
     */
    fn: function() {},
    /**
     * 表示为 MyNamespace.a
     * @type {number}
     */
    a: 1
}

/**
 * 手动指定命名空间
 * @namespace MyNamespace
 */
/**
 * 一个成员函数,MyNamespace.myFn
 * @function
 * @returns {*}
 * @memberof MyNamespace
 */
function myFn() {}

@member

描述当前类的一个成员;

/**
 * @class
 */
function MyClass() {
    /** @member {string} */
    this.name = 'knightyun';
    
    /**
     * 或者一个虚拟的成员
     * @member {number} age
     */
}

@memberof

描述成员所属的类;

/**
 * @class
 */
class MyClass {
    /**
     * @constructor
     * @memberof MyClass
     */
    constructor() {}
    /*
     * @param {string} val
     * @returns {*}
     * @memberof MyClass
     */
    myFn(val) {}
}

JavaScript 变量提升(Hoisting)详解

2019年9月2日 08:00

概念

变量提升是 JavaScript 的一种执行机制,大致就是字面意思,将声明的变量提前,但并不是指在编译时改变语句的顺序,而是将变量提前放入内存中,供后续操作,下面通过实例进行分析;

函数申明

在 JavaScript 中,声明一个函数并执行的话,通常会是以下形式:

function fn() {
    console.log('run');
}

fn();  // run

上面是正常的思维顺序,但是包括其他一些编程语言在内,通常会使用如下形式:

fn();

function fn() {
    console.log('run');
}
// run

这样做在执行上是没用问题的,同时可以在包含大量语句和函数申明的情况下,也可以使用这种特性将普通语句和函数申明分开,提高可读性;

以上情况便是一种常见的提升(Hoisting),即编译时提前将当前执行上下文包含的申明的函数,提前放入内存中,供全文语句执行时调用,为了方便理解而抽象成一种提升行为;

但是如果使用下面的方式申明函数并执行:

fn();

var fn = function() {
    console.log('run');
}
// TypeError: fn is not a function

这里就没有像上面一样的结果了,这属于下面将介绍的变量提升行为;

变量申明

当然函数只是一种类型的变量,还存在其他的变量类型,例如考虑以下语句:

var a = 1;
console.log(a);
// 1

逻辑和执行都是正常的,输出结果也是预期的,但是如果变一下顺序:

console.log(a);
var a = 1;

这种情况,通常可能会认为第一行调用了一个未定义的变量,然后输出 Uncaught ReferenceError: a is not defined 这样的错误,但是呢,并非如此,输出信息如下:

undefined

没错,就只有一个单独的 undefined,这种输出情况就类似于以下代码的执行:

var a;
console.log(a);
// undefined

从这里便可以大致分析出,前面的顺序怪异的代码,相当于在编译时提前将后面出现的变量申明提前,然后执行就输出了一个已申明但未 初始化(赋值) 的值,这便是其他类型的变量的提升行为,即在当前执行上下文中,将后面申明的变量提前放入内存,供前面的语句调用;

注意,前面的代码最后一行的语句是 var a = 1,即对变量进行了申明并赋值,但是最后输出仍然是 undefined 而不是 1,证明变量提升行为只会对变量进行申明操作,并不会对其初始化赋值,不管原语句是否有赋值操作;

然后便能解释之前的代码:

fn();

var fn = function() {
    console.log('run');
}
// TypeError: fn is not a function

这种情况便是将变量 fn 提升,值为 undefined,所以执行 fn() 语句会提示 fn is not a function 而不是 fn is not defined,与使用关键字 function 申明函数情况不一样;

拓展

return 的限制

另外值得一提的是,我们都知道 return 是函数内代码执行结束的标志,其后代码不会执行,但是提升行为却不受此限制,例如:

function fn() {
    console.log(a);
    fnn();
    return ;

    var a = 1;
    function fnn() {
        console.log('exist.')
    }
}

fn();
// undefined
// exist.

提升优先级

上面提到两种提升行为,那么它们的优先级顺序是如何的呢?还是通过代码说明:

function fn1() {
    console.log(a);
    var a = 1;
    function a(){};
}
function fn2() {
    console.log(a);
    function a(){};
    var a = 1;
}

fn1(); // f a() {}
fn2(); // f a() {}

结果证明函数的提升优先级始终高于普通变量的提升;

提升的执行

再来看一种情况:

function fn() {
    fnn();

    var a = 1;
    function fnn() {
        console.log(a);
    }
}

fn(); // undefined

这里按照正常的逻辑,申明函数 fnn() 之前就已经申明了变量 a,所以会感觉函数 fnn 应该可以访问变量 a,但是最后输出的并不是 1,输出 undefined 说明函数 fnn 并没有访问到赋值后的 a,并且所访问的 a 也触发了提升机制,因为输出的不是 RefferenceError,那么就能大致梳理出提升真 正的执行顺序了:

  1. 执行 fn();
  2. 执行 fnn();
    1. 发现前面没有关于函数 fnn() 的申明,于是向后寻找,最后找到了;
    2. 执行 console.log(a);
    3. 发现 fnn 内部没有 a 的定义,向外一层寻找;
  3. a 申明在 fnn() 执行语句以后,所以 a 触发提升,供之前的 console.log 使用;

因此上面的代码相当于是以下面的顺序执行的:

function fn() {
    console.log(a);
    var a = 1;
}

fn();

var, let, const的区别

JavaScript 中申明变量的方式以及对应效果如下:

a = 0;       // 全局变量
var b = 1;   // 局部作用域变量(当前上下文)
let c = 2;   // 块级作用域变量(当前块级上下文)
const d = 3; // 常量

作用域

这里解释一下,变量 a 申明时没有带任何关键字,默认其为全局变量;变量 b 申明带有关键字 var,为当前上下文的局部作用域,如果用在全局则为全局变量;变量 c 使用关键字关键字 let,d 使用关键字 const,二者都是ES6中新增的块级作用域申明,只不过 const 申明的是常量,值不可更改;

通过例子看一下它们的区别;

var a = '全局';
function fn() {
    var aa = '局部'
    console.log(aa);
}
if (true) {
    var b = '全局';
    let bb = '块级';
    const bbb = '块级';
}
for (i = 0; i < 1; i++) {
    var c = '全局'
    let cc = '块级';
    const ccc = '块级';
}

console.log(a);  // “全局”
fn();  // “局部”
console.log(aa); // aa is not defined
console.log(b, c); // “全局” “全局”
console.log(bb, cc); // bb is not defined  cc is not defined
console.log(bbb, ccc); // bbb is not defined  ccc is not defined

可以看出,var 的局部限于全局或者函数内部上下文,而 let 和 const 的块级的意思则是被 块(block) 所包含的上下文,也就是包含在花括号 {} 内部的作用域中,所以也包括函数在内,加上 if, for, while, switch 等情况,且不能被外部作用域访问;

全局变量提升

首先看申明全局变量时的提升行为:

console.log(a);
a = 0;
// ReferenceError: a is not defined

证明不带关键字的申明全局变量,似乎并没有执行变量的提升行为,与以下代码的执行无异:

console.log(a); // a 前面未申明
// ReferenceError: a is not defined

局部变量提升

使用关键字 var 申明的情况:

function fn() {
    console.log(aa);
    var aa = 1;
}
console.log(a);
var a = 1;
// undefined
fn();
// undefined

前面已解释,不再赘述,只是需要注意下面这种情况:

if (false) {
    var a = 1;
}
console.log(a);
// undefined

正常思维可能会理解 if 条件判断为假所以不会执行内部语句,最后会输出 a is not defined,然而并非如此,仍然将申明的变量执行了提升机制;这里可以简单理解为存在即提升,也就是为了避免以上问题的影响,所以出现了块级变量申明 letconst

块级变量提升

使用 letconst 的情况:

if (true) {
    console.log(a);
    let a = 1;
    console.log(aa);
}
// ReferenceError: Cannot access 'a' before initialization
// ReferenceError: aa is not defined

if (true) {
    console.log(b);
    const b = 1;
    console.log(bb);
}
// ReferenceError: Cannot access 'b' before initialization
// ReferenceError: bb is not defined

可以看出,块级变量申明似乎也执行了类似提升的机制,但是处理却与 var 有区别,这里是直接以错误的形式处理输出,提示该变量未进行初始化,而没有变量的申明语句的情况,则是提示未定义的错误,且 letconst 的处理情况一致;

JavaScript 实现斐波那契数列(Febonacci Array)

2019年9月2日 08:00

斐波那契(Febonacci)数列是一个神奇的数列,在很多地方都有应用,可以自行搜索相关图片体会其魅力,这里不赘述,直接来分析一下如何通过 JavaScript 来实现;

概念

斐波那契数列形式如下:

1 1 2 3 5 8 13 21 34...

规律应该很容易看出来,即从第三项开始,每一项的值等于前两项之和,以此类推下去,至于第一项和第二项的值嘛,不要纠结,就是这样规定的…

斐波那契数

首先来实现一下获取指定项的斐波那契数,即获取该数组中第 n 项的值;

方法一:

function getFebNum(n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return getFebNum(n - 1) + getFebNum(n - 2);
    }
}

方法一使用递归的思路,便于理解代码量也少,但是其算法复杂度较大,当 n 相当大的时候,程序运行也无比复杂;

方法二:

function _getFebNum(n) {
    if (n < 1) return 0;
    let one = 1, // 初始为第 -2 项
        two = 0, // 初始为第 -1 项
        three = 0; // 初始为第 1 项
    for (let i = 1; i <= n; i++) {
        three = one + two; 
        one = two;
        two = three;
    }
    return three;
}

这种方法算法复杂度就比较小了,只是换了个获取思路,代码量增加也不太容易理解,其中为了缩减代码量,便于递推的进行,把斐波那契数列向后模拟扩展了两项:

-2 -1 1 2 3 4 5
1 0 1 1 2 3 5

斐波那契数列

接下来通过代码实现获取指定长度的斐波那契数列:

方法一:

function getFebArr(n) {
    let arr = [];
    for (let i = 1; i <= n; i++) {
        arr.push(getFebNum(i));
    }
    return arr;
}

这个方法通过挨个获取斐波那契数,最后组成一个斐波那契数列,需要用到前面的 getFebNum 函数;

方法二:

function _getFebArr(n) {
    let arr = [];
    if (n < 1) return arr;
    let one = 1,
        two = 0,
        three = 0;
    for (let i = 1; i <= n; i++) {
        three = one + two;
        arr.push(three);
        one = two;
        two = three;
    }
    return arr;
}

方法二利用之前 _getFebNum 方法的思路,递推地填充斐波那契数列,降低了算法复杂度;

❌
❌