1. 首页
  2. 杂谈

手撕 44 道 JavaScript 送命题,看看你知道几个

手撕 44 道 JavaScript 送命题,看看你知道几个

最近有位同学分享给我一个网站,上面列举了 44 道 JavaScript 的送命题,想让我做一下。

对于这种要求我能不满足么,果断开搞哇。

网站地址:http://javascript-puzzlers.herokuapp.com/

这个网站的名字叫做 JavaScript Puzzlers! ,直译过来就是 JavaScript 的难题,里面大部分都是让人摸不到头脑的题目,我被活活的虐待了近 4 个小时,简直毫无人性好不好。

客观的说一句,对于不是专业前端的人来讲,有点难的过分了。

这套题目做下来的结果是不仅开始怀疑智商,而且是已经上升到怀疑人生的高度了。

里面有一部分题目作者自己也解释不清,只能通过实验得到答案。

这 44 个问题是在 ECMA 262(5.1) 环境下,浏览器中试验的,如果是 node 环境下可能不同。

所有的题目打开 Chrome 的 F12 在 console 中输入题目都能得到答案,至少实验得到的答案的途径是相当方便的。

本文内容过于硬核,并且全程无尿点,需要具备一定的 JavaScript 基础,并且最好还能自备一点速效救心丸。

1. map 和 parseInt

题目:["1", "2", "3"].map(parseInt) 的结果是什么?

知识点:

  • map
  • parseInt

map 的原型方法:

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array 
}[, thisArg])

map 可以接受两个参数,一个是回调函数 callback 。

  • callback:必须。函数,数组中的每个元素都会执行这个函数
  • currentValue:必须。当前元素的值
  • index:可选。当前元素的索引值
  • array:可选。当前元素属于的数组对象
  • thisArg:可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。如果省略了 thisArg ,或者传入 null、undefined,那么回调函数的 this 为全局对象。

parseInt 的语法:

parseInt(string, radix)
  • string:必需。要被解析的字符串。
  • radix:可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

那么这道题把 map 函数去掉的话,实际上相当于下面 3 句话:

parseInt('1', 0);
parseInt('2', 1);
parseInt('3', 2);

上面这三句话只有第一句会把 radix 这个参数默认为 10 以外,其他都不满足 radix 介于 2 ~ 36 之间,所以这个题的返回值是 [1, NaN, NaN] 。

2. typeof 和 instanceof

题目:[typeof null, null instanceof Object] 的结果是什么?

知识点:

  • typeof
  • instanceof

typeof 的一些常见结果:

type result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Host object Implementation-dependent
Function对象 "function"
Object "object"

不要问为啥,就是这么定义的。

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

所以这道题的结果是:[object, false]

3. reduce 和 Math.pow

题目:[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ] 的结果是什么?

知识点:

  • reduce
  • Math.pow

arr.reduce 的原型方法如下:

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue]) 

arr.reduce 方法对数组中每个元素执行一个自定义的 reducer 函数,并将结果汇总为单个值,这个常常用来累加一个数组中的数字。

  • callback:必需。用于执行每个数组元素的函数。
  • accumulator:它是上一次回调函数时得到的值,或者initialValue。
  • currentValue:数组中正在处理的当前元素。
  • index:可选,数组中正在处理的元素的索引,如果提供了 initialValue ,则起始索引号为 0 ,否则从索引 1 开始。
  • initialValue:作为第一次调用 callback 时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用 reduce 将报错。

注意:如果没有提供 initialValue,reduce 会从索引 1 的地方开始执行 callback 方法,跳过第一个索引。如果提供 initialValue ,从索引 0 开始。

Math.pow(base, exponent),返回基数 base 的指数 exponent 次幂。

所以这道题里面的第一个表达式在调用 reduce 方法的时候没有提供 initialValue ,从索引 1 开始执行,第一次执行的时候 accumulator 取 arr[0] ,这里是 3 , currentValue 取第二个值,这里是 2 ,传给 Math.pow ,得到 9 。

第二个表达式是在空数组上调用 reduce ,并且没有提供 initialValue ,所以直接报错 Uncaught TypeError: Reduce of empty array with no initial value 。

所以最后整个表达式的结果还是抛出错误。

4. 三目运算符优先级

题目:下面这段代码的输出是什么?

var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');

这个题实际是在考察三目运算符和加号的优先级,三目运算符的优先级相当低,所以先计算加号再计算三目运算符。

这道题实际等价于 'Value is true' ? 'Somthing' : 'Nonthing' ,而不是看起来的 'Value is' + (true ? 'Something' : 'Nonthing')

