写点什么

阿里前端面试题

作者:loveX001
  • 2022 年 9 月 22 日
    浙江
  • 本文字数:10075 字

    阅读完需:约 33 分钟

介绍一下 Rollup

Rollup 是一款 ES Modules 打包器。它也可以将项目中散落的细小模块打包为整块代码,从而使得这些划分的模块可以更好地运行在浏览器环境或者 Node.js 环境。


Rollup 优势:


  • 输出结果更加扁平,执行效率更高;

  • 自动移除未引用代码;

  • 打包结果依然完全可读。


缺点


  • 加载非 ESM 的第三方模块比较复杂;

  • 因为模块最终都被打包到全局中,所以无法实现 HMR

  • 浏览器环境中,代码拆分功能必须使用 Require.js 这样的 AMD


  • 我们发现如果我们开发的是一个应用程序,需要大量引用第三方模块,同时还需要 HMR 提升开发体验,而且应用过大就必须要分包。那这些需求 Rollup 都无法满足。

  • 如果我们是开发一个 JavaScript 框架或者库,那这些优点就特别有必要,而缺点呢几乎也都可以忽略,所以在很多像 React 或者 Vue 之类的框架中都是使用的 Rollup 作为模块打包器,而并非 Webpack


总结一下Webpack 大而全,Rollup 小而美


在对它们的选择上,我的基本原则是:应用开发使用 Webpack,类库或者框架开发使用 Rollup


不过这并不是绝对的标准,只是经验法则。因为 Rollup 也可用于构建绝大多数应用程序,而 Webpack 同样也可以构建类库或者框架。



前端进阶面试题详细解答

Promise.all

描述:所有 promise 的状态都变成 fulfilled,就会返回一个状态为 fulfilled 的数组(所有promisevalue)。只要有一个失败,就返回第一个状态为 rejectedpromise 实例的 reason


实现


Promise.all = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return resolve(promises);            let result = [];            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => {                        count++;                        result[index] = value;                        if(count === promises.length) resolve(result);                    },                     reason => reject(reason)                );            })        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

实现一个 add 方法

题目描述:实现一个 add 方法 使计算结果能够满足如下预期:add(1)(2)(3)()=6add(1,2,3)(4)()=10


其实就是考函数柯里化


实现代码如下:


function add(...args) {  let allArgs = [...args];  function fn(...newArgs) {    allArgs = [...allArgs, ...newArgs];    return fn;  }  fn.toString = function () {    if (!allArgs.length) {      return;    }    return allArgs.reduce((sum, cur) => sum + cur);  };  return fn;}
复制代码

说一说你用过的 css 布局

gird布局,layout布局,flex布局,双飞翼,圣杯布局等
复制代码

Promise.reject

Promise.reject = function(reason) {    return new Promise((resolve, reject) => reject(reason));}
复制代码

快排--时间复杂度 nlogn~ n^2 之间

题目描述:实现一个快排


实现代码如下:


function quickSort(arr) {  if (arr.length < 2) {    return arr;  }  const cur = arr[arr.length - 1];  const left = arr.filter((v, i) => v <= cur && i !== arr.length - 1);  const right = arr.filter((v) => v > cur);  return [...quickSort(left), cur, ...quickSort(right)];}// console.log(quickSort([3, 6, 2, 4, 1]));
复制代码

说一说什么是跨域,怎么解决

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。为来防止CSRF攻击1.JSONP    JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。    通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。    <script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>    <script>        function jsonp(data) {            console.log(data)        }    </script>    JSONP 使用简单且兼容性不错,但是只限于 get 请求。2.CORS    CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。3.document.domain    该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域4.webpack配置proxyTable设置开发环境跨域5.nginx代理跨域6.iframe跨域7.postMessage 这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
复制代码

Object.is 实现

题目描述:


Object.is不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。    1. NaN在===中是不相等的,而在Object.is中是相等的    2. +0和-0在===中是相等的,而在Object.is中是不相等的
复制代码


实现代码如下:


