0%

Object.get这个方法,我觉得是在工作中比较常用的。而且ES6没有原生实现的。在之前的文章中。尝试用Reduce实现了一个类似的方法。可以在JS数组那篇文章中的末尾找到。我们来看一看Lodash是怎么实现的。

我们先来看看他的用法。这个函数的主要作用是,根据对象的路径获取值。

1
2
3
4
5
6
7
8
9
10
const object = { 'a': [{ 'b': { 'c': 3 } }] }

get(object, 'a[0].b.c')
// => 3

get(object, ['a', '0', 'b', 'c'])
// => 3

get(object, 'a.b.c', 'default')
// => 'default'

我们可以看出来,路径可以是一个字符串,可以是一个数组。我们最后还可以给一个默认值,当根据这路径没有找到对应值得时候,而返回默认值。

下面我们来看一下源码:

1
2
3
4
5
6
function get(object, path, defaultValue) {
const result = object == null ? undefined : baseGet(object, path)
return result === undefined ? defaultValue : result
}

export default

我们可以看到如果传入的对象为undefined或者根据路径查找出的结果为undefined的时候,就返回defaultValue。否则返回查找的结果。主要的逻辑还是在baseGet中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import castPath from './castPath.js'
import toKey from './toKey.js'

function baseGet(object, path) {
path = castPath(path, object)

let index = 0
const length = path.length

while (object != null && index < length) {
object = object[toKey(path[index++])]
}
return (index && index == length) ? object : undefined
}

export default baseGet

在baseGet中引入了两个方法。首先是这个castPath这个方法是强制转换为path数组。可以先看一下它的实现。

1
2
3
4
5
6
function castPath(value, object) {
if (Array.isArray(value)) {
return value
}
return isKey(value, object) ? [value] : stringToPath(value)
}

如果本身就是一个path数组,就返回value。如果不是的话,就调用isKey这个方法isKey这个方法主要是用来检测value是否是属性名,而不是属性路径。我们先不看里面的具体实现。就看最后的return,如果这个value是对象的名的话(也就是传入的这个value不带.或者[])我们就直接返回这个数组只包含value这一个值。否则返回通过stringToPath这个函数,返回的路径数组。

我们回到前面一个函数,我们看下面while部分的内容。

1
2
3
while (object != null && index < length) {
object = object[toKey(path[index++])]
}

如果object不为空,我们就一直取数组里面的值,然后通过数组里面的key找到对应的value。起初刚看这个时候,还在想直接用object=是不是修改传入的对象。后面才发现真的是基础不牢。其实在JavaScript函数的参数都是值传递,而传入对象也只是传入对象地址的值。我们在用object进行赋值的时候。实际是切断了与传入地址之间的关系。所以并不会改变原对象。这部分之后会单独开一篇文章进行讲解。剩下的核心就是toKey这个方法。
这个方法也很简单

1
2
3
4
5
6
7
8
9
const INFINITY = 1 / 0;

function toKey(value) {
if (typeof value === 'string' || isSymbol(value)) {
return value
}
const result = `${value}`
return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result
}

这个方法就是讲不是string和Symbol的key转为string。不得不佩服这种开源库想的真周到。
我们可以看到其中有一个非常冷面的知识点。
就是当你给一个变量赋值为+0或者0的时候,它都为0。而赋值为-0的时候它等于-0;

1
2
3
const a = 0; //console -> 0
const b = +0; //console -> 0
const c = -0; //console -> 0

而经过字符串转换的时候,0、+0、-0都会转换为0。Lodash居然考虑到了这一点,他用 1/value 是否等于 -(1/0)来判断传入的这个value是否为-0。从而修复了这一个问题。

又扯远了。我们回去继续看。其实核心代码很简单,就是一个不断根据key取值的一个过程。但是通过源码的学习。我们能学习到很多冷门的知识点和一些边界检测。这个过程也是十分有意思的。

在JS中数组的每个位置可以存储任意类型的数据。数组的长度也是动态的,会随着数据添加而自动增长。

数组对查询友好,通过下标随机访问时间复杂度为O(1)。
对插入和删除不友好,因为涉及到数组的移动,平均时间复杂度为O(n)

数组的创建

一是通过new Array构造函数的方式进行创建。

我们可以用它来创建指定的数组长度。
const arr = new Array(10)
这就创建了一个数组长度为10的数组。在使用构造函数时,省略new操作符结果也一样。
const arr = Array(10)

引用规范中的描述
When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.

另一种方法是通过使用数组字面量的方法。

1
2
3
let colors = ["red", "blue", "green"];  // 创建一个包含3个元素的数组
let names = []; // 创建一个空数组
let values = [1,2,]; // 创建一个包含2个元素的数组

在使用字面量表示法创建数组不会调用Array构造函数。

ES6 中 Array新增两个用于创建数组的静态方法, from()和of()。

from()用于将类数组结构转为数组实例。
of()将一组参数转换为数组实例。

form的使用场景有很多,比如:

  1. 字符串拆分数组,Array.from(string);
  2. 将Map、Set转换为数组。
  3. 对现有数组进行浅拷贝。
  4. 可以转换任何可迭代对象,或者类数组对象。
  5. 将arguments对象轻松地转换为数组。

Array.from() 还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像调用 Array.from().map() 那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函数中 this 的值。但这个重写的 this 值在箭头函数中不适用。