所以结果是: Something 。

5. 变量提升

题目:下面这段代码的输出是什么?

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

在 JavaScript 中, functions 和 variables 会被提升。变量提升是 JavaScript 将声明移至作用域 scope (全局域或者当前函数作用域) 顶部的行为,所以上面这段代码其实相当于:

var name = 'World!';
(function () {
    var name = 'undefined';
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

因为代码放在一个闭包里,用外层那个 name = 'World!' 是不能访问的,闭包有隔离变量的作用。

这道题的答案是:Goodbye Jack 。

6. JavaScript 最大数

题目:下面这段代码的输出是什么?

var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
    count++;
}
console.log(count);

这道题看起来是在考察循环,实际上是在考察 JavaScript 能表示的最大的数。

在 JavaScript 里, Math.pow(2, 53) == 9007199254740992 是可以表示的最大值。

而最大值 +1 后还是最大值,所以循环并不会停下来,会成为无限循环。

7. 稀疏数组

题目:下面这段代码的输出是什么?

var ary = [0,1,2];
ary[10] = 10;
var result = ary.filter(function(x) { return x === undefined;});

这道题考察的是稀疏数组,在数组中未被赋值的元素是空。

这道题中,首先给 ary 数组的前三位赋值,然后又给第 11 位赋值,在中间夹杂的 7 位都是空的,并不是 undefined 。

所以在 filter 语句中,返回的是空数组 [] 。

8. 数字精度

题目:下面这段代码的输出是什么?

var two   = 0.2
var one   = 0.1
var eight = 0.8
var six   = 0.6
[two - one == one, eight - six == two]

JavaScript 的 Number 类型为双精度 IEEE 754 64 位浮点类型。

问题产生的原因是因为计算机是通过二进制进行计算的,而二进制的实现和位数的限制导致有些数无法有限表示。

例如浮点数四舍五入,会模仿十进制进行四舍五入,由于二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这是计算机中部分浮点数运算时出现误差,丢失精度的根本原因。

遵循 IEEE 754 标准的语言都有这个问题,比如说另外几个我非常熟悉的语言:Java 和 Python。

看似有穷的数字, 在计算机的二进制表示里却是无穷的,由于存储位数限制因此存在「舍去」,精度丢失就发生了。

这就造成了 0.2 + 0.1 = 0.30000000000000004 和 0.8 – 0.6 = 0.20000000000000007 。

所以这道题的结果是 [true, false]

9. switch 中的 case

题目:下面这段代码的输出是什么?

function showCase(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase(new String('A'));

这道题是考察 case 语句的判断方式,在 JavaScript 中 case 是使用 === 来判断的,这就造成了 A !== new String('A') ,所以最后的结果是 ‘Do not know!’ 。

这里都说道 === 了那就顺便再多一句嘴,说下 ==== 会调用对象的 toString() 方法,这样就会产生 A == new String('A') 了。

10. String() 函数

题目:下面这段代码的输出是什么?

function showCase2(value) {
    switch(value) {
    case 'A':
        console.log('Case A');
        break;
    case 'B':
        console.log('Case B');
        break;
    case undefined:
        console.log('undefined');
        break;
    default:
        console.log('Do not know!');
    }
}
showCase2(String('A'));

这道题和上面一道题很像,但是不一样, String('A') 并不会去创建一个对象,而是会返回一个字符串 'A' ,所以这里最终会打印 'Case A'

11. 求余数运算

题目:下面这段代码的输出是什么?

function isOdd(num) {
    return num % 2 == 1;
}
function isEven(num) {
    return num % 2 == 0;
}
function isSane(num) {
    return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);

这道题是考察求余数,前面两个数字 7 和 4 没什么好说的,字符串 '13' 在计算的过程中会转化成整形参与计算,余数为 1 ,而 -9 参与运算会保留负号为 -1 , Infinity 参与计算会得到 NaN ,最后要注意一点是两个或 | 符号计算时,只要有一个为 true 则都为 true ,所以结果是 [true, true, true, false, false]

12. parseInt()

题目:下面这段代码的输出是什么?

parseInt(3, 8)
parseInt(3, 2)
parseInt(3, 0)

这道题翻译一下:

把 8 进制里的 3 转化成 10 进制,结果是 3 。

把 2 进制中的 3 转换成 10 进制, 2 进制中没有 3 ,所以结果是 NaN 。

把 10 进制中的 3 转换成 10 进制,结果当然还是 3 。

13. Array.prototype

题目:下面这段代码的输出是什么?

console.log(Array.isArray(Array.prototype))

这道题是在问 Array.property 是一个 Array 函数的原型对象?还是一个数组?

在浏览器中试一下,结果是 true 。

14. if

题目:下面这段代码的输出是什么?

var a = [0];
if ([0]) {
  console.log(a == true);
} else {
  console.log("wut");
}

这道题是在问 JavaScript 中的 if 条件语句在什么情况下为 true 。

MDN 中有明确的介绍:

任何一个值,只要它不是 undefined、null、 0、NaN或空字符串(""),那么无论是任何对象,即使是值为假的Boolean对象,在条件语句中都为真。

这道题接下来就是在问 [0] == true 的结果,当然结果是 false 。

15. 对象比较

题目:下面这段代码的输出是什么?

[]==[]

[] 是一个空数组,数组属于对象,而两个对象是怎么也不可能相等的,所以答案是 false 。

16. + 号

题目:下面这段代码的输出是什么?

'5' + 3
'5' - 3

加号遇到字符串的时候就是字符串连接符,而减号正好相反,它是把字符串转换成整形,所以这道题的结果是 ['53', 2]

17. 加减和正负号运算

题目:下面这段代码的输出是什么?

1 + - + + + - + 1

这道题我稍微翻译一下,实际上可以修改为 1 + (- + + + – + 1) ,接下来就是负负得正,正正得正,正负得负,所以最后得到的结果是 2 。

18. 稀疏数组

题目:下面这段代码的输出是什么?

var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; });

