在ES6中,标准库也被同样增强了,像字符串对象就新增了 .includes()
和 .repeat()
方法。
.includes( )
var string = 'food'; var substring = 'foo'; console.log(string.indexOf(substring) > -1);
-
1
-
2
-
3
-
4
现在,我们可以使用 .inclues()
方法,替代以往判断内容 > -1
的方式。
.includes()
方法会极简地返回一个布尔值结果。
const string = 'food'; const substring = 'foo'; console.log(string.includes(substring)); // true
-
1
-
2
-
3
-
4
.repeat( )
function repeat(string, count) { var strings = []; while(strings.length < count) { strings.push(string); } return strings.join(''); }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
在ES6中,我们可以使用一个极简的方法来实现重复字符:
// String.repeat(numberOfRepetitions) 'meow'.repeat(3); // 'meowmeowmeow'
-
1
-
2
Template Literals
使用 字符串模板字面量,我可以在字符串中直接使用特殊字符,而不用转义。
var text = "This string contains \"double quotes\" which are escaped.";
- 1
let text = `This string contains "double quotes" which don't need to be escaped anymore.`;
- 1
字符串模板字面量 还支持直接插入变量,可以实现字符串与变量的直接连接输出。
var name = 'Tiger'; var age = 13; console.log('My cat is named ' + name + ' and is ' + age + ' years old.');
-
1
-
2
-
3
-
4
更简单的版本:
const name = 'Tiger'; const age = 13; console.log(`My cat is named ${name} and is ${age} years old.`);
-
1
-
2
-
3
-
4
ES5中,我们要这样生成多行文本:
var text = ( 'cat\n' + 'dog\n' + 'nickelodeon' );
-
1
-
2
-
3
-
4
-
5
或者:
var text = [ 'cat', 'dog', 'nickelodeon' ].join('\n');
-
1
-
2
-
3
-
4
-
5
字符串模板字面量 让我们不必特别关注多行字符串中的换行转义符号,直接换行即可:
let text = ( `cat dog nickelodeon` );
-
1
-
2
-
3
-
4
字符串模板字面量 内部可以使用表达式,像这样:
let today = new Date(); let text = `The time and date is ${today.toLocaleString()}`;
-
1
-
2
(回到目录)
Destructuring
解构让我们可以使用非常便捷的语法,直接将数组或者对象中的值直接分别导出到多个变量中,
Destructurin
g Arrays
解构数组
var arr = [1, 2, 3, 4]; var a = arr[0]; var b = arr[1]; var c = arr[2]; var d = arr[3];
-
1
-
2
-
3
-
4
-
5
let [a, b, c, d] = [1, 2, 3, 4]; console.log(a); // 1 console.log(b); // 2
-
1
-
2
-
3
-
4
Destructuring Objects
解构对象
var luke = { occupation: 'jedi', father: 'anakin' }; var occupation = luke.occupation; // 'jedi' var father = luke.father; // 'anakin'
-
1
-
2
-
3
let luke = { occupation: 'jedi', father: 'anakin' }; let {occupation, father} = luke; console.log(occupation); // 'jedi' console.log(father); // 'anakin'
-
1
-
2
-
3
-
4
-
5
(回到目录)
Modules
ES6之前,浏览器端的模块化代码,我们使用像Browserify这样的库,
在 Node.js 中,我们则使用 require。
在ES6中,我们现在可以直接使用AMD 和 CommonJS这些模块了。
Exporting in CommonJS
module.exports = 1; module.exports = { foo: 'bar' }; module.exports = ['foo', 'bar']; module.exports = function bar () {};
-
1
-
2
-
3
-
4
Exporting in ES6
在ES6中,提供了多种设置模块出口的方式,比如我们要导出一个变量,那么使用 变量名 :
export let name = 'David'; export let age = 25;
-
1
-
2
还可以为对象 导出一个列表:
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } export { sumTwo, sumThree };
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
我们也可以使用简单的一个 export
关键字来导出一个结果值:
export function sumTwo(a, b) { return a + b; } export function sumThree(a, b, c) { return a + b + c; }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
最后,我们可以 导出一个默认出口:
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } let api = { sumTwo, sumThree }; export default api;
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
最佳实践:总是在模块的 最后 使用
export default
方法。
它让模块的出口更清晰明了,节省了阅读整个模块来寻找出口的时间。
更多的是,在大量CommonJS模块中,通用的习惯是设置一个出口值或者出口对象。
最受这个规则,可以让我们的代码更易读,且更方便的联合使用CommonJS和ES6模块。
Importing in ES6
ES6提供了好几种模块的导入方式。我们可以单独引入一个文件:
import 'underscore';
- 1
这里需要注意的是, 整个文件的引入方式会执行该文件内的最上层代码。
就像Python一样,我们还可以命名引用:
import { sumTwo, sumThree } from 'math/addition';
- 1
我们甚至可以使用 as
给这些模块重命名:
import { sumTwo as addTwoNumbers, sumThree as sumThreeNumbers } from 'math/addition';
-
1
-
2
-
3
-
4
另外,我们能 引入所有的东西(原文:import all the things) (也称为命名空间引入)
import * as util from 'math/addition';
- 1
最后,我们能可以从一个模块的众多值中引入一个列表:
import * as additionUtil from 'math/addtion'; const { sumTwo, sumThree } = additionUtil;
-
1
-
2
像这样引用默认对象:
import api from 'math/addition'; // Same as: import { default as api } from 'math/addition';
-
1
-
2
我们建议一个模块导出的值应该越简洁越好,不过有时候有必要的话命名引用和默认引用可以混着用。如果一个模块是这样导出的:
// foos.js export { foo as default, foo1, foo2 };
-
1
-
2
那我们可以如此导入这个模块的值:
import foo, { foo1, foo2 } from 'foos';
- 1
我们还可以导入commonjs模块,例如React:
import React from 'react'; const { Component, PropTypes } = React;
-
1
-
2
更简化版本:
import React, { Component, PropTypes } from 'react';
- 1
注意:被导出的值是被 绑定的(原文:bingdings),而不是引用。
所以,改变一个模块中的值的话,会影响其他引用本模块的代码,一定要避免此种改动发生。
(回到目录)
Parameters
在ES5中,许多种方法来处理函数的 参数默认值(default values),参数数量(indefinite arguments),参数命名(named parameters)。
ES6中,我们可以使用非常简洁的语法来处理上面提到的集中情况。
Default Parameters
function addTwoNumbers(x, y) { x = x || 0; y = y || 0; return x + y; }
-
1
-
2
-
3
-
4
-
5
ES6中,我们可以简单为函数参数启用默认值:
function addTwoNumbers(x=0, y=0) { return x + y; }
-
1
-
2
-
3
addTwoNumbers(2, 4); // 6 addTwoNumbers(2); // 2 addTwoNumbers(); // 0
-
1
-
2
-
3
Rest Parameters
ES5中,遇到参数数量不确定时,我们只能如此处理:
function logArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments[i]); } }
-
1
-
2
-
3
-
4
-
5
使用 rest 操作符,我们可以给函数传入一个不确定数量的参数列表:
function logArguments(...args) { for (let arg of args) { console.log(arg); } }
-
1
-
2
-
3
-
4
-
5
Named Parameters
命名函数
ES5中,当我们要处理多个 命名参数 时,通常会传入一个 选项对象 的方式,这种方式被jQuery采用。
function initializeCanvas(options) { var height = options.height || 600; var width = options.width || 400; var lineStroke = options.lineStroke || 'black'; }
-
1
-
2
-
3
-
4
-
5
我们可以利用上面提到的新特性 解构 ,来完成与上面同样功能的函数:
We can achieve the same functionality using destructuring as a formal parameter
to a function:
function initializeCanvas( { height=600, width=400, lineStroke='black'}) { // ... } // Use variables height, width, lineStroke here
-
1
-
2
-
3
-
4
-
5
如果我们需要把这个参数变为可选的,那么只要把该参数解构为一个空对象就好了:
function initializeCanvas( { height=600, width=400, lineStroke='black'} = {}) { // ... }
-
1
-
2
-
3
-
4
Spread Operator
我们可以利用展开操作符(Spread Operator)来把一组数组的值,当作参数传入:
Math.max(...[-1, 100, 9001, -32]); // 9001
- 1
(回到目录)
Classes
在ES6以前,我们实现一个类的功能的话,需要首先创建一个构造函数,然后扩展这个函数的原型方法,就像这样:
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Person.prototype.incrementAge = function () { return this.age += 1; };
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
继承父类的子类需要这样:
function Personal(name, age, gender, occupation, hobby) { Person.call(this, name, age, gender); this.occupation = occupation; this.hobby = hobby; } Personal.prototype = Object.create(Person.prototype); Personal.prototype.constructor = Personal; Personal.prototype.incrementAge = function () { return Person.prototype.incrementAge.call(this) += 20; };
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
ES6提供了一些语法糖来实现上面的功能,我们可以直接创建一个类:
class Person { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } incrementAge() { this.age += 1; } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
继承父类的子类只要简单的使用 extends
关键字就可以了:
class Personal extends Person { constructor(name, age, gender, occupation, hobby) { super(name, age, gender); this.occupation = occupation; this.hobby = hobby; } incrementAge() { super.incrementAge(); this.age += 20; console.log(this.age); } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
最佳实践:ES6新的类语法把我们从晦涩难懂的实现和原型操作中解救出来,这是个非常适合初学者的功能,而且能让我们写出更干净整洁的代码。
(回到目录)
Symbols
符号(Symbols)在ES6版本之前就已经存在了,但现在我们拥有一个公共的接口来直接使用它们。
Symbols对象是一旦创建就不可以被更改的(immutable)而且能被用做hash数据类型中的键。
Symbol( )
调用 Symbol()
或者 Symbol(描述文本)
会创建一个唯一的、在全局中不可以访问的符号对象。
一个 Symbol()
的应用场景是:在自己的项目中使用第三方代码库,且你需要给他们的对象或者命名空间打补丁代码,又不想改动或升级第三方原有代码的时候。
举个例子,如果你想给 React.Component
这个类添加一个 refreshComponent
方法,但又确定不了这个方法会不会在下个版本中加入,你可以这么做:
const refreshComponent = Symbol(); React.Component.prototype[refreshComponent] = () => { // do something }
-
1
-
2
-
3
-
4
-
5
Symbol.for(key)
使用 Symbol.for(key)
也是会创建一个不可改变的Symbol对象,但区别于上面的创建方法,这个对象是在全局中可以被访问到的。
调用两次 Symbol.for(key)
会返回相同的Symbol实例。
提示:这并不同于 Symbol(description)
。
Symbol('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol.for('foo') // true
-
1
-
2
-
3
一个Symbols常用的使用场景,是需要使用特别 Symbol.for(key)
方法来实现代码间的协作。
这能让你在你的代码中,查找包含已知的接口的第三方代码中Symbol成员。(译者:这句话好难翻。。。原文:This can be
achieved by having your code look for a Symbol member on object arguments from third parties that contain some known interface. )举个例子:
function reader(obj) { const specialRead = Symbol.for('specialRead'); if (obj[specialRead]) { const reader = obj[specialRead](); // do something with reader } else { throw new TypeError('object cannot be read'); } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
之后在另一个库中:
const specialRead = Symbol.for('specialRead'); class SomeReadableType { [specialRead]() { const reader = createSomeReaderFrom(this); return reader; } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
注意:
Symbol.iterable
在ES6中像其他可枚举的对象,如数组,字符串,generators一样,当这个方法被调用时会激活一个枚举器并返回一个对象。
(回到目录)
Maps
Maps 是一个Javascript中很重要(迫切需要)的数据结构。
在ES6之前,我们创建一个 hash 通常是使用一个对象:
var map = new Object(); map[key1] = 'value1'; map[key2] = 'value2';
-
1
-
2
-
3
但是,这样的代码无法避免函数被特别的属性名覆盖的意外情况:
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned'); > TypeError: Property 'hasOwnProperty' is not a function
-
1
-
2
Maps 让我们使用 set
,get
和 search
操作数据。
let map = new Map(); > map.set('name', 'david'); > map.get('name'); // david > map.has('name'); // true
-
1
-
2
-
3
-
4
Maps最强大的地方在于我们不必只能使用字符串来做key了,现在可以使用任何类型来当作key,而且key不会被强制类型转换为字符串。
let map = new Map([ ['name', 'david'], [true, 'false'], [1, 'one'], [{}, 'object'], [function () {}, 'function'] ]); for (let key of map.keys()) { console.log(typeof key); // > string, boolean, number, object, function }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
提示:当使用
map.get()
判断值是否相等时,非基础类型比如一个函数或者对象,将不会正常工作。
有鉴于此,还是建议使用字符串,布尔和数字类型的数据类型。
我们还可以使用 .entries()
方法来遍历整个map对象:
for (let [key, value] of map.entries()) { console.log(key, value); }
-
1
-
2
-
3
(回到目录)
WeakMaps
在ES5之前的版本,我们为了存储私有数据,有好几种方法。像使用这种下划线命名约定:
class Person { constructor(age) { this._age = age; } _incrementAge() { this._age += 1; } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
在一个开源项目中,命名规则很难维持得一直很好,这样经常会造成一些困扰。
此时,我们可以选择使用WeakMaps来替代Maps来存储我们的数据:
let _age = new WeakMap(); class Person { constructor(age) { _age.set(this, age); } incrementAge() { let age = _age.get(this) + 1; _age.set(this, age); if (age > 50) { console.log('Midlife crisis'); } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
使用WeakMaps来保存我们私有数据的理由之一是不会暴露出属性名,就像下面的例子中的 Reflect.ownKeys()
:
> const person = new Person(50); > person.incrementAge(); // 'Midlife crisis' > Reflect.ownKeys(person); // []
-
1
-
2
-
3
一个使用WeakMaps存储数据更实际的例子,就是有关于一个DOM元素和对该DOM元素(有污染)地操作:
let map = new WeakMap(); let el = document.getElementById('someElement'); // Store a weak reference to the element with a key map.set(el, 'reference'); // Access the value of the element let value = map.get(el); // 'reference' // Remove the reference el.parentNode.removeChild(el); el = null; value = map.get(el); // undefined
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
上面的例子中,一个对象被垃圾回收期给销毁了,WeakMaps会自动的把自己内部所对应的键值对数据同时销毁。
提示:结合这个例子,再考虑下jQuery是如何实现缓存带有引用的DOM元素这个功能的,使用了WeakMaps的话,当被缓存的DOM元素被移除的时,jQuery可以自动释放相应元素的内存。
通常情况下,在涉及DOM元素存储和缓存的情况下,使用WeakMaps是非常适合的。
(回到目录)
Promises
Promises让我们让我们多缩进难看的代码(回调地狱):
func1(function (value1) { func2(value1, function (value2) { func3(value2, function (value3) { func4(value3, function (value4) { func5(value4, function (value5) { // Do something with value 5 }); }); }); }); });
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
写成这样:
func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 });
-
1
-
2
-
3
-
4
-
5
-
6
-
7
在ES6之前,我们使用bluebird 或者
Q。现在我们有了原生版本的 Promises:
new Promise((resolve, reject) => reject(new Error('Failed to fulfill Promise'))) .catch(reason => console.log(reason));
-
1
-
2
-
3
这里有两个处理函数,resolve(当Promise执行成功完毕时调用的回调函数) 和 reject (当Promise执行不接受时调用的回调函数)
Promises的好处:大量嵌套错误回调函数会使代码变得难以阅读理解。
使用了Promises,我们可以让我们代码变得更易读,组织起来更合理。
此外,Promise处理后的值,无论是解决还是拒绝的结果值,都是不可改变的。
下面是一些使用Promises的实际例子:
var fetchJSON = function(url) { return new Promise((resolve, reject) => { $.getJSON(url) .done((json) => resolve(json)) .fail((xhr, status, err) => reject(status + err.message)); }); };
-
1
-
2
-
3
-
4
-
5
-
6
-
7
我们还可以使用 Promise.all()
来异步的 并行 处理一个数组的数据。
var urls = [ 'http://www.api.com/items/1234', 'http://www.api.com/items/4567' ]; var urlPromises = urls.map(fetchJSON); Promise.all(urlPromises) .then(function (results) { results.forEach(function (data) { }); }) .catch(function (err) { console.log('Failed: ', err); });
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
(回到目录)
Generators
就像Promises如何让我们避免回调地狱一样,Generators也可以使我们的代码扁平化,同时给予我们开发者像开发同步代码一样的感觉来写异步代码。Generators本质上是一种支持的函数,随后返回表达式的值。
Generators实际上是支持暂停运行,随后根据上一步的返回值再继续运行的一种函数。
下面代码是一个使用generators函数的简单例子:
function* sillyGenerator() { yield 1; yield 2; yield 3; yield 4; } var generator = sillyGenerator(); > console.log(generator.next()); // { value: 1, done: false } > console.log(generator.next()); // { value: 2, done: false } > console.log(generator.next()); // { value: 3, done: false } > console.log(generator.next()); // { value: 4, done: false }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
就像上面的例子,当next运行时,它会把我们的generator向前“推动”,同时执行新的表达式。
我们能利用Generators来像书写同步代码一样书写异步代码。
// Hiding asynchronousity with Generators function request(url) { getJSON(url, function(response) { generator.next(response); }); }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
这里我们写个generator函数将要返回我们的数据:
function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); }
-
1
-
2
-
3
-
4
-
5
-
6
借助于 yield
,我们可以保证 entry1
确实拿到数据并转换后再赋值给 data1
。
当我们使用generators来像书写同步代码一样书写我们的异步代码逻辑时,没有一种清晰简单的方式来处理期间可能会产生的错误或者异常。在这种情况下,我们可以在我们的generator中引入Promises来处理,就像下面这样:
function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); }); }
-
1
-
2
-
3
-
4
-
5
我们再写一个函数,其中使用 next
来步进我们的generator的同事,再利用我们上面的 request
方法来产生(yield)一个Promise。
function iterateGenerator(gen) { var generator = gen(); var ret; (function iterate(val) { ret = generator.next(); if(!ret.done) { ret.value.then(iterate); } })(); }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
在Generator中引入了Promises后,我们就可以通过Promise的 .catch
和 reject
来捕捉和处理错误了。
使用了我们新版的Generator后,新版的调用就像老版本一样简单可读(译者注:有微调):
iterateGenerator(function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); });
-
1
-
2
-
3
-
4
-
5
-
6
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取
书写异步代码。
// Hiding asynchronousity with Generators function request(url) { getJSON(url, function(response) { generator.next(response); }); }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
这里我们写个generator函数将要返回我们的数据:
function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); }
-
1
-
2
-
3
-
4
-
5
-
6
借助于 yield
,我们可以保证 entry1
确实拿到数据并转换后再赋值给 data1
。
当我们使用generators来像书写同步代码一样书写我们的异步代码逻辑时,没有一种清晰简单的方式来处理期间可能会产生的错误或者异常。在这种情况下,我们可以在我们的generator中引入Promises来处理,就像下面这样:
function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); }); }
-
1
-
2
-
3
-
4
-
5
我们再写一个函数,其中使用 next
来步进我们的generator的同事,再利用我们上面的 request
方法来产生(yield)一个Promise。
function iterateGenerator(gen) { var generator = gen(); var ret; (function iterate(val) { ret = generator.next(); if(!ret.done) { ret.value.then(iterate); } })(); }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
在Generator中引入了Promises后,我们就可以通过Promise的 .catch
和 reject
来捕捉和处理错误了。
使用了我们新版的Generator后,新版的调用就像老版本一样简单可读(译者注:有微调):
iterateGenerator(function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); });
-
1
-
2
-
3
-
4
-
5
-
6
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-mgTQrxhW-1719183459720)]
[外链图片转存中…(img-WMW8AlgA-1719183459721)]
[外链图片转存中…(img-0O7Cijhn-1719183459721)]
[外链图片转存中…(img-GNKWuFzR-1719183459722)]
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
如果你需要这些资料, ⬅ 专栏获取