0%

8 种数据类型

最新的 ECMAScript 标准定义了 8 种数据类型:

7 种基本类型

  • Number
  • String
  • Boolean
  • undefined
  • null
  • Symbol
  • BigInt

1 种引用类型

  • Object

Boolean

布尔类型可以有两个值:true 和 false。

Null

Null 类型只有一个值: null。

Undefined

通过 var 声明的变量,在没有被赋值之前,变量会有个默认值 undefined。

Number

Number 的范围:[-2^53, 2^53]。
特殊的 Number:Infinity, -Infinity, NaN。
数字类型中只有 0 一个整数有 2 种表示方法: 0 可表示为 -0 和 +0(0 是 +0 的简写)。 在实践中,这也几乎没有影响。 例如 +0 === -0 为真。 但是,在除以0的时候要注意:

1
2
3
1 / +0; // Infinity
1 / -0; // -Infinity
-1 / -0; // Infinity

BigInt

BigInt 表示任意精度的整数。使用 BigInt,可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。BigInt 是通过在整数末尾附加 n 或调用构造函数来创建的。

1
2
const x = 2n ** 53n; // 9007199254740992n
const y = x + 1n; // 9007199254740993n

BigInt 不能与数字互换操作。否则,将抛出 TypeError。

String

不同于类 C 语言,JavaScript 字符串是不可更改的。这意味着字符串一旦被创建,就不能被修改。

但是,可以基于对原始字符串的操作来创建新的字符串。例如:

  • 获取一个字符串的子串可通过选择个别字母或者使用 String.substr()
  • 两个字符串的连接使用连接操作符 +

Symbol

出现的原因

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。Symbol 值通过Symbol() 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

1
2
let s = Symbol();
console.log(typeof s); // symbol

注意:Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。

描述

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

1
2
3
4
5
6
7
8
let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1); // Symbol(foo)
console.log(s2); // Symbol(bar)

console.log(s1.toString()); // "Symbol(foo)"
console.log(s2.toString()); // "Symbol(bar)"

如果 Symbol 的参数是一个对象,就会调用该对象的 toString() 方法,将其转为字符串,然后才生成一个 Symbol 值。

1
2
3
4
5
const obj = {
name: 'Jack'
};
const s = Symbol(obj);
console.log(s); // Symbol([object Object])
1
2
3
4
5
6
7
8
const obj = {
name: 'Jack',
toString() {
return 'hello'
}
};
const s = Symbol(obj);
console.log(s); // Symbol(hello)

注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值也是不相等的。

1
2
3
4
5
6
7
8
9
10
11
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

console.log(s1 === s2); // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

console.log(s1 === s2); // false

类型转换

Symbol 值不能与其他类型的值进行运算,否则会报错。

但是,Symbol 值可以显式转为字符串。

1
2
3
let s = Symbol('My symbol');
console.log(String(s)); // 'Symbol(My symbol)'
console.log(s.toString()); // 'Symbol(My symbol)'

另外,Symbol 值也可以转为布尔值。

1
2
3
let s = Symbol();
console.log(Boolean(s)); // true
console.log(!s); // false

应用

Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

1
2
3
4
5
6
7
8
9
10
let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};

注意,Symbol 值作为对象属性名时,不能用点运算符。

1
2
3
4
5
6
const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
console.log(a[mySymbol]); // undefined
console.log(a['mySymbol']); // "Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。

同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

1
2
3
4
5
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);

遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。
但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

1
2
3
4
5
6
7
8
9
10
11
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]

Symbol.for()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。比如,如果你调用 Symbol.for(“cat”) 30 次,每次都会返回同一个 Symbol 值,但是调用 Symbol(“cat”) 30 次,会返回 30 个不同的 Symbol 值。

1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

console.log(s1 === s2); // true

Symbol.keyFor()

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。

1
2
3
4
5
let s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"

let s2 = Symbol("foo");
console.log(Symbol.keyFor(s2)); // undefined

上面代码中,变量s2属于未登记的 Symbol 值,所以返回 undefined。

Object