1
2
3
4
5
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
const a3 = Array.from(a1, function(x) {return x**this.exponent}, {exponent: 2});
console.log(a2); // [1, 4, 9, 16]
console.log(a3); // [1, 4, 9, 16]

Array.of() 可以把一组参数转换为数组。这个方法用于替代在ES6之前常用的Array.prototype.slice.call(arguments),一种异常笨拙的将arguments对象转换为数组的写法:

1
2
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined)); // [undefined]

数组的填充

如果我们要创建数组,并且要给数组中每个元素都填充上值。我们一般用fill方法。

1
const arr = (new Array(3)).fill(1)

我们要注意在二维数组的时候,这样初始化会有问题。举一个例子,

1
2
3
4
5
6
7
8
const arr =(new Array(3)).fill([]);
arr[0][0] = 1;
// 当你给他赋值之后,你会发现整列都会被改变
/*
0: [1]
1: [1]
2: [1]
*/

fill 传递一个入参时,如果这个入参的类型是引用类型,那么 fill 在填充坑位时填充的其实就是入参的引用。所以这三个数组对应了同一个引用。当你修改一个的时候其余两个都会改变。
所以最好用的方法也是最简单的方法,直接用for循环进行赋值。

数组的方法

数组的常见方法都很简单。我们可以简单地过一下

forEach 简单遍历,操作改变数组。
map 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

concat 合并两个数组,不改变,返回新
every 每一个元素都通过指定函数
some 至少有一个通过指定函数
filter 过滤 返回新

find 返回第一个符合指定函数的 元素值!
findIndex 返回第一个符合指定函数的 索引,没有返回-1

includes 方法用来判断一个数组是否包含一个指定的值,字符串区分大小写
indexOf 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
lastIndexOf 从后往前找,如果不存在 返回-1。

join 将数组拼成字符串

slice 从begin开始end结束浅拷贝,包含begin不包含end。返回新。
splice 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

reverse 数组颠倒,改变原数组

flat 数组拍平,参数是递归深度 返回新

sort 排序,如果没有指定排序函数,用Unicode位点进行排序。 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前;如果 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变(也可能会变)。如果 compareFunction(a, b) 大于 0 , b 会被排列到 a 之前。排序为就地算法不增加额外空间

reduce

唯一要详细讲解的就是这个reduce方法。
我们先看看Reducer的语法

1
array.reduce(function(accumulator, arrayElement, currentIndex, arr), initialValue)

方法对数组中的每个元素执行一个由 您 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。
accumulator为上一次迭代函数返回结果,在初始的时候,如果传了initialValue它的初始值就为initialValue,否则就为数组第一个元素。所以,假如数组的长度为n,如果传入初始值,迭代次数为n;否则为n-1。

我们来看看reduce有哪些用法。

  1. 数组求和。

    1
    2
    const arr = [1, 2, 3, 4, 5];
    const sum = arr.reduce((pre, cur) => pre + cur);
  2. 将数组转换为对象。
    一般后台返回的数据,都是数组中嵌对象,有时候我们需要按对象中某个字段如id进行查找。就会十分困难。这时我们就需要将数组转为对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const userList = [
    {
    id: 1,
    username: 'john',
    sex: 1,
    email: 'john@163.com'
    },
    {
    id: 2,
    username: 'jerry',
    sex: 1,
    email: 'jerry@163.com'
    },
    {
    id: 3,
    username: 'nancy',
    sex: 0,
    email: ''
    }
    ];

    const userObject = userList.reduce((pre, cur) => {
    return {...pre, [cur.id]: cur}
    }, {});
  3. 小数组展开成为大数组
    实现简单的数组拍平

    1
    2
    3
    4
    5
    6
    7
    const arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

    const _flatten = function (array) {
    return array.reduce(function (prev, next) {
    return prev.concat(Array.isArray(next) ? _flatten(next) : next)
    }, [])
    }
  4. 数组展开

    1
    2
    3
    4
    5
    const arr = ["今天天气不错", "", "早上好"];

    const arr1 = arr.reduce((pre, cur) => {
    return pre.concat(cur.split(''));
    }, [])
  5. 一次遍历中进行两次计算
    通过一次遍历计算出最大值最小值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const arr = [1,3,2,4,5,6,8,0];

    const initMinMax = {
    minValue: Number.MAX_VALUE,
    maxValue: Number.MIN_VALUE
    }

    arr.reduce((pre, current) => {
    return {
    minValue: Math.min(pre.minValue, current),
    maxValue: Math.max(pre.maxValue, current)
    }
    },initMinMax)
  6. 将映射和过滤合并为一个过程
    我们希望找到没有电子邮件地址的人的用户名,返回它们用户名用逗号拼接的字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const userList = [
    {
    id: 1,
    username: 'john',
    sex: 1,
    email: 'john@163.com'
    },
    {
    id: 2,
    username: 'jerry',
    sex: 1,
    email: 'jerry@163.com'
    },
    {
    id: 3,
    username: 'nancy',
    sex: 0,
    email: ''
    }
    ];

    userList.reduce((pre, cur) => {
    if (cur.email !== '') {
    pre = pre !== '' ? `${pre},${cur.username}` : cur.username;
    }
    return pre;
    }, "")
  7. 按顺序进行异步函数
    我们可以做的另一件事.reduce()是按顺序运行promises(而不是并行)。如果您对API请求有速率限制,或者您需要将每个prmise的结果传递到下一个promise,reduce可以帮助到你。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function fetchMessages(username) {
    return fetch(`https://example.com/api/messages/${username}`)
    .then(response => response.json());
    }

    function getUsername(person) {
    return person.username;
    }

    async function chainedFetchMessages(p, username) {
    // In this function, p is a promise. We wait for it to finish,
    // then run fetchMessages().
    const obj = await p;
    const data = await fetchMessages(username);
    return { ...obj, [username]: data};
    }

    const msgObj = userList
    .map(getUsername)
    .reduce(chainedFetchMessages, Promise.resolve({}))
    .then(console.log);
    // {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}
  8. 实现map函数

    1
    2
    3
    4
    5
    6
    Array.prototype.CustomMap = function(handler) {
    return this.reduce(function(pre, cur, index) {
    pre.push(handler.call(this, cur, index));
    return pre;
    }, []);
    }
  9. 实现filter函数

    1
    2
    3
    4
    5
    6
    7
    8
    Array.prototype.CustomFilter = function(handler) {
    return this.reduce(function(pre, cur, index) {
    if (handler.call(this, cur, index)) {
    pre.push(cur);
    }
    return pre;
    }, []);
    }
  10. 提取对象中的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    const obj = {
    "result": {
    "report": {
    "contactInfo": {
    "callsNumber": 0,
    "contactsNumber": 0,
    "emergencyContactHasOverdue": "No",
    "sameLiainson": {
    "isSame": "Yes",
    "accounts": "aa"
    }
    }
    }
    }
    };

    const objectGetVal = (obj, expr) => {

    if (!Object.is(Object.prototype.toString.call(obj), '[object Object]')) {
    throw new Error(`${obj}不是对象`);
    }
    if (!Object.is(Object.prototype.toString.call(expr), '[object String]')) {
    throw new Error(`${expr}必须是字符串`);
    }
    return expr.split('.').reduce((prev, next) => {
    if (prev) {
    return prev[next]
    } else {
    return undefined;
    }
    }, obj)

    }