还是稀疏数组, map 函数会忽略掉其中的空元素,所以最终输出的结果是 ["1", empty × 2]

19. argument

题目:下面这段代码的输出是什么?

function sidEffecting(ary) {
  ary[0] = ary[2];
}
function bar(a,b,c) {
  c = 10
  sidEffecting(arguments);
  return a + b + c;
}
bar(1,1,1)

argument 是一个类数组对象,修改对象的属性值会影响其他使用到对象的地方,即使变量不在同一范围内。

跟着算下来结果是 21 。

20. JavaScript 最大数

题目:下面这段代码的输出是什么?

var a = 111111111111111110000,
    b = 1111;
a + b;

又是一个最大数问题, JavaScript 中最大值是 Math.pow(2, 53) = 9007199254740992 。

超过这个数以后就不准确了,不准确的含义是不确定。所以这里输出的还是 111111111111111110000 ,是 a 的值。

21. Array.property.reverse

题目:下面这段代码的输出是什么?

var x = [].reverse;
x();

这道题没搞懂,原文的解释是说 reverse 方法会返回调用这个方法的数组本身(就是this),但是 x() 没有调用者,所以 this 指向了全局对象 window 。

但是我在 Chrome 浏览器中尝试的时候是得到了一个错误信息:Uncaught TypeError: Cannot convert undefined or null to object at reverse (<anonymous>)

22. Number.MIN_VALUE

题目:下面这段代码的输出是什么?

Number.MIN_VALUE > 0

这个是在问 Number.MIN_VALUE 的值。

实际上 Number.MIN_VALUE 表示最小正数,即最接近 0 的正数 (实际上不会变成 0)。最大的负数是 -MIN_VALUE 。

所以 Number.MIN_VALUE 是大于 0 的,这里输出是 true 。

23. boolean 强制转换

题目:下面这段代码的输出是什么?

[1 < 2 < 3, 3 < 2 < 1]

先简单转换下: [true < 3, false < 1]

在大于号,小于号运算中 true 会被强制转换为 1 , false 会被强制转换成 0 。

所以这里的结果是 [true, true]

24. 数组比较

题目:下面这段代码的输出是什么?

2 == [[[2]]]

这个题作者骂娘了,原文是 the most classic wtf 。

先公布答案,这个题是 true ,但是我没懂,

自己强行编一个理由解释一波, == 在遇到数组的时候会将数组变成字符串然后进行比较。

25. 3. 和 .3

题目:下面这段代码的输出是什么?

3.toString()
3..toString()
3...toString()

首先搞清楚一件事情,3. 和 .3 都是合法的数字,一个是省略了后面的 0 ,一个是省略了前面的 0 ,都是可以的。

第一句中 toString() 不是数字,所以会报错:Uncaught SyntaxError: Invalid or unexpected token 。

第二句话可以这么拆开看:(3.).toString() ,这样输出一个字符串 '3' 是没有问题的。

第三句话 ..toString() 不是一个合法的数字,所以一样是报错。

26. var 和闭包

题目:下面这段代码的输出是什么?