在计算机科学中,对象是指内存中的可以被标识符引用的一块区域。在 JavaScript 里,对象可以被看作是一组属性的集合。一个 JavaScript 对象就是键和值之间的映射。键是一个字符串(或者 Symbol),值可以是任意类型的值。
ECMAScript 定义的对象中有两种属性:数据属性和访问器属性。

数据类型

访问器属性

null 和 undefined 的区别

1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 作为表示 “无” 的值。
根据 C 语言的传统,null 被设计成可以自动转为 0。
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够,有两个原因:

  • 首先,null 像在 Java 里一样,被当成一个对象。但是,JavaScript 的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich 觉得表示 “无” 的值最好不是对象。
  • 其次,JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。
    因此,Brendan Eich 又设计了一个 undefined。

1、undefined 不是关键字,而 null 是关键字。这意味着 undefined 可以被重写,使用起来可能会不安全。
2、undefined 和 null 被转换为布尔值的时候,两者都为 false。
3、undefined 和 null 进行 == 比较时两者相等,进行 === 比较时两者不等。
4、使用 Number() 对 undefined 和 null 进行类型转换,undefined 是 NaN,null 是 0。

为什么需要堆和栈两个存储空间

因为 JavaScript 引擎需要用栈来维护程序执行上下文的状态(调用栈),如果栈空间太大的话(即所有数据都存储在栈空间中),会影响上下文的切换效率,进而影响整个程序的执行效率,所以通常情况下栈空间不会设置太大,用于存储基本类型这样的小数据,而引用类型将存储到堆中,并且会分配一个内存地址。该内存地址会存储到栈空间。

包装对象

什么是包装对象

JS 的数值,布尔,字符串类型的变量,在一定条件下,也可以自动变成对象,这就是原始类型的包装对象。包装对象其实是一种特殊的引用类型,其与引用类型的主要区别在于生命周期

  1. 一般的引用类型在使用 new 创建其实例时,在执行流离开当前作用域之前一直都保存在内存中
  2. 包装类型的对象只存在该行代码的执行瞬间,然后会立即销毁。(也意味着在运行时不能为基本类型添加属性和方法)

以字符串为例,来演示该流程:

1
2
const str = 'abc';
const strNew = str.substring(0, 2);

在运行到 str.substring(0, 2) 的时候其实偷偷执行了以下三步:

1
2
3
let strObj = new String(str);
const strNew = strObj.substring(0, 2);
strObj = null;

拓展

  1. 包装对象和同样的原始类型的值相等吗?
    不相等。因为包装对象是引用类型,原始类型是基本类型;包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值(万物皆对象的思想),其次使得原始类型的值可以方便地调用某些方法。
  2. 如何给基本类型添加属性和方法?
    在基本包装对象的原型上添加,每个对象都有原型。
  3. 同一个字符串调用两次相同的方法其包装对象相等吗?
    不相等。调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,这也说明了为什么不能直接给字符串、数字、布尔值添加属性和方法。

参考资料:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Data_structures
https://es6.ruanyifeng.com/#docs/symbol
https://blog.csdn.net/weixin_42213025/article/details/105809018
https://www.cnblogs.com/green-jcx/p/9391720.html
公众号前端点线面

var

  1. 有变量提升
  2. 没有块的概念,可以跨块访问;作用域只存在于函数中,不可以跨函数访问
  3. 可以重复声明

let

  1. 有块级作用域
  2. let 也有变量提升,但是 var 声明的变量会被赋值 undefined,let 和 const 声明的变量不会被初始化
  3. 存在暂时性死区。在块级作用域内,let 声明之前的部分被称为暂时性死区,暂时性死区内不可以使用 let 声明的变量
  4. 不允许重复声明
  5. let 不会在全局声明时创建 window 对象的属性

const

  1. 具备 let 所具有的上述特性
  2. 一旦声明必须立即赋值
  3. 声明之后值不可以改变。(对于引用类型的变量而言,指的是它的地址不能发生改变)

Object.freeze()

既然 const 对于引用类型只能确保地址不变,那么怎样才能使得声明的引用类型变量上的值无法改变呢?
这里我们可以使用 Object.freeze() 方法递归解决