实现一个reduce
最后我们来自己尝试实现一个reduce。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Array.prototype.myReduce = function(fn, initVal) {
if (Object.prototype.toString.call(this) != '[object Array]') {
throw new Error('当前是数组的方法,不能使用到别的上面');
}
let total;
if (initVal != undefined) {
total = initVal;
} else {
total = this[0];
}
if (initVal === undefined) {
for (let i = 1; i < this.length; i++) {
total = fn(total, this[i], i, this);
}
} else {
for (let [index, val] of this.entries()) {
total = fn(total, val, index, this);
}
}
return total;
};

当我们了解了一些React基本概念后,我们就可以尝试创建一个简单的React 应用。
创建一个React应用。创建一个React应用仅仅需要三步;

  1. 定义一个React Element
  2. 从DOM上获取一个节点
  3. 将React Element挂载到获取的节点上
    用代码展示一下
1
2
3
4
5
import react from 'React';

const reactElement = <div>Hello React</div>;
const root = document.getElementById('root');
ReactDOM.render(reactElement, root);

我们可以通过代码看到,第一行引入了react,但在下面的代码中并没有使用。但又必须得引入,这是为什么呢?大家应该都知道React中有一个JSX的概念。我们下面的reactElement其实就是通过JSX来定义的。
JSX是一个语法糖。可以用工具将它转为普通的JavaScript(如Babel)从而让浏览器正确的解析他。这个转换的过程非常简单。其实它就是通过调用react中的createElement传入一些参数(如标签名、props、children),从而返回一个对象。我们来手动的转换一下。

1
2
3
4
5
const reactElement = React.createElement(
"div", //标签名
{ title: 'hello' }, //props 我们可以给这个Element传一个title
"Hello React" // 这个元素的 children
)

React.createElement 它不光会创建一个对象,还会做一些验证。比如将字符串变量进行转义,从而防止XSS攻击。

返回的对象是什么呢? 我们可以暂且认为返回的是一个包含type和props的对象。真实返回的对象不只有这两个属性。我们现在只需要关注这两个属性即可。

1
2
3
4
5
6
7
const reactElement = {
type: 'div',
props: {
title: 'hello',
children: 'Hello React'
}
}

这个对象中type就是指定要创建DOM元素的标签名,props对象上存的是JSX上的所有属性,还有一个就是chlidren存的是JSX中的子元素。可以是string也可以是Element数组。

第二行获取一个DOM元素,本身就是JavaScript代码。这部分不需要多说。

我们看看下面这行代码ReactDOM.render(reactElement, root)
我们来自己实现一下render方法。

我们先通过type创建一个DOM元素。并将所有属性赋给这个元素。

const node = document.createElement(reactElement.type);
node["title"] = reactElement.props.title

然后我们再创建子元素。我们这个例子是一个string,我们就简单地创建一个text node

const text = document.createTextNode("")
text["nodeValue"] = reactElement.props.children

最后将textNode添加到div上,然后再将div加到root上。

node.appendChild(text)
root.appendChild(node)

最后我们看下完整代码:

const reactElement = {
    type: 'div',
    props: {
        title: 'hello',
        children: 'Hello React'
    }
}

const root = document.getElementById('root');

const node = document.createElement(reactElement.type);
node["title"] = reactElement.props.title;