(function(){
  var x = y = 1;
})();
console.log(y);
console.log(x);

因为闭包有隔离变量的作用,并且 var 不能提升变量,在闭包外部 x 是访问不到的,所以会输出 Undefined ,但是这里声明 y 是直接 y = 1 ,这样声明的 y 反而是全局的,所以外部可以直接访问到 y 。

27. 正则表达式

题目:下面这段代码的输出是什么?

var a = /123/,
    b = /123/;
a == b
a === b

正则表达式是对象,这是两个对象,所以他们是不会相等的,结果是两个false 。

28. 数组比较

题目:下面这段代码的输出是什么?

var a = [1, 2, 3],
    b = [1, 2, 3],
    c = [1, 2, 4]
a ==  b
a === b
a >   c
a <   c

这道题是数组的比较。

使用 == 或者 === 判断两个数组是不会相等的,所以前两个是 false 。

而大于,小于比较是按照顺序比较的,如 a[0]c[0] 相等,接着比较 a[1]c[1] ,然后顺次往下比较,直到比出结果。

29. 构造函数的原型

题目:下面这段代码的输出是什么?

var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]

JavaScript 中函数才有 prototype 属性,对象是没有的,对象有 proto ,指向对象的构造函数的原型,所以 a.prototype 是 undefined 。 b 是 Object 函数的原型,是一个对象,所以第一个是 false 。第二个是使用 getPrototypeOf 方法获取对象 a 的原型(即 a.proto ),这个和 Object.prototype 相等的,所以第二个表达式返回 true 。

30. 函数的原型对象

题目:下面这段代码的输出是什么?

function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b

这还是考察函数原型。

这里 f.prototype 是获取函数 f 的原型对象, Object.getPrototypeOf(f) 是获取对象构造函数的原型,注意函数也是对象,它的构造函数是 Function() ,因此f的构造函数的原型是 Function.property ,因此这这里输出 false 。

31. Function.name

题目:下面这段代码的输出是什么?

function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]

这道题是在考察函数的属性 name ,function.name 属性返回函数实例的名称,这个属性是不可写的。

所以这一题输出的是[‘foo’, ‘foo’]。

32. str.replace

题目:下面这段代码的输出是什么?

"1 2 3".replace(/\d/g, parseInt)

str.replace 方法的原型如下:

str.replace(regexp|substr, newSubStr|function) 

参数解释:

  • regexp (pattern):一个正则表达式对象(即RegExp对象)或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。它和substr是二选一的。
  • substr (pattern):一个将被newSubStr替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅第一个匹配项会被替换。它和regexp是二选一的。
  • newSubStr (replacement):用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名。参考下面的使用字符串作为参数。它和function是二选一的。