Object.is = function (x, y) {  if (x === y) {    // 当前情况下,只有一种情况是特殊的,即 +0 -0    // 如果 x !== 0,则返回true    // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断    return x !== 0 || 1 / x === 1 / y;  }
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样 // x和y同时为NaN时,返回true return x !== x && y !== y;};
复制代码

陈述 http

基本概念:
HTTP,全称为 HyperText Transfer Protocol,即为超文本传输协议。是互联网应用最为广泛的一种网络协议所有的 www 文件都必须遵守这个标准。
http特性:
HTTP 是无连接无状态的HTTP 一般构建于 TCP/IP 协议之上,默认端口号是 80HTTP 可以分为两个部分,即请求和响应。
http请求:
HTTP 定义了在与服务器交互的不同方式,最常用的方法有 4 种分别是 GET,POST,PUT, DELETE。URL 全称为资源描述符,可以这么认为:一个 URL 地址对应着一个网络上的资源,而 HTTP 中的 GET,POST,PUT,DELETE 就对应着对这个资源的查询,修改,增添,删除4个操作。
HTTP 请求由 3 个部分构成,分别是:状态行,请求头(Request Header),请求正文。
HTTP 响应由 3 个部分构成,分别是:状态行,响应头(Response Header),响应正文。
HTTP 响应中包含一个状态码,用来表示服务器对客户端响应的结果。状态码一般由3位构成:
1xx : 表示请求已经接受了,继续处理。2xx : 表示请求已经处理掉了。3xx : 重定向。4xx : 一般表示客户端有错误,请求无法实现。5xx : 一般为服务器端的错误。
比如常见的状态码:
200 OK 客户端请求成功。301 Moved Permanently 请求永久重定向。302 Moved Temporarily 请求临时重定向。304 Not Modified 文件未修改,可以直接使用缓存的文件。400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。401 Unauthorized 请求未经授权,无法访问。403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因。404 Not Found 请求的资源不存在,比如输入了错误的URL。500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。
大概还有一些关于http请求和响应头信息的介绍。
复制代码

深拷贝

实现一:不考虑 Symbol


function deepClone(obj) {    if(!isObject(obj)) return obj;    let newObj = Array.isArray(obj) ? [] : {};    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)    for(let key in obj) {        // obj.hasOwnProperty() 方法只考虑对象自身的属性        if(obj.hasOwnProperty(key)) {            newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];        }    }    return newObj;}
复制代码


实现二:考虑 Symbol


// hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈function deepClone(obj, hash = new WeakMap()) {    if(!isObject(obj)) return obj;    // 检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象    if(hash.has(obj)) return hash.get(obj);    let newObj = Array.isArray(obj) ? [] : {};    // 备份存在hash中,newObj目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈    hash.set(obj, newObj);    // Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。    Reflect.ownKeys(obj).forEach(key => {        // 属性值如果是对象,则进行递归深拷贝,否则直接拷贝        newObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];    });    return newObj;}
复制代码

Vue 通信

1.props和$emit2.中央事件总线 EventBus(基本不用)3.vuex(官方推荐状态管理器)4.$parent和$children当然还有一些其他办法,但基本不常用,或者用起来太复杂来。 介绍来通信的方式,还可以扩展说一下使用场景,如何使用,注意事项之类的。
复制代码

手写题:数组去重

Array.from(new Set([1, 1, 2, 2]))
复制代码

new 操作符

题目描述:手写 new 操作符实现


实现代码如下:


function myNew(fn, ...args) {  let obj = Object.create(fn.prototype);  let res = fn.call(obj, ...args);  if (res && (typeof res === "object" || typeof res === "function")) {    return res;  }  return obj;}用法如下:// // function Person(name, age) {// //   this.name = name;// //   this.age = age;// // }// // Person.prototype.say = function() {// //   console.log(this.age);// // };// // let p1 = myNew(Person, "lihua", 18);// // console.log(p1.name);// // console.log(p1);// // p1.say();
复制代码

JS 闭包,你了解多少?

应该有面试官问过你:


  1. 什么是闭包?

  2. 闭包有哪些实际运用场景?

  3. 闭包是如何产生的?

  4. 闭包产生的变量如何被回收?


这些问题其实都可以被看作是同一个问题,那就是面试官在问你:你对JS闭包了解多少?


来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。


答案1: 就是一个function里面return了一个子函数,子函数访问了外面那个函数的变量。


答案2: for 循环里面可以用闭包来解决问题。


for(var i = 0; i < 10; i++){    setTimeout(()=>console.log(i),0)}// 控制台输出10遍10.for(var i = 0; i < 10; i++){    (function(a){    setTimeout(()=>console.log(a),0)    })(i)} // 控制台输出0-9
复制代码


答案3: 当前作用域产产生了对父作用域的引用。


答案4: 不知道。是跟浏览器的垃圾回收机制有关吗?


开杠了。请问,小伙伴的答案和以上的内容有多少相似程度?


其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。


闭包的底层实现原理


1. JS执行上下文


我们都知道,我们手写的 js 代码是要经过浏览器 V8 进行预编译后才能真正的被执行。例如变量提升、函数提升。举个栗子。


// 栗子:var d = 'abc';function a(){    console.log("函数a");};console.log(a);   // ƒ a(){ console.log("函数a"); }a();              // '函数a'var a = "变量a";  console.log(a);   // '变量a'a();              // a is not a functionvar c = 123;
// 输出结果及顺序:// ƒ a(){ console.log("函数a"); }// '函数a'// '变量a'// a is not a function
// 栗子预编后相当于:function a(){ console.log("函数a");};var d;console.log(a); // ƒ a(){ console.log("函数a"); }a(); // '函数a'
a = "变量a"; // 此时变量a赋值,函数声明被覆盖
console.log(a); // "变量a"a(); // a is not a function
复制代码


那么问题来了。 请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?


是的,你的疑惑没有错。js 代码运行需要一个运行环境,那这个环境就是执行上下文。 是的,js 运行前的预编译也是在这个环境中进行。


js 执行上下文分三种:


  • 全局执行上下文: 代码开始执行时首先进入的环境。

  • 函数执行上下文:函数调用时,会开始执行函数中的代码。

  • eval执行上下文:不建议使用,可忽略。


那么,执行上下文的周期,分为两个阶段:


  • 创建阶段

  • 创建词法环境

  • 生成变量对象(VO),建立作用域链作用域链作用域链(重要的事说三遍)

  • 确认this指向,并绑定this

  • 执行阶段。这个阶段进行变量赋值,函数引用及执行代码。


你现在猜猜看,预编译是发生在什么时候?


噢,我忘记说了,其实与编译还有另一个称呼:执行期上下文


预编译发生在函数执行之前。预编译四部曲为:


  1. 创建AO对象

  2. 找形参和变量声明,将变量和形参作为 AO 属性名,值为undefined

  3. 将实参和形参相统一

  4. 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从AO对象中拿。


所以,预编译真正的结果是:


var AO = {    a = function a(){console.log("函数a");};    d = 'abc'}
复制代码


我们重新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。

2. 变量对象(VO)的创建过程

变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:


  • 创建 arguments 对象。

  • 对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入arguments 对象中。否则,将参数名和 undefined组成的键值对放入 arguments 对象中。


//举个栗子 function bar(a, b, c) {    console.log(arguments);  // [1, 2]    console.log(arguments[2]); // undefined}bar(1,2)
复制代码


  • 当遇到同名的函数时,后面的会覆盖前面的。


console.log(a); // function a() {console.log('Is a ?') }function a() {    console.log('Is a');}function a() {  console.log('Is a ?')}
/**ps: 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖。**/
复制代码


  • 检查当前环境中的变量声明并赋值为undefined。当遇到同名的函数声明,为了避免函数被赋值为 undefined ,会忽略此声明


console.log(a); // function a() {console.log('Is a ?') }console.log(b); // undefinedfunction a() {  console.log('Is a ');}function a() {console.log('Is a ?');}var b = 'Is b';var a = 10086;
/**这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined。**/
复制代码


根据以上三个步骤,对于变量提升也就知道是怎么回事了。

3. 变量对象变为活动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。


我们还是举上面的例子:


console.log(a); // function a() {console.log('fjdsfs') }console.log(b); // undefinedfunction a() {   console.log('Is a');}function a() { console.log('Is a?');}var b = 'Is b';console.log(b); // 'Is b'var a = 10086; console.log(a);  // 10086var b = 'Is b?';console.log(b); // 'Is b?'
复制代码


在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:


// 创建过程EC= {  VO: {}; // 创建变量对象  scopeChain: {}; // 作用域链}VO = {  argument: {...}; // 当前为全局上下文,所以这个属性值是空的  a: <a reference> // 函数 a  的引用地址  b: undefiend  // 见上文创建变量对象的第三步}
复制代码
词法作用域(Lexical scope

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。


再来举个栗子:


 1: let top = 0; //  2: function createWarp() { 3:   function add(a, b) { 4:     let ret = a + b 5:     return ret 6:   } 7:   return add 8: } 9: let sum = createWarp()10: let result = sum(top, 8)11: console.log('result:',result)
复制代码


分析过程如下:


  • 在全局上下文中声明变量top 并赋值为 0.

  • 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。

  • 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined

  • 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它。

  • 调用函数时,回到第 2 行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.

  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。

  • 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义。

  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相

  • 接下来就是简单的执行过程,不再赘述。。

  • ……

  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。


小结一下


现在,如果再让你回答什么是闭包,你能答出多少?


其实,大家说的都对。不管是函数返回一个函数,还是产生了外部作用域的引用,都是有道理的。


所以,什么是闭包?


  • 解释一下作用域链是如何产生的。

  • 解释一下 js 执行上下文的创建、执行过程。

  • 解释一下闭包所产生的变量放在哪了。

  • 最后请把以上 3 点结合起来说给面试官听。

为什么 0.1 + 0.2 != 0.3,请详述理由

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。


我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为


// (0011) 表示循环0.1 = 2^-4 * 1.10011(0011)
复制代码


那么如何得到这个二进制的呢,我们可以来演算下


小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)


回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。


所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004


下面说一下原生解决办法,如下代码所示


parseFloat((0.1 + 0.2).toFixed(10))
复制代码

深浅拷贝

浅拷贝:只考虑对象类型。


function shallowCopy(obj) {    if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {} for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key] } } return newObj}
复制代码


