( 译、持续更新 ) JavaScript 上分小技巧(三)

简介: 最近家里杂事较多,自学时间实在少的可怜,所以都在空闲时间看看老外写的内容,学习之外顺便翻译分享~等学习的时间充足些再写写自己的一些学习内容和知识点分析(最近有在接触的:复习(C#,SQL)、(学习)TypeScript,(基础操作)MongoDB。

最近家里杂事较多,自学时间实在少的可怜,所以都在空闲时间看看老外写的内容,学习之外顺便翻译分享~等学习的时间充足些再写写自己的一些学习内容和知识点分析(最近有在接触的:复习(C#,SQL)、(学习)TypeScript,(基础操作)MongoDB。TypeScript之后入手AngularJs 2.0)

后续如有内容,本篇将会照常更新并排满15个知识点,以下是其他几篇译文的地址:

第一篇地址:( 译、持续更新 ) JavaScript 上分小技巧(一)

第二篇地址:( 译、持续更新 ) JavaScript 上分小技巧(二)

第四篇地址:( 译、持续更新 ) JavaScript 上分小技巧(四)

#45 - 计算数组的最大值和最小值
有两种方法从参数列表中找到最大值和最小值,但他们都不支持数组本身操作。

Math.max(1, 2, 3, 4); // 4
Math.min(1, 2, 3, 4); // 1

apply()允许你使用内置的函数来找出数组中的最大最小值。

var numbers = [1, 2, 3, 4];
Math.max.apply(null, numbers) // 4
Math.min.apply(null, numbers) // 1

apply()的第一个参数是上下文,在这种情况下无关紧要,但是你可以阅读以了解更多
另外一种方式更加简单,使用ES6新特性spread

var numbers = [1, 2, 3, 4];
Math.max(...numbers) // 4
Math.min(...numbers) // 1

          ### 2016-02-15 更新 ###          

#44 - 了解传递机制
javascript的按值传递的(基本类型),就技术上而言。它既不是值传递的,也不是引用传递的。通过以下的两个真实例子,让我们来理解下javascript中的传递机制吧:
案例1:

var me = {                  // #1
    'partOf' : 'A Team'
}; 
function myTeam(me) {       // #2
    me = {                  // #3
        'belongsTo' : 'A Group'
    }; 
}   
myTeam(me);     
console.log(me);            // #4  : {'partOf' : 'A Team'}

在上面的代码中,当myTeam被调用,javascript将对象me的引用作为value传入函数,因为在函数内部作为新对象进行操作,所以是调用传入对象本身来创建两独立的引用(虽然这里名字me是一样的,这是误导,并且给我们一个单一引用的印象)因此,引用的变量本身是独立的。
当我们在在#3的地方重新指定一个对象。我们在对myTeam这个函数所传引用值进行完全修改,并且它不会对函数范围外的原始值有任何影响。虽然这个对象从函数传入做操作,但是函数范围外还是保留原来值,因此我们得到#4的输出。
案例2:

var me = {                  // #1
    'partOf' : 'A Team'
}; 
function myGroup(me) {      // #2
    me.partOf = 'A Group';  // #3
} 
myGroup(me);
console.log(me);            // #4  : {'partOf' : 'A Group'}

在调用myGroup的场景下,我们传入对象me,但是和案例1不同的是,我们并未对这个对象me指定到新的对象中,所以函数中的对象还是范围外的原始对象的引用,并且我们在此对这个对象做出修改,函数范围外的对象也做了相对应的修改。因此,在#4的地方得到该值。
那么在后面这种情况下,能不能证明javascript是引用传递呢?不能。记着,javascript如果当传入的是对象的时候是引用传递。开发中出现的混乱往往是因为我们不完全理解引用传递是什么。这是确切的原因,有些人更喜欢称之为“共享传递”。

最初发布的作者的实例

准确的说,JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值
然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。
参考url
          ### 2016-02-14 更新 ###          

#43 - 在函数参数中使用解构(ES6)

相信很多人都知道ES6中的解构是什么了,但是他可以在函数中使用知道吗?

var sayHello = function({ name, surname }) {
  console.log(`Hello ${name} ${surname}! How are you?`);
};
sayHello({
  name: 'John',
  surname: 'Smith'
});

这是伟大的功能,接收一个对象作为配置。

注意:在node和几乎所有浏览器中,解构暂不可用。但是如果你想马上尝试一下在Node.js使用,那就使用 --harmony-destructuring吧。

这里附带贴上本兽通过babel转ES5后的代码:

var sayHello = function sayHello(_ref) {
  var name = _ref.name;
  var surname = _ref.surname;
  console.log('Hello ' + name + ' ' + surname + '! How are you?');
};
sayHello({
  name: 'John',
  surname: 'Smith'
});

其实就这么一回事,在ES6中方便了些而已。

#42 - preventing unapply attacks

没看懂,暂不做评价。有兴趣的朋友可查看原文

          ### 2016-02-13 更新 ###          

#41 - 数组的平均值和中间值
以下的案例都以下面的数组为基准:

let values = [2, 56, 3, 41, 0, 4, 100, 23];

为了获取平均值,我们必须算出总和然后再除以它的数量。步骤为:
· 获取数组长度
· 获得总和
· 获取平均值(中和/数组长度)

let values = [2, 56, 3, 41, 0, 4, 100, 23];
let sum = values.reduce((previous, current) => current += previous);
let avg = sum / values.length;
// avg = 28

或者:

let values = [2, 56, 3, 41, 0, 4, 100, 23];
let count = values.length;
values = values.reduce((previous, current) => current += previous);
values /= count;
// avg = 28

现在,获取中间值的步骤:
· 排序数组
· 获取中间值

let values = [2, 56, 3, 41, 0, 4, 100, 23];
values.sort((a, b) => a - b);
let middle = Math.floor(values.length / 2);
let median = values[middle];
// median = 23

或者用一个右位移运算符:

let values = [2, 56, 3, 41, 0, 4, 100, 23];
values.sort((a, b) => a - b);
let median = values[values.length >> 1];
// median = 23

          ### 2016-02-11 更新 ###          

#40 - 使用JSON.stringify
当一个对象有"prop1", "prop2", "prop3"属性时,我们可以用JSON.stringify通过额外的参数来选择性的以字符串方式写对象的属性

var obj = {
    'prop1': 'value1',
    'prop2': 'value2',
    'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
var str = JSON.stringify(obj, selectedProperties);
// str
// {"prop1":"value1","prop2":"value2"}

"str"将会只包含选中的属性。
除了传入数组,函数也可以:

function selectedProperties(key, val) {
    // 第一个val是整个对象,key是空字符串
    if (!key) {
        return val;
    }
    if (key === 'prop1' || key === 'prop2') {
        return val;
    }
    return;
}

最后一个可选参数是修改将对象写成字符串的方式。

var str = JSON.stringify(obj, selectedProperties, '\t\t');

/* str分行输出
{
        "prop1": "value1",
        "prop2": "value2"
}
*/

#39 - javascript的高级属性
在JavaScript中,我们可以配置对象的属性,如配置属性是私有的还是只读。从ECMAScript 5.1开始就有这个功能,所有新的浏览器都已支持。
你可能需要用到Object原型的defineProperty方完成做这些功能,如下:

var a = {};
Object.defineProperty(a, 'readonly', {
  value: 15,
  writable: false
});
a.readonly = 20;
console.log(a.readonly); // 15

其语法如下:

Object.defineProperty(dest, propName, options)

或者同时设置多个属性:

Object.defineProperties(dest, {
  propA: optionsA,
  propB: optionsB, //...
})

选项包括以下属性:
value:如果属性不是getter(见下文),value是个强制性的属性。 {a: 12} === Object.defineProperty(obj, 'a', {value: 12}) 
writable:设置属性是不是可读。注意,如果属性是一个嵌套的对象,其属性仍然是可编辑的。
enumerable:设置属性是不是隐藏的。这意味着for...in和stringify执行后的结果都不会包含这个属性。注意:这并不意味着该属性是私有的!它仍然可以从外部访问,它只是意味着它不会被打印。
configurable:设置属性是不是可修改的。如防止删除或重新定义。再次说明,如果属性是一个嵌套的对象,它的属性仍然是可配置的。
因此,为了创建一个属性私有的常量,你可以如下定义:

Object.defineProperty(obj, 'myPrivateProp', {value: val, enumerable: false, writable: false, configurable: false});

除了属性配置,defineproperty允许我们定义动态特性,这是因为第二个参数是字符串。例如,我们想根据一些外部配置创建属性:
但这并不是全部!高级属性允许我们创建getter和setter,就像其他OOP语言!在这种情况下,writable、enumerable、configurable是不能用的,而是以下代码:

function Foobar () {
  var _foo; // 私有属性
  Object.defineProperty(obj, 'foo', {
    get: function () { return _foo; }
    set: function (value) { _foo = value }
  });
}
var foobar = new Foobar();
foobar.foo; // 15
foobar.foo = 20; // _foo = 20

除了封装和先进的访问器的优势明显,你会发现,我们没有“call” getter,而只是在不需要括号的情况下“get”属性!这是可怕的!例如,让我们想象一下,我们有一个深层嵌套的属性,如这样的对象:

var obj = {a: {b: {c: [{d: 10}, {d: 20}] } } };

现在,为了替代a.b.c[0].d(将会遇到undefined,并且抛出一个错误),我们可以创建一个别名:

Object.defineProperty(obj, 'firstD', {
  get: function () { return a && a.b && a.b.c && a.b.c[0] && a.b.c[0].d }
})
console.log(obj.firstD) // 10

注意:
如果你定义了yigegetter而没有定义setter,并且还是set一个value,那么将会得到一个错误。当使用辅助功能如$.extend或者_.merga这类辅助功能的时候需要小心,这是特别重要的。
链接:
defineProperty
Defining properties in JavaScript
          ### 2016-02-10 更新 ###          

#38 - javascript中多维数组扁平化

这里有三个已知的方法将多维数组合并成一个数组。
给定的数组:

var myArray = [[1, 2],[3, 4, 5], [6, 7, 8, 9]];

我们需要这个结果:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案一:使用concat()apply()

var myNewArray = [].concat.apply([], myArray);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案二:使用reduce()

var myNewArray = myArray.reduce(function(prev, curr) {
  return prev.concat(curr);
});
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案三:

var myNewArray3 = [];
for (var i = 0; i < myArray.length; ++i) {
  for (var j = 0; j < myArray[i].length; ++j)
    myNewArray3.push(myArray[i][j]);
}
console.log(myNewArray3);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

这里看这3种算法的作用。
无限嵌套数组用Underscore的flatten()
如果你对性能很好奇,这里有一个测试,看看它是如何工作的。

          ### 2016-02-08 更新 ###          

#37 - 删除数组重复项
原始方法
如果数组包含着原始值,我们可以用filterindexOf方法去除重复。

var deduped = [ 1, 1, 'a', 'a' ].filter(function (el, i, arr) {
    return arr.indexOf(el) === i;
});
console.log(deduped); // [ 1, 'a' ]

ES2015
我们可以使用arrow函数写的更紧凑些:

var deduped = [ 1, 1, 'a', 'a' ].filter( (el, i, arr) => arr.indexOf(el) === i);
console.log(deduped); // [ 1, 'a' ]

我们更可以用Setsfrom方法用更简洁的代码实现:

var deduped = Array.from( new Set([ 1, 1, 'a', 'a' ]) );
console.log(deduped); // [ 1, 'a' ]

对象
当元素是对象的时候,我们便不能使用相同的方法。因为对象是引用存储并且数据存于value中。

1 === 1 // true
'a' === 'a' // true
{ a: 1 } === { a: 1 } // false

因此我们需要改变我们的方法并使用hash table(哈希表)。

function dedup(arr) {
    var hashTable = {};

    return arr.filter(function (el) {
        var key = JSON.stringify(el);
        var match = Boolean(hashTable[key]);

        return (match ? false : hashTable[key] = true);
    });
}
var deduped = dedup([
    { a: 1 },
    { a: 1 },
    [ 1, 2 ],
    [ 1, 2 ]
]);
console.log(deduped); // [ {a: 1}, [1, 2] ]

因为JavaScript中的哈希表是一个简单的Object,keys都是字符串类型。这意味着,我们通常都不能区分相同值的字符串和数字,即1和"1"。
然而,因为我们使用了JSON.stringify,keys都是字符串类型,将以转义字符串存储并且在hash表中给定一个唯一的key。

var hashTable = {};
hashTable[JSON.stringify(1)] = true;
hashTable[JSON.stringify('1')] = true;
console.log(hashTable); // { '1': true, '\'1\'': true }

这意味着值相同但类型不同的元素将会被作为相同的值而删除。

var deduped = dedup([
    { a: 1 },
    { a: 1 },
    [ 1, 2 ],
    [ 1, 2 ],
    1,
    1,
    '1',
    '1'
]);
console.log(deduped); // [ {a: 1}, [1, 2], 1, '1' ]

Stack overflow
remove duplicates from array
          ### 2016-02-07 更新 ###          

# 36 - 观察DOM的变化
如果你想监听DOM变化并且在变化时做一些处理,MutationObserver是一个好的解决方案。在下面的例子中有一些通过定时器动态加载的元素,当创建完第一个"target"元素后,开始创建"subTarget"元素。在扩展的代码中,首先执行rootObserver,直到targetElement 出现后才开始执行elementObserver 。这有利于扩展复杂站点的内容动态加载。

const observeConfig = {
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true
};

function initExtension(rootElement, targetSelector, subTargetSelector) {
    var rootObserver = new MutationObserver(function(mutations) {
        console.log("Inside root observer");
        targetElement = rootElement.querySelector(targetSelector);
        if (targetElement) {
            rootObserver.disconnect();
            var elementObserver = new MutationObserver(function(mutations) {
                console.log("Inside element observer")
                subTargetElement = targetElement.querySelector(subTargetSelector);
                if (subTargetElement) {
                    elementObserver.disconnect();
                    console.log("subTargetElement found!")
                }
            })
            elementObserver.observe(targetElement, observeConfig);
        }
    })
    rootObserver.observe(rootElement, observeConfig);
}

(function() {
    initExtension(document.body, "div.target", "div.subtarget")

    setTimeout(function() {
        del = document.createElement("div");
        del.innerHTML = "<div class='target'>target</div>"
        document.body.appendChild(del)
    }, 3000);

    setTimeout(function() {
        var el = document.body.querySelector('div.target')
        if (el) {
            del = document.createElement("div");
            del.innerHTML = "<div class='subtarget'>subtarget</div>"
            el.appendChild(del)
        }
    }, 5000);
})()

          ### 2016-02-06 更新 ###          

#35 - 简洁的代码
对于我们懒惰的程序员来说,有时候觉得打字都是浪费时间。所以,我们可以使用一些技巧来帮助我们,使我们的代码更清洁和更简单。
类似的使用:

x += 23; // x = x + 23;
y -= 15; // y = y - 15;
z *= 10; // z = z * 10;
k /= 7; // k = k / 7;
p %= 3; // p = p % 3;
d **= 2; // d = d ** 2;
m >>= 2; // m = m >> 2;
n <<= 2; // n = n << 2;
n ++; // n = n + 1;
n --; // n = n - 1;

if-else(使用三元运算符)

这是我们平常的规范写法:

var newValue;
if(value > 10) 
  newValue = 5;
else
  newValue = 2;

我们可以使用三元运算符:

var newValue = (value > 10) ? 5 : 2;

null,undefined,空值检查

if (variable1 !== null || variable1 !== undefined || variable1 !== '') {
    var variable2 = variable1;
}

简短的写法:

var variable2 = variable1 || '';

注:如果variable1是一个数,将会先检查它是否为0。

数组/对象的表示方法
数组:

var a = new Array();
a[0] = "myString1";
a[1] = "myString2";

我们用下面的代码代替上面的代码:

var a = ["myString1", "myString2"];

对象:

var skillSet = new Array();
skillSet['Document language'] = 'HTML5';
skillSet['Styling language'] = 'CSS3';

我们使用下面的代码代替上面的代码:

var skillSet = {
    'Document language' : 'HTML5', 
    'Styling language' : 'CSS3'
};

#34 - 执行异步调用的循环
让我们试着写一个异步函数,它每秒打印一次循环的索引值。

for (var i=0; i<5; i++) {
    setTimeout(function(){
        console.log(i); 
    }, 1000);
}

以上程序将会输出:

_> 5
_> 5
_> 5
_> 5
_> 5

所以,它根本没有正确的执行出我们所期望的效果。
原因:
因为timeout的异步,而每次的timeout指向的是原始的i,不是副本中的。所以循环直到i=5,然后timeout运行,并且打印当前的i(5)。
好吧,这个问题似乎很容易。一个马上就可以解决的方案是将当前的i值当作一个临时变量缓存在循环中。

for (var i=0; i<5; i++) {
    var temp = i;
    setTimeout(function(){
        console.log(i); 
    }, 1000);
}

但是上面的程序输出的还是:

_> 5
_> 5
_> 5
_> 5
_> 5

所以,那也行不通,因为代码块不会创建一个作用域并且变量的初始化被提升到作用域的前面。事实上,和之前的代码是一样的:
解决方案:
有很多种不同的方案来复制i。最常见的是创建一个闭包,通过声明一个函数,并将i作为参数传入。在这里,我们用自执行函数:

for (var i=0; i<5; i++) {
    (function(num){
        setTimeout(function(){
            console.log(num); 
        }, 1000); 
    })(i);  
}

在JavaScript中,参数是按值传递给函数。像数字、日期和字符串的原始类型基本上是复制的。如果你在函数内改变了它们,不影响它在外部范围的值。对象是特殊的:如果内部函数改变了它的一个属性,则在所有作用域中的对应值跟着变化。

#33 - 使用一行代码简单的创建一个[1,...,N]的范围数组
下面这行代码能够创建一个[1,...,N]的范围数组

Array.apply(null,{length:N}).map(Fn,Context);

让我们将这行代码拆分成几部分。我们都知道javascript的apply()函数的作用。在apply()里,第一个参数是根据第二个参数而来的上下文对象,它是被我们称为apply()函数的参数列表。

function add(a, b){
    return (a+b);
}
add.apply(null,[5, 6]);

将会返回5+6的和。
javascript中的数组的map()函数有2个参数,第一个是回调函数,第二个是回调函数的上下文对象。回调函数有3个参数:value(值)、index(索引)、我们处理的整个阵列。所以普通语法是:

[1, 2, 3].map(function(value, index, arr){
    //Code
}, this);

下面的这行代码创建一个给定长度的数组:

Array.apply(null,{length:N});

把所有部分放到一起就是解决方案:

Array.apply(null,{length:N}).map(Fn,Context);

如果你想创建一个[1,...,N]的范围数组:

Array.apply(null, {length: N}).map(function(value, index){
  return index+1;  
});

#32 - Map() 有秩序的给对象填加属性
object是Object类型中的一个成员,它是个包含原始值、对象或者函数的一个无序的属性集合。储存在对象属性中的函数被成为方法。ECMAScript
看以下代码:

var myObject = {
    z: 1,
    '@': 2,
    b: 3,
    1: 4,
    5: 5
 };
console.log(myObject) // Object {1: 4, 5: 5, z: 1, @: 2, b: 3}
for (item in myObject) {...
// 1
// 5
// z
// @
// b

每个浏览器针对对象技术都有着各自的规则,所以秩序是不确定的。
怎么解决这个问题呢?
Map
使用ES6中的一个新特性---Map。一个Map对象按插入的顺序来迭代它的元素---for...of循环为每个循环项返回一个[key,value]形式的数组。

var myObject = new Map();
myObject.set('z', 1);
myObject.set('@', 2);
myObject.set('b', 3);
for (var [key, value] of myObject) {
  console.log(key, value);
...
// z 1
// @ 2
// b 3

旧浏览器的hack
Mozilla 建议:
因此,如果你想在一个交叉的浏览器环境中模拟一个有序的关联数组,你不得不使用2个单独的数组(一个键的数组和一个值的数组),或构建一个单属性对象的数组等。

// 使用两个数组
var objectKeys = [z, @, b, 1, 5];
for (item in myObject) {
    myObject[item]
...
// 创建一个单属性对象的数组
var myData = [{z: 1}, {'@': 2}, {b: 3}, {1: 4}, {5: 5}];

#31 - 避免将"arguments"进行修改或者传递到其他函数
背景:
在javascript的函数中,名为arguments的变量让你能够操作传入函数的所有参数。arguments是个类似数组的object,arguments能够使用数组符号来做操作,并且有length属性,但是它不能够使用一些数组内置的方法,比如filter、map和forEach。正是因为如此,将参数转换为数组来使用是一种相当普遍的做法:

var args = Array.prototype.slice.call(arguments);

从Array的prototypr中调用slice方法,传入arguments;slice方法返回一个新的浅度复制arguments的数组对象。这是一个常用的速记法:

var args = [].slice.call(arguments);

在这个案例中,我们更简单的用一个字面量的空数组来代替从Array的prototype调用slice方法。
优化:
不幸的是,在基于Chrome和Node的JavaScript V8引擎上,在函数内将arguments传入任何其他函数都会导致性能变慢。看这篇文章:优化杀手。将arguments传递给任何其他函数称为参数泄露。
作为替代,如果想要可以使用的参数数组,你需要做这些:

var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
  args[i] = arguments[i];
}

这是更详细的,但在生产代码中,以这些来进行性能优化是值得的。

          ### 2016-02-04 更新 ###          

相关文章
|
8月前
|
存储 移动开发 前端开发
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新6
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
265 0
|
4月前
|
XML 存储 JavaScript
JavaScript详解DOM和BOM(持续更新)
JavaScript详解DOM和BOM(持续更新)
|
8月前
|
存储 资源调度 前端开发
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新8
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
322 0
|
8月前
|
存储 Web App开发 移动开发
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新 2
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
505 0
|
5月前
|
算法 JavaScript 前端开发
JavaScript实现各算法模板(持续更新中……)
JavaScript实现各算法模板(持续更新中……)
30 0
|
8月前
|
存储 JSON 缓存
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新7
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
252 0
|
8月前
|
Web App开发 编解码 移动开发
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新 5
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
314 0
|
8月前
|
Web App开发 编解码 弹性计算
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新 4
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
247 0
|
8月前
|
Web App开发 移动开发 前端开发
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新 3
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
148 0
|
8月前
|
存储 移动开发 缓存
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新 1
2023年最新前端面试题汇总大全(含答案超详细,HTML,JS,CSS汇总篇)-- 持续更新
288 0