1
2
3
4
5
6
7
8
const myFreeze = obj => {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if(typeof obj[key] === 'object') {
myFreeze(obj[key]);
}
})
}

记录一下关于 js 的奇葩事情

typeof

为什么 typeof null 是 object?
js在底层存储变量的时候,会在变量机器码的低位 1-3 位存储其类型信息,其中 object 的低 1-3 位是 000。typeof 通过机器码判断类型,而由于 null 的所有机器码均为 0,该机器码和对象一样,因此 null 直接被当作对象来看待。这个 bug 的改动可能会牵涉到非常多的其他内容变更,所以一直没有被改,因为它的改动很可能会引发更多的其它 bug。

除以 0

js 里面 0 居然是可以当除数的。

1
2
3
1 / +0; // Infinity
1 / -0; // -Infinity
-1 / -0; // Infinity

isNaN

如图,记录下这一刻。

2021 年 12 月 15 日,leetcode 提交连续 WA 了无数次。最后才知道原来 js 里面 isNaN 判断空格也是 false。

题目链接:
https://leetcode-cn.com/problems/partition-array-into-three-parts-with-equal-sum/
解法分析:前面部分的思路都很简单,主要说一下最后一行代码为什么要写 >= 而不是 ===,这是因为些特别坑的情况,比如 [0, 0, 0, 0],或者是像 [-1, 1, -1, 1, -1, 1, -1, 1] 这样的情况。可以看出,当和为 0 的时候,可以分成大于三等份的情况必定也可以分成刚好三等份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param {number[]} arr
* @return {boolean}
*/
var canThreePartsEqualSum = function(arr) {
const sum = arr.reduce((pre, cur) => pre + cur);

if(sum % 3 !== 0) {
return false;
}

const part = sum / 3;
let count = 0;
let ans = 0;
for(let i = 0; i < arr.length; i++) {
count += arr[i];
if(count === part) {
ans++;
count = 0;
}
}

return ans >= 3;
};

思考和尝试了很久 node 做后端怎样才更方便快捷。首先现在开发肯定要首先 typescript,然后框架方面选择基本也就是 express、koa、egg.js、nest.js、midway.js。一开始我的选择是 nest.js,因为它天然支持 typescript,而且封装程度较高,使用也很方便。但是感觉自己如果要去了解它里面的原理的话难度比较大。出于想要了解原理的目的,我又转向了 koa,因为 koa 的代码是出了名的简短精悍。然后在一晚上的尝试以后, sequelize-typescript 出现了一个不知道缘由的 bug,表无法自动创建。然后我又换成了 typeorm,typeorm 使用起来真的是非常方便。不过这一整套操作下来,安装各种包以及相对应的 @types 包,我又开始怀念起来 nest.js 那种开箱即用的感觉了。尤其是它的命令行配置,以及跟 class-validator 的完美融合使用起来确实是太方便了。总而言之,反反复复思考了很久,最后决定下来了,下次做项目后端还是用 nest.js 吧。

题目链接:
https://www.nowcoder.com/practice/a1169935fd6145899f953ba8fbccb585

instanceof 用处

instanceof 判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。

instanceof 原理

instanceof 主要的实现原理:只要右边变量的 prototype 在左边变量的原型链上即可。因此, instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype ,如果查找失败,则会返回 false

instanceof 实现

过程:

  1. 获取左边变量的隐式原型(即:__ proto __ ,可通过 Object.getPrototypeOf() 获取)

  2. 获取右边变量的显示原型(即:prototype

  3. 进行判断,比较 leftVal. __ proto __ . __ proto __ …… === rightVal.prototype,相等则返回 true,否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myInstanceof(left, right) {
let leftProto = Object.getPrototypeOf(left); // 左边隐式原型
const rightProto = right.prototype; // 右边显示原型

// 在左边的原型链上查找
while(leftProto !== null) {
if(leftProto === rightProto) {
return true;
}
leftProto = Object.getPrototypeOf(leftProto);
}

return false;
}

参考资料:公众号前端点线面

题目链接:
https://www.nowcoder.com/practice/5d7e0cf4634344c98e6ae4eaa2336bed

今天来总结一下数组扁平化的题目。

解题思路

  1. 遍历:for、forEach、reduce
  2. 判断元素是否是数组类型:instanceof、Object.prototype.toString、Array.isArray
  3. 递归

常规解法

1
2
3
4
5
6
7
8
9
10
11
12
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "小明" }];
function flat(arr) {
let res = [];
arr.forEach(item => {
if(Array.isArray(item)) {
res = res.concat(flat(item));
} else {
res.push(item);
}
})
return res;
}