简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。


function deepClone(obj) {    if (typeof obj !== 'object') return;    var newObj = obj instanceof Array ? [] : {};    for (var key in obj) {        if (obj.hasOwnProperty(key)) {            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];        }    }    return newObj;}
复制代码


复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。


const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) { if (map.get(target)) { return target; } // 获取当前值的构造函数:获取它的类型 let constructor = target.constructor; // 检测当前对象target是否与正则、日期格式对象匹配 if (/^(RegExp|Date)$/i.test(constructor.name)) { // 创建一个新的特殊对象(正则类/日期类)的实例 return new constructor(target); } if (isObject(target)) { map.set(target, true); // 为循环引用的对象做标记 const cloneTarget = Array.isArray(target) ? [] : {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget; } else { return target; }}
复制代码

组件之间的传值有几种方式

1、父传子2、子传父3、eventbus4、ref/$refs5、$parent/$children6、$attrs/$listeners7、依赖注入(provide/inject)
复制代码

将虚拟 Dom 转化为真实 Dom

题目描述:JSON 格式的虚拟 Dom 怎么转换成真实 Dom


{  tag: 'DIV',  attrs:{  id:'app'  },  children: [    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] }      ]    },    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] },        { tag: 'A', children: [] }      ]    }  ]}把上诉虚拟Dom转化成下方真实Dom<div id="app">  <span>    <a></a>  </span>  <span>    <a></a>    <a></a>  </span></div>
复制代码


