fox的博客

github地址 -> https://github.com/FoxDaxian


  • Home

  • Archives

electron简析

Posted on 2021-05-09 In 原理

什么是electron

electron是一个可以用HTML、CSS、JS来构建桌面应用的程序,并且具有跨平台型。

electron的重要性

一般来说,桌面应用在不同的系统上需要不同的原生语言。electron,可以让你的桌面应用兼容大多数流行平台。

组成

electron组合chromium和nodejs,并且为原生操作系统暴露出一些自定义的api,比如吊起文件dialog,通知栏,等等
alt

开发体验

开发electron就像构建一个网页一样,你可以无缝使用nodejs。并且你不需要考虑兼容性。

进程

  • 主进程:package.json中的main对应的文件执行后是主进程
  • 渲染进程:每个electron页面都运行着自己的进程,称为渲染进程

联系和区别

  • 主进程,一般来说是main.js。这是整个electron app的入口,控制着app的生命周期,从打开到关闭,控制着打开原生元素,和创建新的渲染进程。并且集成nodejs api

alt

  • 渲染进程:说白了渲染进程就是app的浏览器窗口,不像主进程,这可以有多个,并且相互独立。也可以被隐藏,一般来说命名为index.html,html中有着通用文件特性,但有一点,你可以使用node api。

alt

主进程使用browserWindow实例创建页面(node层使用browserWindow创建实例),每个browserWindow实例都在自己的渲染进程里运行页面。当一个实例被销毁后,相应的渲染进程也会被终止。(页面层使用remote-本质是eventEmmiter)
主进程管理所有页面和与之对应的渲染进程。每个渲染进程都是相互独立的,并且只关心他们自己的页面。
几乎所有与底层API进行交互的逻辑都需要在主进程调用。

联想一波

electron的渲染进程就像谷歌或者其他浏览器中的每个tab,每个web页面就是一个渲染进程
如果你关闭所有的tab,chrome仍然没有关闭,这就像主进程,你可以打开新的窗口或退出整个app

alt

通讯方式 参考

  1. ipcmain 主进程使用 ipcrender 渲染进程使用
  2. remote 渲染进程使用

本质上是事件的监听订阅。

开发方案

非单页

  • electron
  • hbs(等模板)
    使用hbs渲染模板,根据机器语言不同的页面风格。使用hbs打包到目标目录,electron直接loadurl。简单高效。

单页

  • electron
  • mvvm框架
  • store
  • router
    语法是大家所熟悉,不需要再去熟悉模板语言,开发效率高,目录结构清晰,单页用户体验好,不涉及seo问题,但是不支持服务端渲染,除非只做一个架子,但是还得考虑请求时间,毕竟electron是直接加载本地文件。
    建议还是开发单窗口应用,比如网易云音乐。

两个问题

  1. 为什么browserWindow在渲染进程中找不到
    渲染进程本身就是一个browserWindow实例,有些api是通用的有些不是,要是想在渲染进程中使用browserWindow需要引入remote模块
  2. 想在渲染进程使用主进程的api
    需要使用通信,让主进程代劳

参考1
参考2

flutter简析

Posted on 2021-05-09 In 原理

Flutter

目录

  • 简介
  • JIT & AOT
  • 比较
  • 框架结构
  • 数据管理
  • 原理简析
  • 现状

简介

Flutter是谷歌的移动UI框架,主打跨平台、高保真、高性能。可以快速在ios和android上构建高质量的原生用户界面。使用Dart语言开发App。

  • 高性能
    • 开发JIT,发布AOT
    • 使用自己的渲染引擎,无需像rn那样js与native通信
  • 开发效率高
    • 一份代码多平台使用
    • 热重载
  • Dart强类型语言

实现思路:通过在不同平台实现统一接口的渲染引擎来绘制UI,而不依赖系统原生控件。所以解决的是UI的跨平台问题,如有涉及其他系统能力,依然需要原生开发。
Flutter也是受到的React启发,很多思想是相同的,所以有必要去了解react。

JIT & AOT

定义
  • JIT(动态解释)
    边运行边编译,边编译边运行。

  • AOT(静态编译)
    把源代码编译成目标代码(机器码、中间字节码),然后执行

    比较

    JIT优点:

    1. 根据硬件情况编译最优结果
    2. 合理运用内存空间

JIT缺点:

  1. 编译占用运行时资源
  2. 需要在程序路畅和编译时间之间权衡

AOT优点:

  1. 避免运行时的性能和内存消耗
  2. 大大减少程序启动时间

AOT缺点:

  1. 开发效率低

相关推荐: WebAssembly介绍

框架结构