变量名 代表的值
$$ 插入一个 "$"。
$& 插入匹配的子串。
$` 插入当前匹配的子串左边的内容。
$’ 插入当前匹配的子串右边的内容。
$n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始
  • function (replacement):一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。它和newSubStr是二选一的。
变量名 代表的值
match 匹配的子串。(对应于上述的$&。)
p1,p2, … 假如replace()方法的第一个参数是一个RegExp 对象,则代表第n个括号匹配的字符串。(对应于上述的1,2等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+,p2 就是匹配的 \b+。
offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 ‘abcd’,匹配到的子字符串是 ‘bc’,那么这个参数将会是 1)
string 被匹配的原字符串。
NamedCaptureGroup 命名捕获组匹配的对象

replace 方法相当的复杂,这道题中,第一个参数是一个正则表达式,第二个参数是一个函数。

那么可以相当于执行下面的语句:

parseInt(‘1’, 0) // 10进制里的1是1
parseInt(‘2’, 2) // 2进制里没有2,所以NaN
parseInt(‘3’, 4) // 4进制中的3是3

所以最终结果是"1 NaN 3"

33. eval

题目:下面这段代码的输出是什么?

function f() {}
var parent = Object.getPrototypeOf(f);
f.name // ?
parent.name // ?
typeof eval(f.name) // ?
typeof eval(parent.name) //  ?

第一句f.name就是「f」。

parent 是 Function.prototype ,这是一个对象,它没有 name 属性,所以第二句应该是啥都不输出。

eval() 函数会将传入的字符串当做 JavaScript 代码进行执行,eval(f.name)相当 于eval(‘f’) ,执行结果是输出函数 f 的内容,type of 计算返回 function 。

最后一句返回空。

34. exp.test

题目:下面这段代码的输出是什么?

var lowerCaseOnly =  /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]

RegExp.prototype.test() 方法的参数是一个字符串,如果不是字符串会尝试转换成字符串。

这两句相当于 lowerCaseOnly.test("null"),lowerCaseOnly.test(”Undefined“) ,所以返回 [true, true] 。

35. 数组

题目:下面这段代码的输出是什么?

[,,,].join(", ")

这道题其实是在考察数组, [,,,] 实际上是定义了一个有三个空元素的数组,输出是 [empty × 3] 。

三个空元素用逗号连接起来最后输出是两个逗号,因为都是空元素,其实最后一个逗号后面是有一个空元素的。

36. class

题目:下面这段代码的输出是什么?

var a = {class: "Animal", name: 'Fido'};
a.class

class 是关键字,尽量不要使用,我在 Chrome 的浏览器中得到的结果是 "Animal" ,虽然答案并不是这个,但是还是要注意不要使用关键字。

37. 时间转换

题目:下面这段代码的输出是什么?

var a = new Date("epoch")

new Date() 构造函数传入的必须是一个时间字符串,即可以通过 Date.parse() 解析成功。所以本题的答案是 Invalid Date 。

38. 函数形参

题目:下面这段代码的输出是什么?

var a = Function.length,
    b = new Function().length
a === b

Function 构造器本身也是个 Function 。他的 length 属性值为 1,所以 a=1 。

new Function() 是一个对象,他没有形参,说以 b=0 ,本题输出 false 。

39. 时间转换

题目:下面这段代码的输出是什么?

var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c]

a 是一个时间字符串, b 是一个时间对象,表示格林威治 0 时, c 也是一个时间对象,是当前时间。

所以这道题的结果是 [false, false, false]

40. Math.max() 和 Math.min()

题目:下面这段代码的输出是什么?

var min = Math.min(), max = Math.max()
min < max

Max.max() 和 Max.min() 传入的参数是一个数组,如果不传参数,前者返回 +Infinity ,后者返回 -Infinity 。

Infinity 不能做比较,所以这道题的结果是 false 。

41. 正则表达式的记忆功能

题目:下面这段代码的输出是什么?

function captureOne(re, str) {
  var match = re.exec(str);
  return match && match[1];
}
var numRe  = /num=(\d+)/ig,
    wordRe = /word=(\w+)/i,
    a1 = captureOne(numRe,  "num=1"),
    a2 = captureOne(wordRe, "word=1"),
    a3 = captureOne(numRe,  "NUM=2"),
    a4 = captureOne(wordRe,  "WORD=2");
[a1 === a2, a3 === a4]

正则表达式的 /g 表示全局匹配,找到所有匹配,而不是在第一个匹配后停止。

第一个正则有 /g ,第一次匹配成功之后会从当前位置往后查找,在执行 a3 = captureOne(numRe, "NUM=2") 的时候,不是从字符串位置 0 开始查找的,而是从第 5 位开始,所以这里会匹配失败。

所以本题的结果是 [true, false]

42. Date

题目:下面这段代码的输出是什么?

var a = new Date("2014-03-19"),
    b = new Date(2014, 03, 19);
[a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]

这个问题考察的是 Date 对象中的月份问题,使用第二种方式初始化时间对象的时候, Month 是从 0 开始起算的,3 表示的是 4 月份,所以这里的结果是 [false, false]

43. 正则表达式

题目:下面这段代码的输出是什么?

if ('http://giftwrapped.com/picture.jpg'.match('.gif')) {
  'a gif file'
} else {
  'not a gif file'
}

String.prototype.match 接受一个正则, 如果不是, 按照 new RegExp(obj) 转化. 所以 . 并不会转义,所以开头的 ‘http://gifwrapped……’ 中 /gif 就匹配了 /.gif/,所以输出 ‘a gif file’ 。

44. 变量提升

题目:下面这段代码的输出是什么?

function foo(a) {
    var a;
    return a;
}
function bar(a) {
    var a = 'bye';
    return a;
}
[foo('hello'), bar('hello')]

a 作为参数其实已经声明了,所以在两个方法中的 var a; var a = ‘bye’ 其实就是 a; a =’bye’,所以这道题的结果是 ["hello", "bye"]

最后说一句成绩, 44 道题共计做对了 18 道,没比概率高多少,感受到了满满的套路,心态有点爆炸。

参考

https://www.cnblogs.com/lpfuture/p/5996230.html

https://www.cnblogs.com/tylerdonet/p/12742547.html

转载声明:本博客由极客挖掘机创作,采用 CC BY 3.0 CN 许可协议。可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code