const text = document.createTextNode("");
text["nodeValue"] = reactElement.props.children;

node.appendChild(text);
root.appendChild(node);

至此,我们就完全搞清楚了如何创建React应用。以及如何不用React创建一个一模一样的简单应用。

unqiueId([prefix=’’])

这个方法主要作用是,生成唯一ID。如果提供了prefix,会被添加到ID前缀上。

例子

1
2
3
4
5
_.uniqueId('contact_');
// => 'contact_104'

_.uniqueId();
// => '105'

这个方法源码很简单,我们直接上源码。解析直接以注释的方式呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
创建一个全局的对象,用来生成唯一的id。
*/
const idCounter = {}

function uniqueId(prefix='$lodash$') { //如果prefix没有传的话,默认参数为$lodash$
/*
初始化,如果prefix在idCounter中不存在的话。
*/
if (!idCounter[prefix]) {
idCounter[prefix] = 0
}

const id =++idCounter[prefix] //每次调用的时候id+1,由于是++在前,所以id是从1开始。

/*
如果没有传prefix,最终返回id,如果传了,则拼接返回。
*/
if (prefix === '$lodash$') {
return `${id}`
}

return `${prefix}${id}`
}

export default uniqueId

转换成Number

type result
Undefined NaN
Null 0
Boolean 如果是true返回1,false返回0
Symbol 抛出TypeError

String转Number

只支持合法的字符串数字,数字和字母的组合就不行。如果数字前面有空格,或者空格,或者前后都有空格算是合法的。

1
2
3
4
Number('a') // NaN
Number('1a') // NaN
Number(' 1') // 1
Number(' 1 ') // 1

字符串到数字,也只支持十进制、二进制、八进制和十六进制。

1
2
3
Number('0b10') // 2
Number('0o10') // 8
Number('0x10') // 16

+Infinity和-Infinity也算合法Number型字符串。

1
2
Number('+Infinity') //Infinity
Number('-Infinity') //-Infinity

此外还支持科学计数法。用大写或者小写的e表示

1
2
Number('1e1') // 10
Number('1E1') // 10

转换String

type result
Undefined “undefined”
Null “null”
Boolean true转成”true”、false转成”false”
Symbol 抛出一个TypeError异常

Number转String

  1. NaN转为”NaN”
  2. +0或者-0转成”0”
  3. 比0小, 返回由”-“和 ToString(-Number)组成的字符串.
  4. 如果当Number绝对值较大或者较小的时候,字符串表示则是使用科学计数法表示的。

转成Boolean

type result
Undefined false
Null false
Number +0、-0、NaN返回false,其余的都返回true
String 空字符串返回false,其余的都返回true
Symbol true
BigInt 0n返回false,其余的都返回true
Object true

拆箱转换

对象到String和Number的转换都遵循”先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
到 String 的拆箱转换会优先调用 toString。其实String的toString和valueOf是一样的。

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

1
2
3
4
5
6
7
8
9
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}

console.log(o + "")
// toPrimitive
// hello

装箱转换

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

隐式装箱:

  1. 创建基本类型的一个实例;
  2. 在实例中调用制定的方法;
  3. 销毁这个实例。

在隐式装箱过程中会频繁创建临时对象。

显式装箱:

直接用new操作符进行创建实例。

强迫装箱:

Symbol和BigInt类型不能通过new来创建,这时可以定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产一个symbolObject。

1
2
3
4
5
6
var symbolObject = function() {
return this
}.call(Symbol("a"))
console.log(typeof symbolObject) //object
console.log(symbolObject instanceof Symbol) //true
console.log(symbolObject.constructor == Symbol) //true

八种类型

JavaScript语言规定了8种类型,每一个值都属于某一种数据类型。

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Symbol (ES2015/ES6新增)
  7. BigInt (ES2020/ES11新增)
  8. Object

其中Symbol是ES2015中新添加的,我们就先从这这个最不熟悉的类型开始讲解。而ES2020中新添加的BigInt,我们放在Number类型之后讲。

Symbol

symbol代表唯一的标识。
Symbol()函数会返回一个symbol类型的值。创建时,我们可以给symbol一个描述,就像这样Symbol("description"),这主要的目的是用来debugging。你无需知道这个值具体是什么,你只用知道它与任何值都不相等就行,哪怕我们给他相同的描述,它也是不同的值。

1
2
3
4
let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

如果我们直接去console一个Symbol,它会报错TypeError: Cannot convert a Symbol value to a string,因为Symbol不能隐式转换成string,我们可以用toString()进行显示转换,也可以通过Symbol的description属性来去到他的描述

1
2
console.log(id1.toString());  // Symbol(id)
console.log(id1.description); // id

Symbol的作用

Symbol的作用只有一个,作为对象属性的唯一标识符。我们知道对象的key,只有可能是string和symbol,所以我们也可以说它是非字符串对象key的集合。
其最主要的目的就是防止对象属性发生冲突。

我们通过几个例子来理解一下:

1
2
3
4
5
6
7
8
const obj1 = {
a: 'aaa',
b: 'bbb',
}

const obj2 = {
a: 'a'
}

我们对上面两个对象进行合并的话,属性a就会被覆盖。就会变成这样。

1
2
3
4
{
a: 'a',
b: 'bbb'
}

如果我们不想在整合的时候因为冲突而产生覆盖,用Symbol就能解决这个问题。