实现代码如下:


// 真正的渲染函数function _render(vnode) {  // 如果是数字类型转化为字符串  if (typeof vnode === "number") {    vnode = String(vnode);  }  // 字符串类型直接就是文本节点  if (typeof vnode === "string") {    return document.createTextNode(vnode);  }  // 普通DOM  const dom = document.createElement(vnode.tag);  if (vnode.attrs) {    // 遍历属性    Object.keys(vnode.attrs).forEach((key) => {      const value = vnode.attrs[key];      dom.setAttribute(key, value);    });  }  // 子数组进行递归操作  vnode.children.forEach((child) => dom.appendChild(_render(child)));  return dom;}
复制代码

Vuex 有哪些基本属性?为什么 Vuex 的 mutation 中不能做异步操作?

有五种,分别是 State、 Getter、Mutation 、Action、 Module1、state => 基本数据(数据源存放地)2、getters => 从基本数据派生出来的数据3、mutations => 提交更改数据的方法,同步4、actions => 像一个装饰器,包裹mutations,使之可以异步。5、modules => 模块化Vuex
1、Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。2、每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
复制代码

手写发布订阅

class EventListener {    listeners = {};    on(name, fn) {        (this.listeners[name] || (this.listeners[name] = [])).push(fn)    }    once(name, fn) {        let tem = (...args) => {            this.removeListener(name, fn)            fn(...args)        }        fn.fn = tem        this.on(name, tem)    }    removeListener(name, fn) {        if (this.listeners[name]) {            this.listeners[name] = this.listeners[name].filter(listener => (listener != fn && listener != fn.fn))        }    }    removeAllListeners(name) {        if (name && this.listeners[name]) delete this.listeners[name]        this.listeners = {}    }    emit(name, ...args) {        if (this.listeners[name]) {            this.listeners[name].forEach(fn => fn.call(this, ...args))        }    }}
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
阿里前端面试题_JavaScript_loveX001_InfoQ写作社区