Flutter Framework

  • framework中最下面两层是dary UI层,对应flutter中的
    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
    - Rendering层,依赖于dart ui层,相当于一个控制器。rendering层会构建出ui树,当ui树有变化的时候,diff,然后更新ui树,在渲染到屏幕上。
    - Widgets层是flutter提供的一套基础组件库,在flutter中,可以说一切都是widget

    #### Flutter Engine
    - 纯c++实现的sdk,其中包括 skia引擎、 dart运行时、 文字排版引擎等等,在调用dart:ui的时候,调用最终会走到Engine,实现绘制逻辑。

    #### 扩展 - widgets

    **一切皆为widget***

    和html不同,flutter没有css(样式),也没有js(逻辑),flutter只有一个个的widget,widget可以表示不同的html元素,比如input,button等等,widget也可以像html那样嵌套使用,不过是放到child中。

    ![](https://raw.githubusercontent.com/FoxDaxian/FoxDaxian.github.io/master/assets/13_flutter/widget.png)

    ##### 展示Widget
    - 你想加载图片,可以使用 ```Image```widget
    - 你想居中,可以使用```Center```widget
    - 你想采用独特的样式,可以使用```Theme```widget
    - 你想添加手势检测,可以使用```GustureDetector```widget
    还有常用的```Row```、```Column```、```Container```、```Text```等等

    ##### 状态Widget

    - StatelessWidget
    - StateFulWidget

    想要管理widget的状态,你需要继承```StateFulWidget
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
34
35
36
37
38
39
40
41
42
43
44
45
46
// 有状态计数器demo
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;

// 管理自己的状态,重写createState
@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'第一行',
),
new Text(
'$_counter'
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

无状态demo

数据管理

常见的有:

  • flutter_redux
  • event_bus(推荐)
event_bus

事件总线模式、发布订阅模式
减少耦合,统一管理状态

n步曲

  • 创建event bus(支持同步、异步)

    1
    2
    import 'package:event_bus/event_bus.dart';
    EventBus eventBus = EventBus();
  • 定义类

    1
    2
    3
    4
    5
    class UserLoggedInEvent {
    User user;

    UserLoggedInEvent(this.user);
    }
  • 订阅

    1
    2
    3
    4
    5
    6
    7
    eventBus.on<UserLoggedInEvent>().listen((event) {
    print(event.user);
    });
    // 监听所有
    eventBus.on().listen((event) {
    print(event.user);
    });
  • 发布

    1
    2
    User myUser = User('fox');
    eventBus.fire(UserLoggedInEvent(myUser));

原理简析

现状

今天google i/o大会,flutter团队宣布已支持移动、web、桌面和嵌入式设备。

同时发布了flutter for web的首个技术预览版,宣布flutter正在为包括google home hub在内的google只能平台提供支持。

flutter for web是flutter的代码兼容版本,使用基于标准的web技术(html, css, javascript)进行渲染,通过flutter for web,可以将dart编写的flutter代码编译成嵌入到浏览器,并部署到任何web服务器的客户端版本。开发者可以使用flutter的所有特性而无需浏览器插件。

图片高斯模糊

Posted on 2021-05-09 In 原理

图片加载一个老生常谈的问题,由于最近工作中经常有h5宣传页的需求,所以也看了一些方案

  • medium网站上提高用户体验的图片高斯模糊加载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- figure,代表一段独立的内容,常用语引用图片、插图、表格、代码段等等 -->
    <figure>
    <!-- 图片容器 -->
    <div class="aspectRatioPlaceholder is-locked" style="max-width: 383px; max-height: 326px;">
    <!-- 放置reflow的占位元素,style上的padding-bottom是通过接下来相邻div上写好的图片宽高计算而来,可通过十字相乘得到高度百分比 -->
    <div class="aspectRatioPlaceholder-fill" style="padding-bottom: 85.1%;"></div>
    <!-- 实际的图片信息,包括图片id和图片宽高 -->
    <div data-image-id="1*MZY5pNF7fgOarY-J2fNuHQ.png" data-width="383" data-height="326">
    <!-- 可以看到png后缀后有一个query: ?q=20,这个是缩略图的质量,猜想是二十分之一? -->
    <img src="https://cdn-images-1.medium.com/freeze/max/60/1*MZY5pNF7fgOarY-J2fNuHQ.png?q=20" crossorigin="anonymous">
    <!-- 获取上面的图片,渲染到canvas中,canvas宽高为实际的图片宽高,并添加高斯模糊效果,以获取较好的用户体验 -->
    <canvas></canvas>
    <!-- 真正的图片 -->
    <img src="https://cdn-images-1.medium.com/max/1600/1*MZY5pNF7fgOarY-J2fNuHQ.png">
    <!-- 向后兼容,在不支持js脚本或者支持js脚本,但认为禁止js脚本的浏览器中可以被识别 -->
    <noscript>
    <img src="https://cdn-images-1.medium.com/max/1600/1*MZY5pNF7fgOarY-J2fNuHQ.png">
    </noscript>
    </div>
    </div>
    </figure>
  1. 诺,可以先看一遍上面HTML代码中的注释,加上一点自己的理解,估计你能理解个大概
  2. 首先,准备一个真正需要展示给用户的图片和一个可以通过query来生成不同质量的缩略图的服务,哈哈
  3. 然后先加载质量很小很小的缩略图,然后通过div上设置的图片真正宽高,渲染到canvas中,并添加合适的高斯模糊效果
  4. 然后可以通过network中看到,等小图加载完毕后,再去加载原图,保证页面第一版加载更快,缩短执行js的时间
  5. 原图加载完毕后,显示原图,隐藏之前的canvas
  6. 对了,还有一个placeholder元素,这个是为了放置切换时造成的reflow,这个需要js动态设置图片容器的宽高,然后通过比例设置placeholder元素的padding-bottom
  7. 元素上使用了自定义属性,一个通用的、赋予html数据属性的特性,解决了自定义属性混乱无管理的情况,css选择器也可以选中它哦

骨架屏

Posted on 2021-05-09 In 原理

骨架屏

对于前端来说,最重要的莫过于用户体验了,这次我们聊一聊骨架屏 - Skeleton Screen

我们平常对于需要请求加载的内容,可能用的比较多的是loading动画,比如在内容区域放一个菊花图,当请求结束,并且render tree构造完成后,将菊花图移除,展示用户想看的内容。虽然这种方式没啥缺点,但是现在更多的网站开始使用骨架屏代替,因为它能带过来更好的用户体验。
我们看几个例子:

facebook
facebook

jira
jira

linkedin
linkedin

slack
slack

上图展示中,我们可以看到每个site从骨架图到真实内容的一个变化。如果你细心一点你会发现,不同site对于骨架图的block位置是不一致的:

  • facebook将用户固定的头像,author,日期和一小部分文字作为骨架主体
  • jira则是标题和logo对应的很整齐
  • linkedin可以说完全没有对齐,而是使用一种更加的展示骨架布局
  • slack则是使用混合的loading方式,有骨架图也有旋转圆,不仅如此,slack并没有全部使用同一种灰色值,不同的block的颜色代表的该区域的字体颜色,这又是一种切换顺滑度的提升。

不过他们都有一个共同点,就是采用简约的方式布局,我们可以以此为例,创造出独一无二的风格,来提高用户体验和加强品牌的风格,我想这会比一个loading logo带来更好的效果。

上面简单的介绍了一下骨架图,接下来我们来说一下具体实现吧。

优先我们实现一个简单的带有loading效果的骨架结构:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
 <!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
@keyframes loading {
0% {
background-position: -400px 0
}

100% {
background-position: 400px 0
}
}

.box1 {
position: absolute;
margin: 0 auto;
left: 50%;
margin-left: -400px;
top: 0;
width: 800px;
height: 60px;
background-color: blue;
background-image: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%);
animation-duration: 1s;
animation-iteration-count: infinite;
animation-name: loading;
animation-timing-function: linear;
transition: 0.3s;
}
.bgMasker {
background-color: #fff;
}

.line1 {
background-color: #fff;
height: 20px;
position: absolute;
right: 0;
top: 0;
left: 60px;
}

.line2 {
background-color: #fff;
height: 20px;
position: absolute;
right: 700px;
top: 20px;
left: 60px;
}

.line3 {
background-color: #fff;
height: 20px;
position: absolute;
left: 400px;
right: 0;
top: 20px;
}

.line4 {
background-color: #fff;
height: 20px;
top: 40px;
position: absolute;
left: 60px;
right: 500px;
}

.line5 {
background-color: #fff;
height: 20px;
position: absolute;
left: 600px;
right: 0;
top: 40px;
}
</style>
</head>

<body>
<!-- 骨架 -->
<div class="box1">
<div class="bgMasker line1"></div>
<div class="bgMasker line2"></div>
<div class="bgMasker line3"></div>
<div class="bgMasker line4"></div>
<div class="bgMasker line5"></div>
</div>
</body>

</html>

有一点需要说一下,由于我们使用的是渐变色的动画效果,所以我们的布局有一点的变化,我们采用的是整体加上背景色,然后内容使用定位和left,right来构成block的方式,具体内容请参考上面的代码

效果如下:

block

然后我们做一下简单的骨架图和内容的切换,这里就不贴代码了,切换有很多种实现方式,不固定思维。我这边做了两种,一种是直接切换,一种是淡入的切换,可以简单参考一下:

block

block


饿了么骨架图方案

  1. ssr,请求后用puppeteer插入script生成当前页的骨架图,或者build的时候直接生成(个人觉得应该是这种),然后插入到根元素内,然后数据加载后直接隐藏并展示真实数据
  2. 分块,对于图片,将采用最小大小尺寸 1 * 1的纯色gif图,然后进行拉伸
  3. 数据请求后对骨架进行隐藏等操作

http referer

Posted on 2021-05-09 In 原理

referer指的是请求来自哪里,值为url格式,大白话说,就是你是从哪里访问的当前网站。
常用来检测方可来自于哪,比如防盗链、恶意请求等等

防盗链

打个比方,如果用户请求一个资源,比如下载链接地址,那么下载链接地址的referer就指向你的来源地址,这时候你就可以通过referer判断是否来自你的域下,进行安全权限检测,对来源进行限制。不过如果你直接输入url打开的下载链接地址,那么referer就不存在或者为空,你又可以做其他的骚操作。
总之referer用好,会让你的网站更安全、也更灵活.

js模块的前生今世

Posted on 2021-05-09 In 原理

我曾经做过js讲师,在我的任教过程中,模块系统一直是学生们的薄弱点。有一个充分的理由可以解释这个问题:模块在javascript中有一段奇怪且不稳定的历史。这篇文章我们将讨论这段历史,并且,你讲了解过去的模块的相关知识,以更好的理解当前模块的工作原理。
在学习如何在js中创建模块之前,首先需要明白,模块是什么以及为什么会存在模块。环顾你的周边,你会发现,很多复杂的东西都是有一个个分离的部件组合在一起构成,进而形成一个完整的东西。

以一只手表为例:

手表结构

可以看到,一只手表由成百上千的内部部件组成,每一个内部部件都有特定的功能和清晰地边界以方便与其他部件协作。把这些部件组合在一起,就组成了这只完整的手表。我不是一个手表制造业的专家,但是我因为这种方法的优点是非常直观的。

可复用性

如果你仔细的观察一些上图中的构造,你会发现有很多部件都是重复的。由于这种模块化为中心的设计,手表中的不同功能也可以用到相同的部件。这种可复用部件的能力简化的工作制造流程,并且提高的利润。

可组合性

这种设计是可组合性的非常直观的案例。通过制定每个部件清晰地边界,能够很好地组合每一个部件,以创造一个功能齐全的手表。

可利用性(或许有更好的解释?)

设想一下制造过程,公司不会制造手表,他们只是将这些部件拼接起来以产出一只完整的手表。他们可以自己制作这些部件,也可以将这些部件外包给其他工厂,这不重要,重要的是这些部件组合在一起就是一只完整的手表,而这些部件来自于哪是无关紧要的。

隔离性

明白手表的整个系统是很困难的,因为它由很多小而复杂的,功能专一的部件组成,每个部件都可以单独考虑,构造和修复。这种隔离性允许人们单独工作,不会成为彼此的负担。并且,如果一个部件循环,仅仅需要更换这个部件看,而不是更换这只表。

组织化

组织是每个独立的拥有清晰边界的部件为了与其他部件组合的副产品,伴随着模块化,自然就会出现这种情况。

随着手表这样的结构不断产出,我们可以越来越清晰地认识到模块化的好处,那么,如果我们换成软件领域呢?其实是一样的。就像手表的设计一样,软件也应该被设计,分割成不同的具有特定功能的部件,并且具有为了与其他部件组合的清晰边界。不过,在软件中,这种部件被叫做模模块。到现在为止,模块给我们的感觉可能与react组件和函数大相径庭。那模块到底包含什么呢?

每一个模块都具有三部分:依赖,代码内容还有导出。

依赖

当一个模块需要其他模块的功能,它可以

react```模块,如果你想使用``` lodash```,你也只需要```impiort lodash```模块。
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
34
35
36
37

#### code

确定好你的模块需要的依赖之后,你就可以开始编写这个模块

#### exports

```exports```是当前模块的```接口```,引入这个模块的开发者可以使用你导出的一切功能。

说了这么多概念,下面让我们来点实际的代码。

先来看一个react router的例子,方便起见,可以看一下react提供的[模块目录](https://github.com/ReactTraining/react-router/tree/master/packages/react-router/modules),在react router中合理的利用模块,事实证明,在大多数情况下,他们直接映射react组件到模块,在react项目中分离组件是很有意义的,重新审查上面的手表结构,将部件换成组件同样有意义。

来看一下```MemoryRouter```模块的代码,现在不要关心代码的含义,只需要集中在代码的结构上。

```javascript
// imports
import React from 'react';
import { createMemoryHistory } from 'history';
import Router from './Router';

// code
class MemoryRouter extends React.Component {
history = createMemoryHistory(this.props);

render() {
return (
<Router
history={this.history}
children={this.props.children}
/>;
)
}
}

//exports
export default MemoryRouter;

你可以注意到这个模块的顶部定义了依赖,和一些使当前模块正产工作的必需的模块。接下来,可以看到一些代码。在这个例子中,创建了一个叫做MemoryRouter的新的react组件,最后,在底部定义了对外导出:MemoryRouter,也就是说,任何导入该模块的模块都会得到MemoryRouter这个组件。

现在,我们对软件中的模块有了一个浅显的认识,让我们回顾一些手表设计带来的好处,在相同设计的软件中有哪些可以可以直接应用。

可复用性

因为模块可以在任何需要它的地方

reacrt```、```lodash```还有```jquery```都是可以从npm上下载的npm包。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

#### 可组合性

由于模块定义了导入和导出,所以很容易组合起来,不仅如此。一个软件好的设计应该是低耦合,模块增加了代码的灵活性。

#### 可利用性

npm上有世界上数量最多的免费模块,超过七十万个,如果你需要某个功能的包,就去npm上找吧。

#### 隔离性

这里使用手表的描述也是合适的。不在赘述。

#### 组织化

模块最大的好处也许是组织化了,模块带来的分离,正如你所见的,帮助你避免污染全局命名空间,减少命名冲突。

-----

现在你大概了解了模块的结构和优点。是时候正式构建模块了。对此我们的方法是非常有条理的。原因是之前提到的,javascript中的模块有非常奇怪的历史,即使有更新的方法在javascript中创建模块,你也会时不时的看到一些老的创建方式。如果模块从2018年开始,这个可能没有一点用处,也就是说,我们会回到2010年的```模块```时代。那时,angularjs刚刚发布,jquery还在大范围使用。大部分公司使用javascript去构建复杂的web应用,而管理这些复杂的工具就是--模块。

创建模块的第一个想法可能就是用文件分离代码。

```javascript
// users.js
var users = ['Tyler', 'Sarah', 'Dan'];

function getUsers() {
return users;
}

// dom.js
function addUserToDom(name) {
var node = document.createElement('li');
var text = document.createTextNode(name);
node.appendChild(text);

document.getElementById('users').appendChild(node);
}

document.getElementById('submit')
.addEventListener('click', function() {
var input = document.getElementById('input');
addUserToDom(input.value);
input.value = '';
});

var users = window.getUsers();
for (var i = 0; i < users.length; i++) {
addUserToDom(users[i]);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- index.html -->
<html>
<head>
<title>Users</title>
</head>

<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>

<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>

这里查看全部源代码

ok,我们成功的将app分离成不同的功能文件,是不是意味着我们已经实现了模块?不,绝对没有。我们做的只不过是分离代码所在的位置。在js中,只有创建函数才能生成新的作用域。我们未在函数中生命的变量,全都在全局对象上。也就是说,你可以访问他们通过

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
34
35
36
37
38
39
40
41
42
43
那么,如果分享分离没有给我们提供模块的功能,那我们要怎么做呢?重复强调一下模块的优点:复用性、组合型、利用性、隔离性还有可组织性。js有没有原始的特性以供我们创造模块,以达到上面说的优点?常规函数?当你思考函数的特点,它的特点和模块优点相似。所以,接下来该怎么做呢?如果我们暴露一个对象来替代直接把整个app暴露在全局对象下,并且命名这个对象为```app```,我们可以吧所有我们app需要用到的方法,挂在在这个```app```对象下。这样会防止我们污染全局变量。我们可以在里面放置任何东西,这样对于其他应用来说依然是不可见得。

```javascript
// users.js
function usersWrapper () {
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
return users
}

APP.getUsers = getUsers
}

usersWrapper()

// dom.js

function domWrapper() {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)

document.getElementById("users")
.appendChild(node)
}