reduce 实现 flat

使用 reduce 展开一层

1
arr.reduce((pre, cur) => pre.concat(cur), []);

reduce 实现 flat

1
2
3
4
5
const flat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
}, [])
}

扩展运算符和 some 实现 flat

1
2
3
4
5
6
const flat = arr => {
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}

参考文章:
https://juejin.cn/post/6844904025993773063
https://blog.csdn.net/qq_41805715/article/details/101232148

typeof 类型保护

typeof 类型保护用于判断变量是哪种原始类型。

1
2
3
4
5
6
7
8
9
10
11
function fn(param: string | number) {
if (typeof param === 'string') {
console.log(param + 1);
}
if (typeof param === 'number') {
console.log(param + 1);
}
}
// 原本是联合类型,由于应用了 typeof,后面作用域的 param 类型就确定了。
fn('1'); // 11
fn(1); // 1

同理还有 instance 保护。

自定义类型保护 is

typeofinstanceof 类型保护能够满足一般场景,对于一些更加特殊的,可以通过自定义类型保护来对应类型,比如我们自己定义一个请求参数的接口类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface RequestParams {
url: string;
onSuccess?: () => void;
onFailed?: () => void;
}

function isValidRequestParams(request: any): request is RequestParams {
return request && request.url;
}

let request;
// 检测客户端发送过来的参数
if (isValidRequestParams(requst)) {
request.url;
}

这里面通过判断,我们需要手动告诉编译器通过 isValidRequestParams 的判断以后则 request 就是 RequestParams 类型的参数,编译器通过类型谓词 parameterName is Type 得知,isValidRequestParams 返回了 truerequest 就是 RequestParams 类型。

参考资料:
https://tsejx.github.io/typescript-guidebook/syntax/advanced/type-guards

