摘要:它们都用于声明变量。盲目使用替换后可能会导致预期意外的结果。有鉴于此,还是建议使用字符串,布尔和数字类型的数据类型。像使用这种下划线命名约定在一个开源项目中,命名规则很难维持得一直很好,这样经常会造成一些困扰。
今天群里有小伙伴跟我聊天,问了我几个关于ES6的问题,我才意识到,大部分初学者在学习的过程中,都是学了HTML/CSS/JS之后就开始上手学习框架了,而对于ES6的重视程度却不是那么足,或是仅仅了解部分ES6的用法。用 let / const 来代替 var
由于本项目将会用到大部分ES6的新特性,为避免将来有小伙伴看不懂还得去查,今天这篇就多带带为我们之后的开发做一下ES6的知识储备。
因为 JavaScript 的 var 关键字是声明全局的变量,所以在 ES6 中引入了两个新的变量声明来解决这个问题,即 let 和 const 。
它们都用于声明变量。 区别在于 const 在声明后不能改变它的值,而 let 则可以。
和 var 不一样的是,let 和 const 不存在变量提升。
一个 var 的例子:
var snack = "Meow Mix"; function getFood(food) { if (food) { var snack = "Friskies"; return snack; } return snack; } getFood(false); // undefined
使用 let 替换了 var 后的表现:
let snack = "Meow Mix"; function getFood(food) { if (food) { let snack = "Friskies"; return snack; } return snack; } getFood(false); // "Meow Mix"
当我们重构使用 var 的老代码时,一定要注意这种变化。
盲目使用 let 替换 var 后可能会导致预期意外的结果。
注意:let 和 const 是块级作用域语句。所以在语句块以外引用这些变量时,会造成引用错误 ReferenceError。
console.log(x); let x = "hi"; // ReferenceError: x is not defined
在本项目中,将使用 let 声明一个变量,使用 const 来声明一个不可改变的常量。用块级作用域代替 IIFES
我们以往创建一个 立即执行函数 时,一般是在函数最外层包裹一层括号。
ES6支持块级作用域,我们现在可以通过创建一个代码块(Block)来实现,不必通过创建一个函数来实现,
(function () { var food = "Meow Mix"; }()); console.log(food); // Reference Error
使用支持块级作用域的ES6的版本:
{ let food = "Meow Mix"; }; console.log(food); // Reference Error箭头函数
有些时候,我们在函数嵌套中需要访问上下文中的 this。比如下面的例子:
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; // Cannot read property "name" of undefined }); };
一种通用的方式是把上下文中的 this 保存在一个变量里:
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { var that = this; // Store the context of this return arr.map(function (character) { return that.name + character; }); };
我们也可以把 this 通过属性传进去:
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }, this); };
还可以直接使用 bind:
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }.bind(this)); };
而如果使用 箭头函数,this 的值不用我们再做如上几段代码的特殊处理,直接使用即可。
上面的代码可以重写为下面这样:
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(character => this.name + character); };
当你需要维护一个 this 上下文的时候尽量使用 箭头函数。
当我们编写只返回一个表达式值的简单函数时,也可以使用箭头函数,如下:
var squares = arr.map(function (x) { return x * x }); // Function Expression
const arr = [1, 2, 3, 4, 5]; const squares = arr.map(x => x * x); // Arrow Function for terser implementation字符串
在ES6中,字符串对象新增了 .includes() 和 .repeat() 方法。
.includes( )var string = "food"; var substring = "foo"; console.log(string.indexOf(substring) > -1);
现在,我们可以使用 .inclues() 方法,替代以往判断内容 > -1 的方式。
.includes() 方法会极简地返回一个布尔值结果。
const string = "food"; const substring = "foo"; console.log(string.includes(substring)); // true.repeat()
function repeat(string, count) { var strings = []; while(strings.length < count) { strings.push(string); } return strings.join(""); }
在ES6中,我们可以使用一个极简的方法来实现重复字符:
// String.repeat(numberOfRepetitions) "meow".repeat(3); // "meowmeowmeow"字符串模版字面量
使用 字符串模板字面量,我可以在字符串中直接使用特殊字符,而不用转义。
var text = "This string contains "double quotes" which are escaped.";
let text = `This string contains "double quotes" which don"t need to be escaped anymore.`;
字符串模板字面量 还支持直接插入变量,可以实现字符串与变量的直接连接输出。
var name = "Tiger"; var age = 13; console.log("My cat is named " + name + " and is " + age + " years old.");
更简单的版本:
const name = "Tiger"; const age = 13; console.log(`My cat is named ${name} and is ${age} years old.`);
ES5中,我们要这样生成多行文本:
var text = ( "cat " + "dog " + "nickelodeon" );
或者:
var text = [ "cat", "dog", "nickelodeon" ].join(" ");
字符串模板字面量 让我们不必特别关注多行字符串中的换行转义符号,直接换行即可:
let text = ( `cat dog nickelodeon` );
字符串模板字面量 内部可以使用表达式,像这样:
let today = new Date(); let text = `The time and date is ${today.toLocaleString()}`;解构
解构让我们可以使用非常便捷的语法,直接将数组或者对象中的值直接分别导出到多个变量中,
解构数组解构数组
var arr = [1, 2, 3, 4]; var a = arr[0]; var b = arr[1]; var c = arr[2]; var d = arr[3];
let [a, b, c, d] = [1, 2, 3, 4]; console.log(a); // 1 console.log(b); // 2解构对象
解构对象
var luke = { occupation: "jedi", father: "anakin" }; var occupation = luke.occupation; // "jedi" var father = luke.father; // "anakin"
let luke = { occupation: "jedi", father: "anakin" }; let {occupation, father} = luke; console.log(occupation); // "jedi" console.log(father); // "anakin"模块
ES6之前,浏览器端的模块化代码,我们使用像Browserify这样的库,
在 Node.js 中,我们则使用 require。
在ES6中,我们现在可以直接使用AMD 和 CommonJS这些模块了。
module.exports = 1; module.exports = { foo: "bar" }; module.exports = ["foo", "bar"]; module.exports = function bar () {};使用 ES6 的出口
在ES6中,提供了多种设置模块出口的方式,比如我们要导出一个变量,那么使用 变量名 :
export let name = "David"; export let age = 25;
还可以为对象 导出一个列表:
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } export { sumTwo, sumThree };
我们也可以使用简单的一个 export 关键字来导出一个结果值:
export function sumTwo(a, b) { return a + b; } export function sumThree(a, b, c) { return a + b + c; }
最后,我们可以 导出一个默认出口:
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } let api = { sumTwo, sumThree }; export default api; /* * 与以下的语句是对等的: * export { api as default }; */
实践:总是在模块的 最后 使用 export default 方法。ES6 中的导入
它让模块的出口更清晰明了,节省了阅读整个模块来寻找出口的时间。
更多的是,在大量CommonJS模块中,通用的习惯是设置一个出口值或者出口对象。
坚持这个规则,可以让我们的代码更易读,且更方便的联合使用CommonJS和ES6模块。
ES6提供了好几种模块的导入方式。我们可以多带带引入一个文件:
import "underscore";
这里需要注意的是, 整个文件的引入方式会执行该文件内的最上层代码。
就像Python一样,我们还可以命名引用:
import { sumTwo, sumThree } from "math/addition";
我们甚至可以使用 as 给这些模块重命名:
import { sumTwo as addTwoNumbers, sumThree as sumThreeNumbers } from "math/addition";
另外,我们能 引入所有的东西 (也称为命名空间引入)
import * as util from "math/addition";
最后,我们能可以从一个模块的众多值中引入一个列表:
import * as additionUtil from "math/addtion"; const { sumTwo, sumThree } = additionUtil;
像这样引用默认对象:
import api from "math/addition"; // Same as: import { default as api } from "math/addition";
我们建议一个模块导出的值应该越简洁越好,不过有时候有必要的话命名引用和默认引用可以混着用。如果一个模块是这样导出的:
// foos.js export { foo as default, foo1, foo2 };
那我们可以如此导入这个模块的值:
import foo, { foo1, foo2 } from "foos";
我们还可以导入commonjs模块,例如React:
import React from "react"; const { Component, PropTypes } = React;
更简化版本:
import React, { Component, PropTypes } from "react";
注意:被导出的值是被 绑定的,而不是引用。参数
所以,改变一个模块中的值的话,会影响其他引用本模块的代码,一定要避免此种改动发生。
在ES5中,许多种方法来处理函数的 参数默认值(default values),参数数量(indefinite arguments),参数命名(named parameters)。
ES6中,我们可以使用非常简洁的语法来处理上面提到的集中情况。
function addTwoNumbers(x, y) { x = x || 0; y = y || 0; return x + y; }
ES6中,我们可以简单为函数参数启用默认值:
function addTwoNumbers(x=0, y=0) { return x + y; }
addTwoNumbers(2, 4); // 6 addTwoNumbers(2); // 2 addTwoNumbers(); // 0rest 参数
ES5中,遇到参数数量不确定时,我们只能如此处理:
function logArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments[i]); } }
使用 rest 操作符,我们可以给函数传入一个不确定数量的参数列表:
function logArguments(...args) { for (let arg of args) { console.log(arg); } }命名参数
命名函数
ES5中,当我们要处理多个 命名参数 时,通常会传入一个 选项对象 的方式,这种方式被jQuery采用。
function initializeCanvas(options) { var height = options.height || 600; var width = options.width || 400; var lineStroke = options.lineStroke || "black"; }
我们可以利用上面提到的新特性 解构 ,来完成与上面同样功能的函数:
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
如果我们需要把这个参数变为可选的,那么只要把该参数解构为一个空对象就好了:
function initializeCanvas( { height=600, width=400, lineStroke="black"} = {}) { // ... }展开操作
我们可以利用展开操作符(Spread Operator)来把一组数组的值,当作参数传入:
Math.max(...[-1, 100, 9001, -32]); // 9001类 Classes
在ES6以前,我们实现一个类的功能的话,需要首先创建一个构造函数,然后扩展这个函数的原型方法,就像这样:
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Person.prototype.incrementAge = function () { return this.age += 1; };
继承父类的子类需要这样:
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; };
ES6提供了一些语法糖来实现上面的功能,我们可以直接创建一个类:
class Person { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } incrementAge() { this.age += 1; } }
继承父类的子类只要简单的使用 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); } }
实践:ES6新的类语法把我们从晦涩难懂的实现和原型操作中解救出来,这是个非常适合初学者的功能,而且能让我们写出更干净整洁的代码。Symbols
Symbols在ES6版本之前就已经存在了,但现在我们拥有一个公共的接口来直接使用它们。
Symbols是不可更改的(immutable)并且唯一的(unique),它可用作任何hash数据类型中的键。
调用 Symbol() 或者 Symbol(描述文本) 会创建一个唯一的、在全局中不可以访问的Symbol对象。
一个 Symbol() 的应用场景是:在自己的项目中使用第三方代码库,且你需要给他们的对象或者命名空间打补丁代码,又不想改动或升级第三方原有代码的时候。
例如,如果你想给 React.Component 这个类添加一个 refreshComponent 方法,但又确定不了这个方法会不会在下个版本中加入,你可以这么做:
const refreshComponent = Symbol(); React.Component.prototype[refreshComponent] = () => { // do something }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
Symbols常用的一个使用场景,尤其是使用 Symbol.for(key) 方法,是用于实现代码间的互操作。
在你的代码中,通过在包含一些已知接口的第三方库的对象参数中查找Symbol成员,你可以实现这种互操作。
例如:
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"); } }
之后在另一个库中:
const specialRead = Symbol.for("specialRead"); class SomeReadableType { [specialRead]() { const reader = createSomeReaderFrom(this); return reader; } }
注意:关于Symbol互操作的使用,一个值得一提的例子是Symbol.iterable 。Symbol.iterable存在ES6的所有可枚举对象中:数组(Arrays)、Maps
字符串(strings)、生成器(Generators)等等。当它作为一个方法被调用时,它将会返回一个带有枚举接口的对象。
Maps 是一个JavaScript中很重要(迫切需要)的数据结构。
在ES6之前,我们创建一个 hash 通常是使用一个对象:
var map = new Object(); map[key1] = "value1"; map[key2] = "value2";
但是,这样的代码无法避免函数被特别的属性名覆盖的意外情况:
> getOwnProperty({ hasOwnProperty: "Hah, overwritten"}, "Pwned"); > TypeError: Property "hasOwnProperty" is not a function
Maps 让我们使用 set,get 和 search 操作数据。
let map = new Map(); > map.set("name", "david"); > map.get("name"); // david > map.has("name"); // true
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 }
提示:当使用 map.get() 判断值是否相等时,非基础类型比如一个函数或者对象,将不会正常工作。
有鉴于此,还是建议使用字符串,布尔和数字类型的数据类型。
我们还可以使用 .entries() 方法来遍历整个map对象:
for (let [key, value] of map.entries()) { console.log(key, value); }WeakMaps
在ES5之前的版本,我们为了存储私有数据,有好几种方法。像使用这种下划线命名约定:
class Person { constructor(age) { this._age = age; } _incrementAge() { this._age += 1; } }
在一个开源项目中,命名规则很难维持得一直很好,这样经常会造成一些困扰。
此时,我们可以选择使用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"); } } }
使用WeakMaps来保存我们私有数据的理由之一是不会暴露出属性名,就像下面的例子中的 Reflect.ownKeys():
> const person = new Person(50); > person.incrementAge(); // "Midlife crisis" > Reflect.ownKeys(person); // []
一个使用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
上面的例子中,一旦对象被垃圾回收器给销毁了,WeakMaps会自动的把这个对象所对应的键值对数据同时销毁。
提示:结合这个例子,再考虑下jQuery是如何实现缓存带有引用的DOM元素这个功能的。使用WeakMaps的话,当被缓存的DOM元素被移除的时,jQuery可以自动释放相应元素的内存。Promises
通常情况下,在涉及DOM元素存储和缓存的情况下,使用WeakMaps是非常有效的。
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 }); }); }); }); });
写成这样:
func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 });
在ES6之前,我们使用bluebird 或者
Q。现在我们有了原生版本的 Promises:
new Promise((resolve, reject) => reject(new Error("Failed to fulfill Promise"))) .catch(reason => console.log(reason));
这里有两个处理函数,resolve(当Promise执行成功完毕时调用的回调函数) 和 reject (当Promise执行不接受时调用的回调函数)
Promises的好处:大量嵌套错误处理回调函数会使代码变得难以阅读理解。
使用Promises,我们可以通过清晰的路径将错误事件让上传递,并且适当地处理它们。
此外,Promise处理后的值,无论是解决(resolved)还是拒绝(rejected)的结果值,都是不可改变的。
下面是一些使用Promises的实际例子:
var request = require("request"); return new Promise((resolve, reject) => { request.get(url, (error, response, body) => { if (body) { resolve(JSON.parse(body)); } else { resolve({}); } }); });
我们还可以使用 Promise.all() 来 并行化 的处理一组异步的操作。
let urls = [ "/api/commits", "/api/issues/opened", "/api/issues/assigned", "/api/issues/completed", "/api/issues/comments", "/api/pullrequests" ]; let promises = urls.map((url) => { return new Promise((resolve, reject) => { $.ajax({ url: url }) .done((data) => { resolve(data); }); }); }); Promise.all(promises) .then((results) => { // Do something with results of all our promises });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 }
就像上面的例子,当next运行时,它会把我们的generator向前“推动”,同时执行新的表达式。
我们能利用Generators来像书写同步代码一样书写异步代码。
// Hiding asynchronousity with Generators function request(url) { getJSON(url, function(response) { generator.next(response); }); }
这里我们写个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); }
借助于 yield,我们可以保证 entry1 确实拿到数据并转换后再赋值给 data1。
当我们使用generators来像书写同步代码一样书写我们的异步代码逻辑时,没有一种清晰简单的方式来处理期间可能会产生的错误或者异常。在这种情况下,我们可以在我们的generator中引入Promises来处理,就像下面这样:
function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); }); }
我们再写一个函数,其中使用 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); } })(); }
在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); });
在使用Generator后,我们可以重用我们的老版本代码实现,以此展示了Generator的力量。
当使用Generators和Promises后,我们可以像书写同步代码一样书写异步代码的同时优雅地解决了错误处理问题。
此后,我们实际上可以开始利用更简单的一种方式了,它就是async-await。
async await 给我们提供了一种更轻松的、更简单的可以替代的实现上面 Generators 配合 Promises 组合代码的一种编码方式,让我们来看看例子:
var request = require("request"); function getJSON(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); }); } async function main() { var data = await getJSON(); console.log(data); // NOT undefined! } main();
它们看上去和Generators很像。我强烈推荐使用 async await 来替代Generators + Promises的写法。
Getter/Setter 函数ES6 实现了 getter 和 setter 函数,比如下面这个例子:
class Employee { constructor(name) { this._name = name; } get name() { if(this._name) { return "Mr. " + this._name.toUpperCase(); } else { return undefined; } } set name(newName) { if (newName == this._name) { console.log("I already have this name."); } else if (newName) { this._name = newName; } else { return false; } } } var emp = new Employee("James Bond"); if (emp.name) { console.log(emp.name); } emp.name = "Bond 007"; console.log(emp.name);
浏览器也在对象中实现了 getter 和 setter 函数,我们可以使用它们来实现 计算属性,在设置和获取一个属性之前加上监听器和处理。
var person = { firstName: "James", lastName: "Bond", get fullName() { console.log("Getting FullName"); return this.firstName + " " + this.lastName; }, set fullName (name) { console.log("Setting FullName"); var words = name.toString().split(" "); this.firstName = words[0] || ""; this.lastName = words[1] || ""; } } person.fullName; person.fullName = "Bond 007"; person.fullName;总结
虽然我们不使用ES6依然能完成整个项目,但能熟练使用ES6新特性,将会是代码更简洁优雅。
所以,尽快把ES6的知识掌握了吧。记得点好看呦!
本系列文章目录:用vue-cli3从0到1做一个完整功能手机站(一)
从0到1开发实战手机站(二):Git提交规范配置
从0到1使用VUE-CLI3开发实战(三): ES6知识储备
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/101167.html
摘要:开篇从今天起,小肆将和大家从头开始做一个完整的实战项目。关注技术放肆聊跟小肆一起行动起来在这个项目中,小肆力争做到以下几点应用目前最新的技术,并随时间更新。 开篇 从今天起,小肆将和大家从头开始做一个完整的实战项目。其中遇到的每个知识点都是我们工作中常见的,这些知识点大多在网上都能找到但却没有哪个教程能都讲得到,那就由小肆来做吧。 关注技术放肆聊,跟小肆一起行动起来! 在这个项目中,小...
摘要:小肆前几天发了一篇年精品开源项目库的汇总,今天小肆要使用的是在组件中排行第三的。记得点好看呦前置阅读用从到做一个完整功能手机站一从到开发实战手机站二提交规范配置从到使用开发实战三知识储备从到使用开发实战四封装 小肆前几天发了一篇2019年Vue精品开源项目库的汇总,今天小肆要使用的是在UI组件中排行第三的Vuetify。 vuetify介绍 Vuetify是一个渐进式的框架,完全根据M...
摘要:前言一直混迹社区突然发现自己收藏了不少好文但是管理起来有点混乱所以将前端主流技术做了一个书签整理不求最多最全但求最实用。 前言 一直混迹社区,突然发现自己收藏了不少好文但是管理起来有点混乱; 所以将前端主流技术做了一个书签整理,不求最多最全,但求最实用。 书签源码 书签导入浏览器效果截图showImg(https://segmentfault.com/img/bVbg41b?w=107...
阅读 833·2021-11-25 09:43
阅读 3660·2021-11-19 09:40
阅读 861·2021-09-29 09:34
阅读 1757·2021-09-26 10:21
阅读 853·2021-09-22 15:24
阅读 4150·2021-09-22 15:08
阅读 3231·2021-09-07 09:58
阅读 2589·2019-08-30 15:55