document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)

input.value = ""
})

var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
}

domWrapper()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>

<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>

<script src="app.js"></script>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>

这里查看全部源代码

现在你查看window对象,相比于,只有我们的

1
2
3
4
5
6
7
8
9
10

让我们更进一步。有没有办法可以丢弃包裹函数?我们只是定义了它们,然后立即调用。给他们一个全局命名的唯一原因就是我们之后可以立即调用它们。如果我们没有给他们全局命名,有没有办法直接直接调用没有名字(匿名)的函数。不卖关子了,当然有了,就是```Immediately Invoked Function Expression```,简写为```IIFE```。

#### IIFE

它看起来像下面这样:
```javascript
(function() {
console.log('Pronounced IF-EE');
})()

注意,这仅仅是一个被小括号

1
2
3
4
```javascript
(function() {
console.log('Pronounced IF-EE');
})

然后,就像其他函数一样,为了调用函数,我们增加了一对小括号在函数而最后。

1
2
3
(function() {
console.log('Pronounced IF-EE');
})()

现在,为了放弃丑陋的包裹函数和干净的全局命名空间让我们来使用

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
34
35
36
37
38
39

```javascript
// users.js

(function () {
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
return users
}

APP.getUsers = getUsers
})()

// dom.js

(function () {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)

document.getElementById("users")
.appendChild(node)
}

document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)

input.value = ""
})

var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
})()

这里查看全部源代码

么么哒。现在你在查看window对象,你会发现,我们仅仅挂在了一个app对象在上面,他将作为全局方法的命名空间。

这就是IIFE模块模式。

IIFE模块模式有什么优点呢?首先,最重要的一点是,我们没有污染全局命名空间,这避免了变量冲突,并且提供代码私有性。有利就有弊,我们仍然有一个全局app变量,如果其他框架使用了相同的代码,我们就有麻烦了。第二点,你可能主要到了html文件中的script的顺序,如果顺序不对,那么app直接会挂掉。
不过,就算这不是最完美的。我们依然进步了一大块。我们知道了IIFE模块模式的优点和缺点。如果我们用我们的标准创建并管理模块,它有哪些特性呢?

早些时候,我们对模块分离的第一感觉每个文件都是一个新的模块。就算这种想法在js中不是开箱就用的。我认为对模块来说这是一个非常显著的分离。每个文件就是一个单独的模块,然后我们需要一个特性是每个文件(模块)都能定义自己的导入和导出。并可在其他文件(模块)中导入。

我们的标准

  • 基于文件的模块
  • 明确的导入
  • 明确的导出

现在,我们明确了我们想要的标准,让我们开始开发api。我们需要定义的看起来像是

1
2
3
4
5
6
7
8
9

```javascript
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
return users
}