1
2
3
4
5
6
7
8
const obj1 = {
[Symbol('a')]: 'aaa',
b: 'bbb'
}

const obj2 = {
[Symbol('a')]: 'a'
}

这样合并后的结果就是

1
2
3
4
5
{
Symbol(a): 'aaa',
Symbol(a): 'a',
b: 'bbb'
}

因为Symbol()返回值是唯一的,也就是: Symbol('a') === Symbol('a') 的返回值为false。

在举一个例子,在开发中我们经常会遇到这样一个需求。根据传入不同的参数去做不同的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

const obj = {
a: 'a',
b: 'b',
c: 'c'
}

function handler(type) {
if (type === obj.a) {
...
} else if (type === obj.b) {
...
} else if (type === obj.c {
...
}
}

我们在调用的时候传入obj.a或者obj.bobj.c,这样的话其实我们并不用关心obj对象中a、b、c具体的值,他们各自的值是多少无所谓,只要他们直接互不相同就行。这是Symbol就很好地满足了这个需求。我们就可以把obj对象写成这样

1
2
3
4
5
const obj = {
a: Symbol(),
b: Symbol(),
c: Symbol()
}

获取Symbol()对应属性的值

在symbol和对象使用的时候我们就需要[]了。

1
2
3
4
5
6
7
8
9
10
let s = symbol();

const obj = {
s = 1,
[s] = 2
}

console.log(obj.s) // 1
console.log(obj['s']) // 1
console.log(obj[s]) // 2

Symbol 与 for…in

Symbols在for…in中不可枚举,Object.keys也是枚举不到key的。如果想要枚举的话,可以借助Object.getOwnPropertySymbols(obj)这个方法。Reflect.ownKeys(obj) 这个方法会返回对象所有key的数组,使用这个相当于使用
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

Symbol 与 JSON.stringify()

当使用JSON.stringify()的时候,symbol作为key也会被忽略。

全局Symbol

我们知道,通常symbol是不同的。但是有时候我们希望相同名称的symbol的值是相同的。symbol提供了一个全局注册表。使用Symbol.for(key),它会从注册表里去读取,如果这个symbol不存在,它便会创建一个。它是全局可用的,甚至跨service worker、iframe。

1
2
3
4
5
6
7
8
// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
alert( id === idAgain ); // true

还有一个是与Symbol.for(key)相反的操作Symbol.keyFor(sym),它是传入全局symbol变量,返回它的name,注意的是它只适用于全局的Symbol,非全局的Symbol会返回undefined。

1
2
3
4
5
6
7
// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// get name by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Undefined

表示未定义,任何变量在未赋值之前都是Undefined类型、值为undefined。它与null还是有一些区别,null表示定义了但是值为空,undefined表示根本就没有定义。这里有一个要注意的问题,undefined在JavaScript中不是一个关键词,它可能会被改写的。一般建议用void 0来获取undefined的值。因为 void 运算会把任意一个表达式变成 undefined,也就是说不管void后面跟什么返回的都是undefined。不少 JavaScript 压缩工具在压缩过程中,也是将 undefined 用 void 0 代替掉了。

Null

Null表示空值。前面也说过它代表着定义了但是值为空,它与undefined不同,null是JavaScript的关键词,可以放心的使用。

Boolean

这个很简单就两个值,true和false。

String

在JavaScript中最重要的一点就是,String是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
ES7确定了字符串的最大长度为2^53-1。但是在老版本V8中字符串最大长度限制在256MB(2^28 - 16),而在新版本中32位平台还与之前一样,64位平台的最大长度将限制在2^(32-1)-1-24大约1GB。
但是我们要知道JavaScript中String的最大长度,并不是字符数。
要讲清楚这个概念,我们就不得不引入码点和代码单元的概念。
码点是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如U+0041 就是拉丁字母A 的码点。Unicode的码点可以分成17个级别。
第1个代码级别称为基本字符区域(BMP),(U+0000 - U+FFFF),在基本字符区域中,每个字符用16位表示,通常被称为代码单元,换句话说也就是在基本字符区域内一个码点需要一个代码单元,一个码点对应一个字符。
而其余的16个级别,码点从U+10000 到U+10FFFF , 其中包括一些辅助字符(supplementary character)。而辅助字符采用一对连续的代码单元进行编码。也就是需要两个代码单元。
总结一下:一个字符对应一个码点(且每个字符的码点都是唯一的),但一个码点可能需要一个代码单元或者两个代码单元才能表示。
而JavaScript字符串把每个UTF16单元当做一个字符来处理,所以处理非BMP的时候就需要注意。比如 “𝌷”的length就为2。
一个空的字符串的length为0.
如果你要是直接访问String.length返回的是1

1
console.log(String.length) // 1

这是因为String中的这个length其实继承于Function.prototype的。函数实例的length为声明的参数长度,String接受一个value参数,所以length为1。

Number

在JavaScript中Number这种类型使用 IEEE754 格式来表示
整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。接下来我们讲讲什么是IEEE754

IEEE754

我们先记住几个基础知识,十进制小数点向左移动1位相当于将该数除以10,向右移动1位相当于将该数乘以10。同理,二进制小数点向左移动1位相当于将该数除以2,向右移动1位相当于将该数乘以2。

我们可以根据上面的概念,引入科学计数法。一个很大或者很小的数可以表示为:

1
2
3
a * 10^n
|a|>=1 且 |a|<10
n为整数

如果用科学记数法来表示二进制数:

1
2
3
4
a * 2^n
指数基数为2
|a|>=1且|a|<2,也就是说a的范围包含两个区间(-2,-1]、[1,2)
n为整数

浮点数

在计算器科学中,浮点是一种对于实数的近似值数值表示法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数。

IEEE 754规定了四种表示浮点数值的方式

  • 单精度(32位)
  • 双精度(64位)
  • 延伸单精度(43比特以上,很少使用)
  • 延伸双精度(79比特以上,通常以80位实现)

JavaScript使用的是双精度浮点型。

二进制浮点数由三部分组成

  1. 符号位s(sign bit),0表示正数,1表示负数。
  2. 阶码e(exponent bias),规定为实际指数值加上一个偏移值。偏移值为2^(n-1)−1,其中的n为存储指数的比特位长度。
    由于科学记数法的指数可为正,可为负。因此,一个数转换为浮点数之后,符号位、阶码这两部分都是带符号的。
    如果我们要比较两个浮点数的大小,那么除了要判断比较数值本身的符号位,还需要再判断比较阶码的符号位,最后才是非符号部分的比较。显然,这会复杂化比较逻辑。
    在使用了偏移值之后,无论指数部分是正是负,都可以转换为非负数。将真值映射到正数域的数值(真值在数轴上正向平移一个偏移量),称为移码。使用移码来比较两个真值的大小比较简单,只要高位对齐后逐位比较即可,不用考虑符号位问题。
  3. 尾数f(fraction),用于存储“有效数字”的小数部分,使用原码表示。

在64位双精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占11bit
  3. 尾数,占52bit

在32位单精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占8bit
  3. 尾数,占23bit

IEEE 754计算

  1. 如果指数位全为1,且尾数不为0,就超过了最大值,这样的数就无法表示,表示为NaN,此时无视符号位。这就是为什么NaN != NaN。因为NaN有不同的可能。
  2. 如果指数位全为1,尾数为0的话,根据符号位得到两个特殊值,符号位为0就是正无穷Infinity,符号位为1的话就得负无穷-Infinity。
  3. 如果指数位和尾数位都为0的话,就会根据符号位表示正负0。
  4. 在正常的浮点数区间,那么对应的数字的计算公式为:(-1)^s x(2^(e-1023))x(1.f) 对于e-1023和1.f可能会有些疑惑。其中 1023 代表指数的偏移量,那么为什么会有这个偏移量呢,首先,作为11位的阶码,如果是无符号,那么表示的范围为[0-2047], 当处于我们第三种情况时,e的范围要去掉0和2047,变成[1,2046] ,如果要表示正负,则需要使用一个bit去作为符号位,范围为[-1022, 1023] ,如果没有偏移量,那么需要引入补码,计算更加复杂,为了简化运算,则使用无符号的阶码,引入了偏移量的概念,通过偏移将其转换成[1,2046] ,所以偏移量为 1023。 那么对于1.f来说,因为对于浮点数,在规范中采用科学计数法的方法,对于十进制12.34来说,用科学技术法表示为 1.234×10^2,不会表示为 0.1234×10^3,其首位肯定是一个不为0的数字,那么在二进制中,只有0和1,那么他的首位就只能是1(用科学计数法表示,首位不能为0)。因为对于所有的浮点数他的首位都是1,因此可以省略一位进行,不需要额外占用1位,所以f实际上的有效位数有53位(其中首位是1)。在计算时需要使用上这个隐含的1,这也是1.f的由来.
  5. 当指数为0且尾数不为0时,是非规范化的数字,用于表示那些非常接近0的数字。

从上面的分类我们得知,前两种是非正常的值,第一种含有 2^52 种情况(尾数有52位),第二种含有两种情况(正无穷和负无穷),那么根据排列组合,共有 2*1*2^52 = 2^53 种情况。这些非正常的值在JS中以 NaN、+Infinity、-Infinity表示,因此在Number中多了三种情况。那么总数就是 2^64-2^53+3

Number数值范围

Number的数值范围为-2^53 到 2^53。由上可知在双精度浮点型,尾数占52bit又由于整数位的第一位肯定为1,所以第一位可以省略掉,从而多了一位。

浮点数精度问题

因为十进制小数转成二进制会变成无限循环小数,它就变得不准确了。
从十进制数转为二进制数,小数的转换采用的乘2取整的过程。这必然就会有无限不循环的情况产生。这样用计算机存储产生截取是必然的,必定会有一定的精度损失!

遇到浮点数的比较,正确的方法应该是使用JavaScript提供的最小精度值:

1
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。

BigInt

这个是ES2020新增的,我们从上得知Number的最大整数是2^53-1。如果超过了这个数就会有问题。而BigInt的出现就再也不用担心这个问题。

创建BigInt的方式也很简单

  1. 直接在整数后面加n
    let a = 123n;
  2. 调用BigInt函数
    let a = BigInt(123);

BigInt运算

就像Number运算一样,只不过在做除法运算的时候会舍去小数部分。

1
2
1n + 2n  // 3n
1n / 2n // 0n

我们要注意BigInt类型和Number类型不能混用。
在BigInt和Number直接可以用BigInt()和Number()进行显示转换。但是如果BigInt超过了Number,会进行截断。
BigInt也不支持一元加法,我们知道可以用+”value”,将String类型的转为Number类型。这在BigInt中并不支持。

比较运算符,例如 < 和 >,使用它们来对 bigint 和 number 类型的数字进行比较没有问题。
由于 number 和 bigint 属于不同类型,它们可能在进行 == 比较时相等,但在进行 ===(严格相等)比较时不相等。

在进行布尔运算时与Number一样,BigInt 0nfalse,其他值为 true

Object

Object就是一组数据和功能的集合。对象通过new操作符后面跟着要创建的对象类型的名称来创建。

let o = new Object();  //如果不传参数的话,可以省略圆括号但是不推荐。

Object是所有类的基类。Object类所具有的任何属性和方法也同样存在于更具体的对象中。Object的每个实例都具有下列的属性和方法。

  1. constructor
    用于创建当前对象的函数,也就是构造函数。
  2. hasOwnProperty
    检测给定的属性是否在当前对象实例中,可以是String也可以是Symbol。
  3. isPrototypeOf
    检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中。就是说如果B继承与A,那么A就是B的原型。A.isPrototypeOf(B) === true
  4. propertyIsEnumerable
    此方法可以确定对象中指定的属性是否可以被 for…in 循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回 false。
  5. toLocaleString
    返回对象的字符串表示形式,针对当前语言环境进行了本地化。但只有Array、Date和Number重写了此方法。其他的只是默认调用toString方法。
  6. toString
    每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。
  7. valueOf
    valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值
    相同。

Number、String和Boolean,这三个构造方法,在new的时候产生与其基本类型对应的对象类型。而直接调用的时候表示强制类型转换。

Symbol、和BigInt比较特殊,直接用new的时候回报错。

我们发现有时候对象可以直接用在基本类型上,比如字符串字面量可以直接调用String类型的方法,这是因为
JavaScript运算符提供了装箱操作,他会根据基础类型构造一个临时对象,使我们能在基础类型上调用对于对象的方法。

什么是语义化?

语义化说白了,它是给机器看的,就是让机器能读懂你的内容,让机器知道你哪里是标题,哪里是段落。正因为如此。语义化最大的优点就是利于搜索引擎的抓取和建立索引也就是俗称SEO。除此之外,正确的使用语义化标签能够清晰的展示网页的结构,对开发者也更加友好,除此之外语义化还可以支持读屏软件使一些视障人士也可以无障碍的使用我们的网站。

语义化初步

我们来写一个最简单布局,这个布局就包括,头部,内容,和尾部从上到下依次排列。这也是最常见的网页布局。在没有语义化得时候,我们通常是使用div和给一个相对含义的className或者id的形式来布局

1
2
3
<div id="header></div>
<div class="section"></div>
<div id="footer"></div>

我们用语义化的形式来改写它。

1
2
3
<header></header>
<section></section>
<footer></footer>

这样看起来结构就清楚了很多。通过上面的例子我们也可以很容易看出,语义化可以使我们写出一致的代码。在不使用语义化标签的时候一个header就有很多种写法<div class="header"><div id="header">等等,这完全取决于每个开发者个人的风格。而使用语义化标签后,就让代码一致性变得更加的容易。

语义化标签介绍

<header> 和 <hgroup>

header标签很好理解,一般都在文档的最顶部。通常包含标题以及导航和搜索工具。它不一定非得是网页的头部,也可以是任意section或者article的头部部分。它也没有个数限制。
我们知道h1-h6是最基本的标题,表示文章中不同层级的标题。但有时候我们想让副标题在同一个层级中,这是我们就需要用到hgroup标签。hgroup标签实际就是一个标题组。

<nav>

nav 元素代表页面的导航链接区域。用于定义页面的主要导航部分。导航部分的常见示例是菜单,目录和索引。

1
2
3
4
5
6
7
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>

<section> 和 <article>

article它表示具有一定独立性质的文章,它的特点就是独立和可重用。每个article里面都可以有自己的header、section、footer。

1
2
3
4
5
6
7
<article>
<h1>你好,我是这边文章的标题</h1>
<p>你好,我是文章的内容</p>
<footer>
<p>最终解释权归XXX所有</p>
</footer>
</article>

section的主要作用是按主题分段。也就是说section中的内容跟大的主题是相关的。而且在section中会自动给标题h1-h6降级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<article>

<h1>web语义化</h1>
<p>什么是语义化?</p>

<section>
<h2>语义化详解</h2>
<p>语义化就是。。。</p>
</section>

<section>
<h2>语义化特点</h2>
<p>语义化特点就是。。。</p>
</section>

</article>

<aside>

aside一般是跟文章主题不相关的部分,可能包含导航或者广告之类的东西。一般在aside中的nav多数是关联页面,它与header中的nav有些许不同。

一般有header标签,也会有footer标签。通常他都是在文档的最后,通常都是放一些作者信息,版权信息,友情链接之类的。

<small>

<small>标签呈现的小号字体的效果,表示注释和附属细则,一般在footer中,包裹着版权信息。

1
<footer><small>&copy;Company A</small> Date</footer>

<time>

在出现日期的地方,我们可以加上

1
<time datetime="2020-11-08T11:21:00+08:00">2020年11月8日,星期二</time>

<figure> 和 <figcaption>

当图片不是独立的出现,当它与主文章相关,我们就可以使用figure标签。这种插入不限于图片,也可以是代码、表格等。figcaption表示内容的标题。

1
2
3
4
<figure>
<img src="#" />
<figcaption>title</figcaption>
</figure>

我们以一个Button组件为例。尝试从设计到实现一个React组件。

按钮的设计

轮廓线(按钮的边框)

主要的作用就是按钮与背景色相近的时候让按钮更加的明显。
成像效果不好的显示器,显示效果会有偏差。加上轮廓线确保显示。

类型检测

静态类型检测是写组件必不可少的。一方面是在开发时避免类型问题,它还可以通过IDE的智能提醒方便使用组件的开发者。将使用Typescript对组件进行类型检测声明。

Props声明

首先是Props声明,Props声明我们用组件名后面跟Props的形式来定义接口名称,并将它导出。

1
2
3
4
5
export interface BaseButtonProps {
type?: ButtonType;
children?: React.ReactNode;
...
}

在定义props的时候,我们要保持接口小,props数量要少。

组件声明

在组件使用中,应该无状态组件>有状态组件>Class组件。尤其是在hook出现后,函数组件被React大肆推广。

函数组件

我们用FC来声明函数组件,FC就是FunctionComponent的缩写,我们可以通过源码看到,它的props默认定义了children。

1
2
3
const ButtonBase:FC<BaseButtonProps> = props => {
return <div>{props.children}</div>
}

然后将这个组件导出

1
export default Button;

但是在之前FC类型来声明函数组件时并不能完美支持propsDefault(现在已经解决)。
一般用解构赋值的默认值来代替propsDefault

1
2
3
4
const BaseButton:FC<BaseButtonProps> = props => {
const { type = "default" } = props;
return <div>{props.children}</div>
}

也有直接使用普通函数来进行组件声明

1
2
3
4
const BaseButton = (props: BaseButtonProps): JSX.Element => {
const { type = "default" } = props;
return <div>{type}</div>
}

用普通函数来进行组件声明还有以下好处:

  1. React.FC隐式的提供了一个children props,如果你的组件不需要子组件,用普通函数组件也许是一个很好的选择
  2. 支持泛型。例如

    1
    2
    3
    4
    5
    6
    7
    type OptionValue = string | number;

    type Option<T extends OptionValue> = {
    value: T;
    };

    const App = <T extends OptionValue>(props: Option<T>): JSX.Element => <div>{props.value}</div>
  3. 在使用defaultProps不会出错。

【注意】在使用普通函数来进行组件声明的时候,defaultProps的类型和组件本身的props没有关联性,会使得defaultProps无法得到类型约束。比如

1
2
3
4
5
6
7
interface ButtonProps {
name: string;
}

const Button = (props: ButtonProps) => <div>{props.name}</div>

Button.defaultProps = { name: 123 } //这样是不会报错的

我们需要改写成这样

1
2
3
4
5
type Partial<T> = {
[P in keyof T]?: T[P];
};

Button.defaultProps = { name: 'Liam' } as Partial<ButtonProps>

概念

树的结构就很像生活中的树,一个枝干上分出很多的树杈。 在数据结构中,我们将树中的每个元素称为节点。没有父节点的叫做根节点。没有子节点的节点叫做叶子节点。如果多个节点的父节点是同一个节点,那么我们就将它们称为兄弟节点。
树还有 高度、深度、层。这三个概念

  • 节点的高度: 节点到叶子节点的最长路径,也就是边数
  • 节点的深度: 根节点到这个节点所经历的边的个数
  • 节点的层数: 节点的深度+1
  • 树的的高度: 根节点的高度

二叉树

每个节点上最多有两个叉。

满二叉树

除了叶子节点之外,每个节点都有左右两个子节点。

完全二叉树

最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。

存储方式。

  1. 链式存储法。 每个节点有三个字段,一个存储数据,另外两个是指向左右节点的指针。
  2. 数组存储法。根节点在i=1的位置,左节点存储在2 i的位置,右节点存储在2 i + 1的位置。反过来 i/2存储的就是父节点的位置。 如果是完全二叉树,数组存储方式是最不占空间的。堆就是一种完全二叉树。

二叉树的遍历

  • 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
  • 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
  • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

在之前的文章中,介绍过JavaScript的typeof运算符。在JavaScript中typeof主要是用于类型检测。
而在TypeScript中typeof不光能类型检测,还能用来获取类型。
具体是什么,得结合环境来看。
举一个简单地例子

1
2
3
let bar = { a: 0 }
let b = typeof bar // "object"
type c = typeof bar // "{a: number}"

我们可以看出来,如果typeof赋值给一个变量的话,那将用JavaScript运算符。 如果赋给一个type的话,就是获取它的类型。TypeScript将其对象中的a属性自动推断为number类型。

也可以获取类型通过一个字符数组

1
2
3
const data = ['text 1', 'text 2'] as const;
type Data = typeof data[number];
// type Data = "text 1" | "text 2"

或者是一个tuple

1
2
3
4
5
6
export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

const animals = tuple('cat', 'dog', 'rabbit', 'snake');
type Animal = (typeof animals)[number];
//'cat' | 'dog' | 'rabbit' | 'snake'

还可以与keyof结合,将对象的key转为联合类型。

1
2
3
4
5
6
7
const datas = {
key1: 'value1',
key2: 'value2',
key3: 'value3',
}
type data = keyof typeof datas;
// type data = "key1" | "key2" | "key3"