大家好,我是若川。持续组织了5个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。
ECMAScript 规范每年都会更新一次,正式标准化 JavaScript 语言的 ECMAScript 的下一次年度更新将在 2022 年 6 月左右获得批准。自 2015 年以来,TC39 团队成员每年都会一起讨论可用的提案,并发布已接受的提案。今年是 ECMAScript 的第 13 版,其中包括许多实用的功能。所有在 2022 年 3 月之前达到阶段 4 的提案都将包含在全新的 ECMAScript 2022 标准中。
对于一个提案,从提出到最后被纳入ES新特性,TC39的规范中分为五步:
stage0(strawman),任何TC39的成员都可以提交。
stage1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。
stage2(draft),演进到这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
state3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
state4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中
目前,一些提案还处于第三阶段,一些提案已经进入第四阶段。提案的功能将在达到第 4 阶段后被添加到新的ECMAScript标准中,这意味着它们已获得 TC-39 的批准,通过了测试,并且至少有两个实现。下面就来看看 ECMAScript 2022 预计会推出的新功能吧~
一、Top-level Await
在ES2017中引入了 async 函数和 await 关键字,以简化 Promise 的使用,但是 await 关键字只能在 async 函数内部使用。尝试在异步函数之外使用 await 就会报错:SyntaxError - SyntaxError: await is only valid in async function。
顶层 await 允许我们在 async 函数外面使用 await 关键字,目前提案正处于第 4 阶段,并且已经在三个主要的浏览器的 JavaScript 引擎中实现,模块系统会协调所有的异步 promise。
顶层 await 允许模块充当大型异步函数,通过顶层 await,这些ECMAScript模块可以等待资源加载。这样其他导入这些模块的模块在执行代码之前要等待资源加载完再去执行。下面来看一个简单的例子。
由于 await 仅在 async 函数中可用,因此模块可以通过将代码包装在 async 函数中来在代码中包含 await:
// a.jsimport fetch from "node-fetch";let users;export const fetchUsers = async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users = resp.json();}fetchUsers();export { users };
// usingAwait.jsimport {users} from './a.js';console.log('users: ', users);console.log('usingAwait module');
我们还可以立即调用顶层async函数(IIAFE):
import fetch from "node-fetch";(async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users = resp.json();})();export { users };
这样会有一个缺点,直接导入的 users 是 undefined,需要在异步执行完成之后才能访问它:
// usingAwait.js
import {users} from './a.js';console.log('users:', users); // undefinedsetTimeout(() => {console.log('users:', users);
}, 100);console.log('usingAwait module');
当然,这种方法并不安全,因为如果异步函数执行花费的时间超过100毫秒, 它就不会起作用了,users 仍然是 undefined。
另一个方法是导出一个 promise,让导入模块知道数据已经准备好了:
//a.js
import fetch from "node-fetch";
export default (async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users = resp.json();
})();
export { users };//usingAwait.js
import promise, {users} from './a.js';
promise.then(() => { console.log('usingAwait module');setTimeout(() => console.log('users:', users), 100);
});
虽然这种方法似乎是给出了预期的结果,但是有一定的局限性:导入模块必须了解这种模式才能正确使用它。
而顶层await就可以消除这些缺点:
// a.jsconst resp = await fetch('https://jsonplaceholder.typicode.com/users');const users = resp.json();export { users};// usingAwait.jsimport {users} from './a.mjs';console.log(users);console.log('usingAwait module');
顶级 await 在以下场景中将非常有用:
动态加载模块
const strings = await import(`/i18n/${navigator.language}`);
资源初始化
const connection = await dbConnector();
依赖回退
let translations;
try {translations = await import('https://app.fr.json');
} catch {translations = await import('https://fallback.en.json');
}
目前,在这些地方已经支持 Top-level await:
V8 v8.9
Webpack 5.0.0
Babel
Chrome DevTools REPL
Node REPL
二、类的实例成员
1. 公共实例字段
公共类字段允许我们使用赋值运算符 (=) 将实例属性添加到类定义中。下面来一个计数器的例子:
import React, { Component } from "react";export class Incrementor extends Component {constructor() {super();this.state = {count: 0,};this.increment = this.increment.bind(this);}increment() {this.setState({ count: this.state.count + 1 });}render() {return (<button onClick={this.increment}>Increment: {this.state.count}</button>);}
}
在这个例子中,在构造函数中定义了实例字段和绑定方法,通过新的类语法,我们可以使代码更加直观。新的公共类字段语法允许我们直接将实例属性作为属性添加到类上,而无需使用构造函数方法。这样就简化了类的定义,使代码更加简洁、可读:
import React from "react";export class Incrementor extends React.Component {state = { count: 0 };increment = () => this.setState({ count: this.state.count + 1 });render = () => (<button onClick={this.increment}>Increment: {this.state.count}</button>);
}
有些小伙伴可能就疑问了,这个功能很早就可以使用了呀。但是它现在还不是标准的 ECMAScript,默认是不开启的,如果使用 create-react-app 创建 React 项目,那么它默认是启用的,否则我们必须使用正确的babel插件才能正常使用(@babel/preset-env)。
下面来看看关于公共实例字段的注意事项:
公共实例字段存在于每个创建的类实例上。它们要么是在
Object.defineProperty()
中添加,要么是在基类中的构造时添加(构造函数主体执行之前执行),要么在子类的super()返回之后添加:
class Incrementor {count = 0
}const instance = new Incrementor();
console.log(instance.count); // 0
未初始化的字段会自动设置为 undefined:
class Incrementor {count
}const instance = new Incrementor();
console.assert(instance.hasOwnProperty('count'));
console.log(instance.count); // undefined
可以进行字段的计算:
const PREFIX = 'main';class Incrementor {[`${PREFIX}Count`] = 0
}const instance = new Incrementor();
console.log(instance.mainCount); // 0
2. 私有实例字段、方法和访问器
默认情况下,ES6 中所有属性都是公共的,可以在类外检查或修改。下面来看一个例子:
class TimeTracker {name = 'zhangsan';project = 'blog';hours = 0;set addHours(hour) {this.hours += hour;}get timeSheet() {return `${this.name} works ${this.hours || 'nothing'} hours on ${this.project}`;}
}let person = new TimeTracker();
person.addHours = 2; // 标准 setter
person.hours = 4; // 绕过 setter 进行设置
person.timeSheet;
可以看到,在类中没有任何措施可以防止在不调用 setter 的情况下更改属性。
而私有类字段将使用哈希#前缀定义,从上面的示例中,我们可以修改它以包含私有类字段,以防止在类方法之外更改属性:
class TimeTracker {name = 'zhangsan';project = 'blog';#hours = 0; // 私有类字段set addHours(hour) {this.#hours += hour;}get timeSheet() {return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;}
}let person = new TimeTracker();
person.addHours = 4; // 标准 setter
person.timeSheet // zhangsan works 4 hours on blog
当我们尝试在 setter 方法之外修改私有类字段时,就会报错:
person.hours = 4 // Error Private field '#hours' must be declared in an enclosing class
我们还可以将方法或 getter/setter 设为私有,只需要给这些方法名称前面加#即可:
class TimeTracker {name = 'zhangsan';project = 'blog';#hours = 0; // 私有类字段set #addHours(hour) {this.#hours += hour;}get #timeSheet() {return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;}constructor(hours) {this.#addHours = hours;console.log(this.#timeSheet);}
}let person = new TimeTracker(4); // zhangsan works 4 hours on blog
由于尝试访问对象上不存在的私有字段会发生异常,因此需要能够检查对象是否具有给定的私有字段。可以使用 in 运算符来检查对象上是否有私有字段:
class Example {#fieldstatic isExampleInstance(object) {return #field in object;}
}
查看更多公有和私有字段提案信息:https://github.com/tc39/proposal-class-fields
3. 静态公共字段
在ES6中,不能在类的每个实例中访问静态字段或方法,只能在原型中访问。ES 2022 将提供一种在 JavaScript 中使用 static 关键字声明静态类字段的方法。下面来看一个例子:
class Shape {static color = 'blue';static getColor() {return this.color;}getMessage() {return `color:${this.color}` ;}
}
我们可以从类本身访问静态字段和方法:
console.log(Shape.color); // blueconsole.log(Shape.getColor()); // blueconsole.log('color' in Shape); // trueconsole.log('getColor' in Shape); // trueconsole.log('getMessage' in Shape); // false
实例不能访问静态字段和方法:
const shapeInstance = new Shape();console.log(shapeInstance.color); // undefinedconsole.log(shapeInstance.getColor); // undefinedconsole.log(shapeInstance.getMessage());// color:undefined
静态字段只能通过静态方法访问:
console.log(Shape.getColor()); // blue
console.log(Shape.getMessage()); //TypeError: Shape.getMessage is not a function
这里的 Shape.getMessage() 就报错了,这是因为 getMessage 不是一个静态函数,所以它不能通过类名 Shape 访问。可以通过以下方式来解决这个问题:
getMessage() {return `color:${Shape.color}` ;
}
静态字段和方法是从父类继承的:
class Rectangle extends Shape { }console.log(Rectangle.color); // blue
console.log(Rectangle.getColor()); // blue
console.log('color' in Rectangle); // true
console.log('getColor' in Rectangle); // true
console.log('getMessage' in Rectangle); // false
4. 静态私有字段和方法
与私有实例字段和方法一样,静态私有字段和方法也使用哈希 (#) 前缀来定义:
class Shape {static #color = 'blue';static #getColor() {return this.#color;}getMessage() {return `color:${Shape.#getColor()}` ;}
}
const shapeInstance = new Shape();
shapeInstance.getMessage(); // color:blue
私有静态字段有一个限制:只有定义私有静态字段的类才能访问该字段。这可能在我们使用 this 时导致出乎意料的情况:
class Shape {static #color = 'blue';
static #getColor() {return this.#color;
}
static getMessage() {return `color:${this.#color}` ;
}
getMessageNonStatic() {return `color:${this.#getColor()}` ;
}
}class Rectangle extends Shape {}console.log(Rectangle.getMessage()); // Uncaught TypeError: Cannot read private member #color from an object whose class did not declare it
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // TypeError: Cannot read private member #getColor from an object whose class did not declare it
在这个例子中,this 指向的是 Rectangle 类,它无权访问私有字段 #color。当我们尝试调用 Rectangle.getMessage() 时,它无法读取 #color 并抛出了 TypeError。可以这样来进行修改:
class Shape {static #color = 'blue';static #getColor() {return this.#color;}static getMessage() {return `${Shape.#color}`;}getMessageNonStatic() {return `color:${Shape.#getColor()} color`;}
}class Rectangle extends Shape {}
console.log(Rectangle.getMessage()); // color:blue
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // color:blue
静态字段目前是比较稳定的,并且提供了各种实现:
5. 类静态初始化块
静态私有和公共字段只能让我们在类定义期间执行静态成员的每个字段初始化。如果我们需要在初始化期间像 try … catch 一样进行异常处理,就不得不在类之外编写此逻辑。该提案就提供了一种在类声明/定义期间评估静态初始化代码块的优雅方法,可以访问类的私有字段。
先来看一个例子:
class Person {static GENDER = "Male"static TOTAL_EMPLOYED;static TOTAL_UNEMPLOYED;try {// ...} catch {// ...}
}
上面的代码就会引发错误,可以使用类静态块来重构它,只需将try...catch包裹在 static 中即可:
class Person {static GENDER = "Male"static TOTAL_EMPLOYED;static TOTAL_UNEMPLOYED;static {try {// ...} catch {// ...}}
}
此外,类静态块提供对词法范围的私有字段和方法的特权访问。这里需要在具有实例私有字段的类和同一范围内的函数之间共享信息的情况下很有用。
let getData;class Person {#xconstructor(x) {this.#x = { data: x };}static {getData = (obj) => obj.#x;}
}function readPrivateData(obj) {return getData(obj).data;
}const john = new Person([2,4,6,8]);readPrivateData(john); // [2,4,6,8]
这里,Person 类与 readPrivateData 函数共享了私有实例属性。
三、Temporal
JavaScript 中的日期处理 Date() 对象一直是饱受诟病,该对象是1995 年受到 Java 的启发而实现的,自此就一直没有改变过。虽然Java已经放弃了这个对象,但是 Date() 仍保留在 JavaScript 中来实现浏览器的兼容。
Date() API 存在的问题:
只支持UTC和用户的PC时间;
不支持公历以外的日历;
字符串到日期解析容易出错;
Date 对象是可变的,比如:
const today = new Date();
const tomorrow = new Date(today.setDate(today.getDate() + 1));console.log(tomorrow);
console.log(today);
此时,两个时间输出是一样的,不符合我们的预期。正因为 Date() 对象存在的种种问题。平时我们经常需要借助moment.js
、Day.js
等日期库,但是它们的体积较大,有时一个简单的日期处理就需要引入一个库,得不偿失。
目前,由于Date API 在很多库和浏览器引擎中的广泛使用,没有办法修复API的不好的部分。而改变Date API 的工作方式也很可能会破坏许多网站和库。
正因如此,TC39提出了一个全新的用于处理日期和时间的标准对象和函数——Temporal。新的Temporal API 提案旨在解决Date API的问题。它为 JavaScript 日期/时间操作带来了以下修复:
仅可以创建和处理不可变Temporal对象;
提供用于日期和时间计算的简单 API;
支持所有时区;
从 ISO-8601 格式进行严格的日期解析;
支持非公历。
Temporal 将取代 Moment.js 之类的库,这些库很好地填补了 JavaScript 中的空白,这种空白非常普遍,因此将功能作为语言的一部分更有意义。
由于该提案还未正式发布,所以,可以借助官方提供的prlyfill来测试。首选进行安装:
npm install @js-temporal/polyfill
导入并使用:
import { Temporal } from '@js-temporal/polyfill';console.log(Temporal);
Temporal 对象如下:
下面就来看看 Temporal 对象有哪些实用的功能。
1. 当前时间和日期
Temporal.Now 会返回一个表示当前日期和时间的对象:
// 自1970年1月1日以来的时间(秒和毫秒)
Temporal.Now.instant().epochSeconds;
Temporal.Now.instant().epochMilliseconds;// 当前位置的时间
Temporal.Now.zonedDateTimeISO();// 当前时区
Temporal.Now.timeZone();// 指定时区的当前时间
Temporal.Now.zonedDateTimeISO('Europe/London');
2. 实例时间和日期
Temporal.Instant 根据 ISO 8601 格式的字符串返回一个表示日期和时间的对象,结果会精确到纳秒:
Temporal.Instant.from('2022-02-01T05:56:78.999999999+02:00[Europe/Berlin]');
// 输出结果:2022-02-01T03:57:18.999999999Z
Temporal.Instant.from('2022-02-011T05:06+07:00');
// 输出结果:2022-01-31T22:06:00Z
除此之外,我们还可以获取纪元时间的对应的日期(UTC 1970年1月1日0点是纪元时间):
Temporal.Instant.fromEpochSeconds(1.0e8);
// 输出结果:1973-03-03T09:46:40Z
3. 时区日期和时间
Temporal.ZonedDateTime 返回一个对象,该对象表示在特定时区的日期/时间:
new Temporal.ZonedDateTime(1234567890000, // 纪元时间Temporal.TimeZone.from('Europe/London'), // 时区Temporal.Calendar.from('iso8601') // 默认日历
);Temporal.ZonedDateTime.from('2025-09-05T02:55:00+02:00[Africa/Cairo]');Temporal.Instant('2022-08-05T20:06:13+05:45').toZonedDateTime('+05:45');
// 输出结果:Temporal.ZonedDateTime.from({timeZone: 'America/New_York',year: 2025,month: 2,day: 28,hour: 10,minute: 15,second: 0,millisecond: 0,microsecond: 0,nanosecond: 0
});
// 输出结果:2025-02-28T10:15:00-05:00[America/New_York]
4. 简单的日期和时间
我们并不会总是需要使用精确的时间,因此 Temporal API 提供了独立于时区的对象。这些可以用于更简单的活动。
Temporal.PlainDateTime:指日历日期和时间;
Temporal.PlainDate:指特定的日历日期;
Temporal.PlainTime:指一天中的特定时间;
Temporal.PlainYearMonth:指没有日期成分的日期,例如“2022 年 2 月”;
Temporal.PlainMonthDay:指没有年份的日期,例如“10 月 1 日”。
它们都有类似的构造函数,以下有两种形式来创建简单的时间和日期:
new Temporal.PlainDateTime(2021, 5, 4, 13, 14, 15);
Temporal.PlainDateTime.from('2021-05-04T13:14:15');new Temporal.PlainDate(2021, 5, 4);
Temporal.PlainDate.from('2021-05-04');new Temporal.PlainTime(13, 14, 15);
Temporal.PlainTime.from('13:14:15');new Temporal.PlainYearMonth(2021, 4);
Temporal.PlainYearMonth.from('2019-04');new Temporal.PlainMonthDay(3, 14);
Temporal.PlainMonthDay.from('03-14');
5. 日期和时间值
所有 Temporal 对象都可以返回特定的日期/时间值。例如,使用ZonedDateTime:
const t1 = Temporal.ZonedDateTime.from('2025-12-07T03:24:30+02:00[Africa/Cairo]');t1.year; // 2025
t1.month; // 12
t1.day; // 7
t1.hour; // 3
t1.minute; // 24
t1.second; // 30
t1.millisecond; // 0
t1.microsecond; // 0
t1.nanosecond; // 0
其他有用的属性包括:
dayOfWeek(周一为 1 至周日为 7)
dayOfYear(1 至 365 或 366)
weekOfYear(1 到 52,有时是 53)
daysInMonth(28、29、30、31)
daysInYear(365 或 366)
inLeapYear(true或false)
6. 比较和排序日期
所有 Temporal 对象都可以使用 compare() 返回整数的函数进行比较。例如,比较两个ZonedDateTime对象:
Temporal.ZonedDateTime.compare(t1, t2);
这个比较结果会有三种情况:
当两个时间值相等时,返回 0;
当 t1 在 t2 之后时,返回 1;
当 t1 在 t2 之前时,但会 -1;
const date1 = Temporal.Now,
const date2 = Temporal.PlainDateTime.from('2022-05-01');Temporal.ZonedDateTime.compare(date1, date2); // -1
compare() 的结果可以用于数组的 sort() 方法来对时间按照升序进行排列(从早到晚):
const t = ['2022-01-01T00:00:00+00:00[Europe/London]','2022-01-01T00:00:00+00:00[Africa/Cairo]','2022-01-01T00:00:00+00:00[America/New_York]'
].map(d => Temporal.ZonedDateTime.from(d)).sort(Temporal.ZonedDateTime.compare);
7. 日期计算
提案还提供了几种方法来对任何 Temporal 对象执行日期计算。当传递一个Temporal.Duration对象时,它们都会返回一个相同类型的新的 Temporal,该对象使用years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds 和 nanoseconds 字段来设置时间。
const t1 = Temporal.ZonedDateTime.from('2022-01-01T00:00:00+00:00[Europe/London]');t1.add({ hours: 8, minutes: 30 }); // 往后8小时30分t1.subtract({ days: 5 }); // 往前5天t1.round({ smallestUnit: 'month' }); // 四舍五入到最近的月份
until() 和 since() 方法会返回一个对象,该 Temporal.Duration 对象描述基于当前日期/时间的特定日期和时间之前或之后的时间,例如:
t1.until().months; // 到t1还有几个月t2.until().days; // 到t2还有几天t3.since().weeks; // t3已经过去了几周
equals() 方法用来确定两个日期/时间值是否相同:
const d1 = Temporal.PlainDate.from('2022-01-31');
const d2 = Temporal.PlainDate.from('2023-01-31');
d1.equals(d2); // false
8. 使用国际化 API 格式化日期
虽然这不是 Temporal API 的一部分,但 JavaScript Intl(国际化)API提供了一个 DateTimeFormat() 构造函数,可以用于格式化 Temporal 或 Date 对象:
const d = new Temporal.PlainDate(2022, 3, 14);// 美国日期格式:3/14/2022
new Intl.DateTimeFormat('en-US').format(d);// 英国日期格式:14/3/2022
new Intl.DateTimeFormat('en-GB').format(d);// 西班牙长日期格式:miércoles, 14 de abril de 2022
new Intl.DateTimeFormat('es-ES', { dateStyle: 'full' }).format(d);
附:
TC39 关于 Temporal 的提案进度:https://tc39.es/proposal-temporal/
Chrome 关于 Temporal 的实现进度:https://chromestatus.com/feature/5668291307634688#details
四、内置对象
1. Object.hasOwn()
在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 来检查一个属性是否属于对象。
提案中的 Object.hasOwn 特性是一种更简洁、更可靠的检查属性是否直接设置在对象上的方法。
const example = {property: '123'
};console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property'));
2. at()
at() 是一个数组方法,用于通过给定索引来获取数组元素。当给定索引为正时,这种新方法与使用括号表示法访问具有相同的行为。当给出负整数索引时,就会从数组的最后一项开始检索:
const array = [0,1,2,3,4,5];console.log(array[array.length-1]); // 5
console.log(array.at(-1)); // 5console.log(array[array.lenght-2]); // 4
console.log(array.at(-2)); // 4
除了数组,字符串也可以使用at()方法进行索引:
const str = "hello world";console.log(str[str.length - 1]); // d
console.log(str.at(-1)); // d
3. cause
在 ECMAScript 2022 提案中,new Error() 中可以指定导致它的原因:
function readFiles(filePaths) {return filePaths.map((filePath) => {try {// ···} catch (error) {throw new Error(`While processing ${filePath}`,{cause: error});}});
}
4. 正则表达式匹配索引
这个新提案已经进入第 4 阶段,它将允许我们利用 d 字符来表示我们想要匹配字符串的开始和结束索引。以前,我们只能在字符串匹配操作期间获得一个包含提取的字符串和索引信息的数组。在某些情况下,这是不够的。因此,在这个新提案中,如果设置标志 /d,将额外获得一个带有开始和结束索引的数组。
const matchObj = /(a+)(b+)/d.exec('aaaabb');console.log(matchObj[1]) // 'aaaa'
console.log(matchObj[2]) // 'bb'
由于 /d 标识的存在,matchObj还有一个属性.indices,它用来记录捕获的每个编号组:
console.log(matchObj.indices[1]) // [0, 4]
console.log(matchObj.indices[2]) // [4, 6]
我们还可以使用命名组:
const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');console.log(matchObj.groups.as); // 'aaaa'
console.log(matchObj.groups.bs); // 'bb'
这里给两个字符匹配分别命名为as和bs,然后就可以通过groups来获取到这两个命名分别匹配到的字符串。
它们的索引存储在 matchObj.indices.groups 中:
console.log(matchObj.indices.groups.as); // [0, 4]
console.log(matchObj.indices.groups.bs); // [4, 6]
匹配索引的一个重要用途就是指向语法错误所在位置的解析器。下面的代码解决了一个相关问题:它指向引用内容的开始和结束位置
const reQuoted = /“([^”]+)”/dgu;
function pointToQuotedText(str) {const startIndices = new Set();const endIndices = new Set();for (const match of str.matchAll(reQuoted)) {const [start, end] = match.indices[1];startIndices.add(start);endIndices.add(end);}let result = '';for (let index=0; index < str.length; index++) {if (startIndices.has(index)) {result += '[';} else if (endIndices.has(index+1)) {result += ']';} else {result += ' ';}}return result;
}console.log(pointToQuotedText('They said “hello” and “goodbye”.'));
// ' [ ] [ ] '
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
识别上方二维码加我微信、拉你进源码共读群
今日话题
略。分享、收藏、点赞、在看我的文章就是对我最大的支持~