module.exports.getUsers = getUsers

也可以这样:

1
2
3
4
5
6
7
8
9
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
return users
}

module.exports = {
getUsers: getUsers
}

不管有多少个方法,我们都可以添加到

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

```javascript
// users.js

var users = ["Tyler", "Sarah", "Dan"]

module.exports = {
getUsers: function () {
return users
},
sortUsers: function () {
return users.sort()
},
firstUser: function () {
return users[0]
}
}

好了,我们解决了如何从模块导出,接下来我们需要解决如何导入。同样一切从简,首先假设我们有一个叫做

1
2
3
4
5
6
7

```javascript
var users = require('./users')

users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]

哦耶~ 利用假象的

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

看完这个标准,有没有灵光一现?这tm不就是commonjs吗?

commonjs小组定义了模块模式去解决js作用域问题,以确保每个模块在他们自己的命名空间执行。通过模块明确导出那些变量来实现,通过其他模块定义的require来正确工作。
如果你之前使用过node,conmonjs你会很熟悉。使用node,你可以开箱即用的使用require和module.exports语法,不过,浏览器并未支持。事实上,就算浏览器支持,浏览器也不会使用commonjs,因为它不是异步加载模块。众所周知,浏览器是单线程。异步才是王道。
简单总结一下,commonjs有两个问题,首先浏览器不支持,第二,浏览器就算支持了也会因为commonjs的同步加载造成很糟糕的用户体验。如果我们能修复这两个问题,这也许是一个好的方案。不过,花费很多的时间去考虑研究commonjs是否对浏览器足够友好有没有意义呢?不管怎么样,这有一个新的解决方案,它叫做模块打包器。