今天在写人机交互课程的实验报告,复习了一下之前学到的 web 性能分析的方法,然后突发奇想也给我的网页做了个性能测试,当然结果很不乐观(

网页性能分析的重要性

简单来说,一方面,提高用户体验;另一方面,提高网页的 SEO 排名。

整体分析

先来看整体的分析情况,Performance 是 61 分,SEO 是 97 分。

性能只能说很不理想,但是可惜的是我没有在新建博客的第一时间就先测一下,以至于我不太清楚影响性能的是 hexo + next 本身的问题,还是因为我后来做的美化导致的。另一方面 hexo 是模板建站,我对于里面的源码基本毫不了解,想要优化也不知道从何做起。总之先来看看这 6 个指标吧。

lighthouse 分析网页的六大指标

简单介绍一下这 6 个指标:

FCP

FCP(First Content Paint),即首次内容绘制,是指浏览器从响应用户输入网络地址,在页面首次绘制文本,图片(包括背景图)、非白色的 canvas 或者 svg 之间的这段时间。以往的话,一般还会提到像 FP(First Paint)、FMP(First Meaning Paint)这两个概念,不过现在已经被 lighthouse 废弃了,FP 指标可以通过 Performance 获取。

TTI

TTI(Time to Interactive),即可交互时间,指的是网页第一次达到可以交互状态的时间,可交互状态的 UI 组件可以交互,而且页面已经达到了相对稳定流畅的程度。

说到 TTI 一般还会提到 RAIL 性能模型,包含四个指标,分别是 Response (响应)、Animation(动画)、Idle(空置状态)和 Load(加载)。

  • Response:如果用户点击了一个按钮,你需要保证在用户察觉出延迟之前就得到反馈。只要有输入,这个原则就适用。如果没有在合理的时窗内完成响应,,用户就会察觉到这个延迟,一般而言在这个时间会被定为 100 毫秒。另外,如果用户等待时间可能超出 500ms 的话,需要加上 loading 动画来缓解用户的紧张焦虑。

  • Animation:如果动画帧率发生变化,用户很可能会注意到。一般而言,web 性能优化的目标是达到每秒生成 60 帧。谈到 Animation,就会想起浏览器的渲染流程。

    一般来说渲染流程会被分为这四步,明天的主要笔记内容大概会与此相关;今天主要谈 web 性能测试,就先不细说这部分了。

  • Idle:可以利用空闲的时间来完成被推迟了的工作。例如,尽可能减少预加载数据,然后利用空闲时间加载剩余的数据。

  • Load:最终目标是能够在 1000 毫秒以内呈现页面内容,这方面涉及到关键路径的优化问题,我也放到明天再谈。注意这里 1s 内并不需要渲染出所有的页面内容,可以把还未完成的任务留到空闲时间继续完成。

SI

SI(Speed Index),即首屏时间,代表页面内容渲染所消耗的时间。优化 Speed Index 一般从两方面入手:优化内容效率和优化关键渲染路径。SI 低于 4s 则表示页面加载速度较优。

TBT

TBT(Total Blocking Time)是衡量用户事件响应的指标。TBT会统计在FCP和TTI时间之间,主线程被阻塞的时间总和。当主线程被阻塞超过 50ms 导致用户事件无法响应,这样的阻塞时长就会被统计到TBT中。TBT越小说明页面能够更好的快速响应用户事件。

LCP

LCP(Largest Content Paintful)是一个页面加载时长的技术指标,用于表示当前页面中占比最大的内容显示出来的时间点。它可以代表当前页面主要内容展示的时间。LCP低于 2.5s 则表示页面加载速度较优。

CLS

CLS(Cumulative Layout Shift)是一个衡量页面内容是否稳定的指标,CLS会将页面加载过程中非预期的页面布局的累积。CLS的分数越低,表明页面的布局稳定性越高,通常低于0.1表示页面稳定性良好。

lighthouse 的优势

lighthouse 在指出性能不足之处的时候,一般还会具体指出哪些地方可以进行优化,例如这次分析,

后续

接下来我先考虑的是要把之前鼠标的交互动画减少一些,然后想办法压缩一下静态资源。去除无用 CSS 和 JS 稍微有些难办,因为里面的代码对我来说就像一个黑盒一样,我也没有足够的课余时间去阅读源码。

参考文章:
https://www.cnblogs.com/frank-link/p/15243695.html
https://www.cnblogs.com/loveyt/p/13582359.html
https://blog.csdn.net/m0_37411791/article/details/106394219
https://zhuanlan.zhihu.com/p/20276064

搞了一天,遇到了不少的奇葩 bug,这次 hexo 建站目前算是暂时完结。
记录一下自己做了哪些优化:

  • 添加看板娘
  • 添加鼠标点击爱心特效
  • 添加鼠标点击爆炸特效
  • 添加头像 & 头像旋转功能
  • 添加 fork me on github
  • 添加 RSS
  • 更换字体为思源宋体
  • 修改文章内链接文本样式
  • 修改文章底部的标签样式
  • 在每篇文章末尾添加本文结束标记
  • 通过 leancloud & valine 加上了网站的阅读量功能以及评论功能
  • 实现文章字数统计功能
  • 实现全站总字数统计功能
  • 在文章底部增加版权信息
  • 添加博文置顶功能
  • 修改网站 favicon 图标
  • 添加侧边栏 github 图标
  • 添加了本地搜索功能
  • 最后通过 picgo、jsDelivr 和 github 仓库搭建了一个简易的博客图床
  • 其实还添加了一个动态的背景效果,本地运行没问题,但是部署上去就没有了,尚待解决此bug

参考文章:
https://www.jianshu.com/p/f054333ac9e6
https://www.heson10.com/archives/