#### 模块打包器

模块打包器的作用是检查你的代码库。寻找所有的imports和exports,然后解析打包成浏览器可以明白的代码到一个单独的新文件。而且你不再用小心翼翼的引入所有script,你应该直接引入打包好的那个文件。

app.js ---> |
users.js -> | Bundler | -> bundle.js
dom.js ---> |

所以,模块打包器到底做了什么捏?这个问题很大,我也不能全部解释清楚,不过,这有一个通过webpack打包之后的输出,你可以自己领悟领悟,哈哈。

[这里](https://github.com/tylermcginnis/modules/tree/commonjs)查看所有源代码,你也可以下载下来,执行 npm install,然后执行webpack


```javascript
(function(modules) { // webpackBootstrap
// 模块缓存
var installedModules = {};
// require函数
function __webpack_require__(moduleId) {
// 检查module是否有缓存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个module并缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行module
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 设置module为已load
module.l = true;
// 返回模块的导出
return module.exports;
}
// 暴露模块对象
__webpack_require__.m = modules;
// 暴露模块缓存
__webpack_require__.c = installedModules;
// 定义getter函数
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(
exports,
name,
{ enumerable: true, get: getter }
);
}
};
// 在导出中定义__esModule
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 创建假的命名空间对象
// mode & 1: value是模块id,通过它引入
// mode & 2: 合并所有属性到ns对象上
// mode & 4: ns已经存在时,直接返回
// mode & 8|1: 行为和require一样
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string')
for(var key in value)
__webpack_require__.d(ns, key, function(key) {
return value[key];
}.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({

/***/ "./dom.js":
/*!****************!*\
!*** ./dom.js ***!
\****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(`
var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
function addUserToDOM(name) {\n
const node = document.createElement(\"li\")\n
const text = document.createTextNode(name)\n
node.appendChild(text)\n\n
document.getElementById(\"users\")\n
.appendChild(node)\n}\n\n
document.getElementById(\"submit\")\n
.addEventListener(\"click\", function() {\n
var input = document.getElementById(\"input\")\n
addUserToDOM(input.value)\n\n
input.value = \"\"\n})\n\n
var users = getUsers()\n
for (var i = 0; i < users.length; i++) {\n
addUserToDOM(users[i])\n
}\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),

/***/ "./users.js":
/*!******************!*\
!*** ./users.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {

eval(`
var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
function getUsers() {\n
return users\n}\n\nmodule.exports = {\n
getUsers: getUsers\n
}\n\n//# sourceURL=webpack:///./users.js?`);})
});

你可以注意到有很多奇奇怪怪的代码,你可以阅读注释来简单了解一下到底发生了什么。但是,一个很有趣的事是,打包后的代码用一个IIFE包裹起来了。也就是说,他们使用了IIFE模块模式得到了一个相对来说最完美的方案。

javascript的未来是一个活生生的,丰满的语言。TC-31标准委员会,一年内多次讨论如何潜在改善提高javascript语言。换言之,模块是编写可伸缩性、可维护的js代码的关键特性。在2013年甚至更早之前,这种说法很显然是不存在的。js需要一种模块的标准。一种内建的可处理模块的解决方法,这也拉开了实现js模块化的序幕。

如你现在所知道的。如果你之前接受过创建js系统模块的任务,这个模块最终看起来将是什么样的?commonjs?每个文件以一种很清晰的方式定义导入和导出,很显然,这个是重中之重。但是有个问题,commonjs加载模块是同步的。虽然这对服务端没有压力,但是对浏览器不是很友好。一个改变是让commonjs支持异步加载,另一种我们使用语言自己的模块化,也就是

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

这次,我们不需要再假想这种实现了,TC-39标准委员会提出了精确的设计和描述,也就是"ES Modules"。下面让我们以这种标准化的模块创建javascript模块。

#### EM Modules
正如上面所说的,为了指定你要导出的模块,你需要使用```export```关键字。

```javascript
// utils.js

// Not exported
function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}

// Exported
export function first (arr) {
return arr[0]
}

// Exported
export function last (arr) {
return arr[arr.length - 1]
}

有几种方式可以导入

1
2
3
4
5
6

```javascript
import * as utils from './utils'

utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3

如果我们不想导入全部导出呢?在这个例子中,如果你只想引入

1
2
3
4
5

```javascript
import { first } from './utils'

first([1,2,3]) // 1

还有呢,不仅仅可以指定多个导出,你还可以指定一个

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

```javascript
// leftpad.js

export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}

当你使用

name from './patn'```
1
2
3

```javascript
import leftpad from './leftpad'

现在,如果你有默认导出,也有其他格式的导出怎么办呢?这不是问题,按照正确的语法写就可以了,ES Module没有这种限制。

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
// utils.js

function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}

// regular export
export function first (arr) {
return arr[0]
}

// regular export
export function last (arr) {
return arr[arr.length - 1]
}

// default export
export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}

那导入语法看起来是什么样的?我觉得你可以想象得到。

1
import leftpad, { first, last } from './utils'

还是挺爽的是吧?

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
34
35
36
37
38
ES Modules的关键点在于,它是js语言的一部分,并且现代浏览器已经支持这种写法了。现在,让我们回到一开始的app,不过这次我们使用ES Modules来改写一遍。

[这里](https://github.com/tylermcginnis/modules/tree/esModules)查看所有源代码

```javascript
// users.js

var users = ["Tyler", "Sarah", "Dan"]

export default function getUsers() {
return users
}

// dom.js

import getUsers from './users.js'

function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)

document.getElementById("users")
.appendChild(node)
}

document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)

input.value = ""
})

var users = getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}

使用IIFE模式,我们需要使用script引入每个js文件。使用commonjs,我们需要使用webpack等打包器处理我们的代码,然后引入打包后的文件。而ES Modules中,在一些现在浏览器中,我们仅仅需要使用script标签引入我们的未被处理过的入口文件,然后为script标签增加属性:

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


```html
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>

<body>
<h1>Users</h1>
<ul id="users">
</ul>
<input id="input" type="text" placeholder="New User"></input>
<button id="submit">Submit</button>

<script type=module src='dom.js'></script>
</body>
</html>

死代码消除

到这里,还有一个commonjs与ES Modules的不同没有介绍。
commonjs中,你可以在任何地方引入模块,甚至通过判断。

1
2
3
if (pastTheFold === true) {
require('./parallax')
}

ES Modules需要静态解析(参考js词法解析,也会有提升的效果)的,import语句必须在模块顶部,也就是说,他不能再判断语句中或者其他类似的语句中使用。

1
2
3
if (pastTheFold === true) {
import './parallax' // "import' and 'export' may only appear at the top level"
}

这是因为加载器会进行模块树的静态解析。找到那些真正被用到的,丢弃那些未被使用到的。这是一个很大的话题。换句话说,这也是为什么ES Modules希望你声明import语句在模块顶部,这样打包器会更快的解析的你依赖树,解析完毕,他才会去真正的工作。

对了,其实你可以使用import()来动态导入。请自行查找。

希望通过这篇文章可以帮到你。

原文链接


  • script标签上加上type='module'的加载模式都是defer。
  • IIFE:匿名函数
  • AMD:依赖前置异步模块加载
  • CMD:就近依赖异步模块加载
  • commonjs(cjs):服务端通用的模块加载
  • UMD:不是单独的标准,是IIFE、AMD(CMD)、commonjs的结合。
  • es:js自己的模块打包
标准 变量问题 依赖 动态/懒 加载 静态分析
IIFE ✔ × × ×
AMD ✔ ✔ ✔ ×
CMD ✔ ✔ ✔ ×
commonjs ✔ ✔ ✔ ×
es6 ✔ ✔ ✔ ✔

再强调一点:es6的模块是值的引用,commonjs是值的拷贝。参考文章

koa 中间件原理

Posted on 2021-05-09 In 源码分析
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
'use strict';

module.exports = compose;

function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError('Middleware stack must be an array!');
for (const fn of middleware) {
if (typeof fn !== 'function')
throw new TypeError('Middleware must be composed of functions!');
}

return function(context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(
new Error('next() called multiple times')
);
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

代码很短,咱们直接分析:

  • 本身是一个高阶函数,返回一个函数
  • compose函数接收一个数组形式middleware的参数,如果不为数组或者数组中有非函数的项,抛出异常
  • 返回的函数接收koa上下文环境参数 - ctx 和 下一个middleware, 不管怎样,该函数返回一个promise对象。以供之后进行下一步异步操作

分下compose返回的函数

  • 初始化index,用户判断是否多次调用next,因为每调用一次next,都会是i + 1,而 index 只会同步为 i 一次
  • 包含一个递归的dispatch函数,接收当前要执行的middle的索引,默认从0开始
  • 之后获取当前要执行的middleware
  • 如果为middleware的长度那么说明无可执行的middleware(因为数组的长度比真实的项索引多1),重新赋值为next(手动设置最后一个中间件)
  • 如果fn为空,return promise
  • try cache 执行的fn,并将fn的返回值作为promise的成功结果,dispatch.bind(null, next middleow) 为next,当你调用next的时候即调用下一个
  • 捕获错误,返回err

koa中间件是一个可选的、灵活的,不一定要执行完所有的中间件,你可以只执行某几个,中间件可以操作ctx上的所有数据,可以通过koa中的respond处理ctx的相关属性,来进行数据展示等操作

使用lerna管理你的项目

Posted on 2021-05-09 In 介绍

有段时间没更新博客了,是时候更新一波了。
之前不是vue-next出了吗,然后就去学习了一下,发现整个目录不是那么熟悉了,变成这样了:

于是就这个线索去研究了一下,发下这是用的 lerna + yarn 的架构,不仅vue,包括jest,babel等都是用的这类架构,他们有相同的前缀,比如@babel/xxx,不过这个前缀(scope)是需要付费的。

lerna有什么优点呢?

  • 分离一个大型的codebase到多个小的孤立或者公共的repo
  • 可以统一管理版本号,一键发布,自动生成changelog(lerna publish)
  • 一键安装依赖,包括link(lerna bootstrap)
  • 目录清晰,像上面vue-next那样。

所以说!

开整!!!

首先使用lerna + yarn来管理我们的npm工作区:
所以创建一个空的reop,然后npx lerna init初始化lerna项目,然后左改改右改改,像下面这样,意思是说用yarn替代lerna的工作区定义,然后pkg中指定workspaces,指定private和root,表明别发布我。

1
2
3
4
5
6
// lerna.js
{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true
}
1
2
3
4
5
6
7
8
9
// package.json
{
"name": "root",
"private": true,
"workspaces": [
"packages/*",
"demo"
]
}

哈哈哈哈,开个玩笑,不过lerna的初始工作就好了,剩下的就是安装依赖啊,写代码啊,发布啊。用指令表示就是:

  • lerna bootstrap(或者增加postinstall hooks自动执行)
  • 写代码
  • lerna version 指定版本
  • lerna publish (前需要登录npm,例如: npm login)

不过,仅仅只有上面这些肯定是不够的,我们还需要增加:

  • 本地预览
  • 本地unit测试
  • 一些自动化脚本
  • 格式化检查工具
  • 其他(ts、commitlint、cz)

这块就不啰嗦了,直接丢一个repo: oneForAll供大家参考,欢迎交流哈。
目录如下:

package.json 中的字段

Posted on 2021-05-09 In 介绍
  • workspace
    将一个大型工程进行模块化,根据单一职责,方便日后的维护,但是这种方法不受用与所有工程,比如一些工程需要将所有仓库放到一个统一的存储库中,方便协作共建维护。这就派生出了 monorepos,对应的方法就是 yarn 的 workspace 或者 lerna,它能够帮你节约每次 cd + install or upgrade 的时间,更方便管理。
    yarn
    中文
  • resolutions
    强制指定子依赖的版本,防止升级后导致多个版本并存,打包后内容大
    yarn
    demo
    中文
  • private
    如果为 true,那么 npm 将拒绝发布它,用来防止私人存储库意外发布的一种方法
  • engines
    指定项目运行的node版本范围

Object.assign引发的问题

Posted on 2021-05-09 In 原理浅析

缘由

今天看到一段代码

1
return Object.assign(func1, func2);

心生疑惑,为什么 Object.assign 的参数可以是函数?
于是有了下面这一堆东西,其实都是老生常谈的东西,可能是岁数大了吧,有些片段都快丢失了,哈哈

prototype

js 中 万物皆是对象!!!

proto(隐式原型)与 prototype(显式原型)
对象具有属性proto,可称为隐式原型
实例(对象)的 proto === 构造(该实例)函数的 prototype
函数 Function 是特殊的对象,除了有proto外,还有自己的特有的属性 - 原型对象(prototype)
原型对象有一个属性 - constructor,指回 x.prototype 的 x(原函数)

所以 函数 还是 构造函数的函数(Function)都会指回 Object

1
2
3
4
5
6
7
8
9
10
// 特例
function aa() {}
aa.prototype; // => {constructor: ƒ}

Function.prototype; // => ƒ () { [native code] } 函数也是对象哦

// 所以
Function.prototype.constructor; // => ƒ Function() { [native code] }

Function.prototype.constructor === Function; // => true

Object.prototype 是 原型的尽头,在往上就是 null 了

看张老图吧

Objecg.assign

定义

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象

分为两个关键点 源对象自身且可枚举的属性 和 目标对象,一个个解释

枚举

判断是否为枚举属性: Object.propertyIsEnumerable(prop)
如果判断的属性存在于 Object 对象的原型内,不管它是否可枚举都会返回 false。

总的来说,不管什么类型,只要可以用 for…in 遍历出来的属性,全都可以拷贝到 对象 上

例如 string 和 number:

对象

所说的对象是哪些呢?通过 instanlceof 可知(不包含全部类型)

上面是前提,下面让我们看一个 demo 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn() {}
console.log(fn[0], fn[1], fn[2]); // => undefined undefined undefined

const str = "963";
for (let k in str) {
console.log(`${k}: ${str[k]}`); // => 0: 9
// => 1: 6
// => 2: 3
}

Object.assign(fn, "963");

console.log(fn[0], fn[1], fn[2]); // => 9 6 3

结果如下:

深入讲解proto 和 prototype
属性的可枚举型

1234

FoxDaxian

遇到好玩的,稀奇的,新鲜的,记录下来,分享出去。
40 posts
11 categories
4 tags
© 2021 FoxDaxian
Powered by Hexo v3.9.0
|
Theme – NexT.Muse v7.2.0