基于常见混淆或误解的 JavaScript 技巧集合
内容
贡献
如果您学到了一些东西,请给这篇文章点赞💓、🦄或🔖!
这是一系列 JavaScript 技巧,涵盖了常见的困惑和误解。我根据个人经验以及我在这里和 StackOverflow 上回答的大量问题创建了这些技巧。
如果您有任何补充,我很乐意在评论中听到!
我制作了其他易于理解的教程内容!请考虑:
- 订阅我的DevTuts 邮件列表
- 订阅我的DevTuts YouTube 频道
内容
值与引用变量赋值
理解 JavaScript 如何赋值给变量是编写无 bug 的 JavaScript 代码的基础。如果不理解这一点,你很容易编写出无意中更改变量值的代码。
当 JavaScript 将五种原始类型(即 、Boolean
、null
、undefined
和String
)之一赋值Number
给变量时,JavaScript 运行时会确定该原始类型是通过引用赋值还是通过值赋值。具体如何赋值并不重要,因为原始类型无法被修改(它们是不可变的)。但是,当赋值为Array
、Function
或Object
指向内存中数组/函数/对象的引用时,就会赋值。
示例时间!在下面的代码片段中,var2
被设置为等于var1
。由于var1
是原始类型(String
),var2
被设置为等于的字符串值,此时var1
可以认为 完全不同于。因此,重新赋值对 没有任何影响。var1
var2
var1
const var1 = 'My string';
let var2 = var1;
var2 = 'My new string';
console.log(var1);
// 'My string'
console.log(var2);
// 'My new string'
让我们将其与对象分配进行比较。
const var1 = { name: 'Jim' };
const var2 = var1;
var2.name = 'John';
console.log(var1);
// { name: 'John' }
console.log(var2);
// { name: 'John' }
工作原理:
- 对象
{ name: 'Jim' }
在内存中创建 - 该变量
var1
被赋予了对所创建对象的引用 - 该变量
var2
被设置为相等var1
...这是对内存中同一对象的引用! var2
已发生变异,这实际上意味着var2 引用的对象已发生变异var1
指向与相同的对象var2
,因此我们在访问时会看到这种变化var1
如果你期望像原始赋值这样的行为,你可能会看到这会带来什么问题!如果你创建一个无意中改变对象的函数,情况会变得特别糟糕。
闭包
闭包是一种重要的 JavaScript 模式,用于赋予变量私有访问权限。在本例中,createGreeter
它返回一个匿名函数,该函数可以访问提供的greeting
“Hello”。以后所有使用时,sayHello
都可以访问这个问候语!
function createGreeter(greeting) {
return function(name) {
console.log(greeting + ', ' + name);
};
}
const sayHello = createGreeter('Hello');
sayHello('Joe');
// Hello, Joe
在更实际的场景中,您可以设想一个初始函数apiConnect(apiKey)
,它返回一些需要使用 API 密钥的方法。在这种情况下,apiKey
只需提供一次,之后无需再次提供。
function apiConnect(apiKey) {
function get(route) {
return fetch(`${route}?key=${apiKey}`);
}
function post(route, params) {
return fetch(route, {
method: 'POST',
body: JSON.stringify(params),
headers: {
Authorization: `Bearer ${apiKey}`
}
});
}
return { get, post };
}
const api = apiConnect('my-secret-key');
// No need to include the apiKey anymore
api.get('http://www.example.com/get-endpoint');
api.post('http://www.example.com/post-endpoint', { name: 'Joe' });
解构
不要被 JavaScript 参数解构吓到!这是一种从对象中干净地提取属性的常用方法。
const obj = {
name: 'Joe',
food: 'cake'
};
const { name, food } = obj;
console.log(name, food);
// 'Joe' 'cake'
如果要提取不同名称的属性,可以使用以下格式指定它们。
const obj = {
name: 'Joe',
food: 'cake'
};
const { name: myName, food: myFood } = obj;
console.log(myName, myFood);
// 'Joe' 'cake'
在下面的示例中,解构用于干净地将person
对象传递给introduce
函数。换句话说,解构可以(并且经常)直接用于提取传递给函数的参数。如果你熟悉 React,你可能之前就见过这种情况!
const person = {
name: 'Eddie',
age: 24
};
function introduce({ name, age }) {
console.log(`I'm ${name} and I'm ${age} years old!`);
}
introduce(person);
// "I'm Eddie and I'm 24 years old!"
扩展语法
JavaScript 中一个可能让人摸不着头脑但其实相对简单的概念就是展开运算符!在下面的例子中,Math.max
它不能应用于arr
数组,因为它不接受数组作为参数,而是接受单个元素作为参数。展开运算符 ...
用于从数组中提取单个元素。
const arr = [4, 6, -1, 3, 10, 4];
const max = Math.max(...arr);
console.log(max);
// 10
Rest 语法
让我们来谈谈 JavaScript 剩余语法。你可以用它将传递给函数的任意数量的参数放入数组中!
function myFunc(...args) {
console.log(args[0] + args[1]);
}
myFunc(1, 2, 3, 4);
// 3
数组方法
JavaScript 数组方法通常能提供出色而优雅的方式来执行所需的数据转换。作为 StackOverflow 的贡献者,我经常看到关于如何以某种方式操作对象数组的问题。这往往是数组方法的完美用例。
我将在这里介绍一些不同的数组方法,这些方法按有时会混淆的相似方法进行组织。此列表并非详尽无遗:我鼓励您复习并练习 MDN(我最喜欢的 JavaScript 参考)上讨论的所有方法。
映射、过滤、减少
map
人们对 JavaScript 数组方法、filter
、存在一些困惑reduce
。这些方法对于转换数组或返回聚合值非常有用。
- map:返回数组,其中每个元素都按照函数指定的方式进行转换
const arr = [1, 2, 3, 4, 5, 6];
const mapped = arr.map(el => el + 20);
console.log(mapped);
// [21, 22, 23, 24, 25, 26]
- filter:返回函数返回 true 的元素数组
const arr = [1, 2, 3, 4, 5, 6];
const filtered = arr.filter(el => el === 2 || el === 4);
console.log(filtered);
// [2, 4]
- 减少:按照函数指定的值进行累积
const arr = [1, 2, 3, 4, 5, 6];
const reduced = arr.reduce((total, current) => total + current, 0);
console.log(reduced);
// 21
注意:建议始终指定初始值,否则可能会收到错误。例如:
const arr = [];
const reduced = arr.reduce((total, current) => total + current);
console.log(reduced);
// Uncaught TypeError: Reduce of empty array with no initial value
注意:如果没有 initialValue,则 reduce 将数组的第一个元素作为 initialValue,并从第二个元素开始迭代
您还可以阅读 Sophie Alpert (@sophiebits) 的这条推文,其中建议使用reduce
查找、findIndex、indexOf
数组方法find
、findIndex
和indexOf
经常被混淆。使用方法如下。
- find:返回第一个符合指定条件的实例。不再继续查找其他匹配的实例。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const found = arr.find(el => el > 5);
console.log(found);
// 6
再次注意,虽然 5 之后的所有内容都符合条件,但只会返回第一个匹配的元素。这在通常情况下,for
当你找到匹配项时就会中断循环的情况下非常有用!
- findIndex:它的工作原理与 find 几乎相同,但它不是返回第一个匹配元素,而是返回第一个匹配元素的索引。以下示例为了清晰起见,使用了名称而不是数字。
const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
const foundIndex = arr.findIndex(el => el === 'Frank');
console.log(foundIndex);
// 1
- indexOf:工作原理与 findIndex 几乎相同,但它接受的参数不是函数,而是一个简单的值。当你的逻辑比较简单,不需要使用函数来检查是否匹配时,可以使用这个方法。
const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
const foundIndex = arr.indexOf('Frank');
console.log(foundIndex);
// 1
推送、弹出、移位、取消移位
有很多很棒的数组方法可以帮助以有针对性的方式添加或删除数组中的元素。
- push:这是一个相对简单的方法,它将一个元素添加到数组的末尾。它会就地修改数组,函数本身会返回新数组的长度。
const arr = [1, 2, 3, 4];
const pushed = arr.push(5);
console.log(arr);
// [1, 2, 3, 4, 5]
console.log(pushed);
// 5
- pop:这将从数组中删除最后一个元素。同样,它会就地修改数组。函数本身返回从数组中删除的元素。
const arr = [1, 2, 3, 4];
const popped = arr.pop();
console.log(arr);
// [1, 2, 3]
console.log(popped);
// 4
- shift:这会从数组中删除第一个元素。同样,它会就地修改数组。函数本身返回从数组中删除的元素。
const arr = [1, 2, 3, 4];
const shifted = arr.shift();
console.log(arr);
// [2, 3, 4]
console.log(shifted);
// 1
- unshift:这会将一个或多个元素添加到数组的开头。同样,它会就地修改数组。与许多其他方法不同,该函数本身会返回数组的新长度。
const arr = [1, 2, 3, 4];
const unshifted = arr.unshift(5, 6, 7);
console.log(arr);
// [5, 6, 7, 1, 2, 3, 4]
console.log(unshifted);
// 7
拼接,切片
这些方法可以修改或返回数组的子集。
- splice:通过删除或替换现有元素和/或添加新元素来更改数组的内容。此方法会就地修改数组。
The following code sample can be read as: at position 1 of the array, remove 0 elements and insert b.
const arr = ['a', 'c', 'd', 'e'];
arr.splice(1, 0, 'b');
console.log(arr);
// ['a', 'b', 'c', 'd', 'e']
- slice:返回数组的浅拷贝,从指定的起始位置到指定的结束位置。如果没有指定结束位置,则返回数组的剩余部分。重要的是,此方法不会直接修改数组,而是返回所需的子集。
const arr = ['a', 'b', 'c', 'd', 'e'];
const sliced = arr.slice(2, 4);
console.log(sliced);
// ['c', 'd']
console.log(arr);
// ['a', 'b', 'c', 'd', 'e']
种类
- sort:根据提供的函数对数组进行排序,该函数接受第一个元素和第二个元素作为参数。它会就地修改数组。如果函数返回负数或 0,则顺序保持不变。如果返回正数,则元素顺序会被切换。
const arr = [1, 7, 3, -1, 5, 7, 2];
const sorter = (firstEl, secondEl) => firstEl - secondEl;
arr.sort(sorter);
console.log(arr);
// [-1, 1, 2, 3, 5, 7, 7]
呼,你都明白了吗?我也没明白。事实上,写这篇文章的时候我不得不参考很多 MDN 文档——不过没关系!只要知道有哪些方法,就能帮你搞定 95% 的问题。
生成器
别害怕*
。生成器函数指定value
下次next()
调用时会产生什么。它可以有有限数量的 yield,之后next()
返回一个undefined
值,也可以使用循环产生无限数量的值。
function* greeter() {
yield 'Hi';
yield 'How are you?';
yield 'Bye';
}
const greet = greeter();
console.log(greet.next().value);
// 'Hi'
console.log(greet.next().value);
// 'How are you?'
console.log(greet.next().value);
// 'Bye'
console.log(greet.next().value);
// undefined
并使用生成器来获取无限值:
function* idCreator() {
let i = 0;
while (true) yield i++;
}
const ids = idCreator();
console.log(ids.next().value);
// 0
console.log(ids.next().value);
// 1
console.log(ids.next().value);
// 2
// etc...
身份运算符(===)与相等运算符(==)
一定要了解JavaScript 中识别运算符 ( ===
) 和相等运算符 ( )之间的区别!运算符会在比较值之前进行类型转换,而运算符在比较之前不会进行任何类型转换。==
==
===
console.log(0 == '0');
// true
console.log(0 === '0');
// false
对象比较
我发现 JavaScript 新手常犯的一个错误是直接比较对象。变量指向的是内存中对象的引用,而不是对象本身!一种比较对象的方法是将对象转换为 JSON 字符串。但这有一个缺点:对象属性的顺序无法保证!一种更安全的比较对象方法是使用专门进行深度对象比较的库(例如 lodash 的 isEqual)。
以下对象看起来相等,但实际上它们指向不同的引用。
const joe1 = { name: 'Joe' };
const joe2 = { name: 'Joe' };
console.log(joe1 === joe2);
// false
相反,以下计算结果为真,因为一个对象被设置为等于另一个对象,因此指向相同的引用(内存中只有一个对象)。
const joe1 = { name: 'Joe' };
const joe2 = joe1;
console.log(joe1 === joe2);
// true
请务必查看上面的“值与引用”部分,以充分理解将变量设置为等于指向内存中对象引用的另一个变量的后果!
回调函数
太多人被 JavaScript 回调函数吓到了!它们很简单,比如这个例子。函数console.log
作为回调传递给myFunc
。它在完成时执行setTimeout
。就是这样!
function myFunc(text, callback) {
setTimeout(function() {
callback(text);
}, 2000);
}
myFunc('Hello world!', console.log);
// 'Hello world!'
承诺
一旦你理解了 JavaScript 回调,你很快就会发现自己陷入了嵌套的“回调地狱”。这时,Promises 就能派上用场了!将你的异步逻辑封装在一个回调函数中,Promise
用于处理resolve
成功或reject
失败。分别用于then
处理成功和catch
失败。
const myPromise = new Promise(function(res, rej) {
setTimeout(function() {
if (Math.random() < 0.9) {
return res('Hooray!');
}
return rej('Oh no!');
}, 1000);
});
myPromise
.then(function(data) {
console.log('Success: ' + data);
})
.catch(function(err) {
console.log('Error: ' + err);
});
// If Math.random() returns less than 0.9 the following is logged:
// "Success: Hooray!"
// If Math.random() returns 0.9 or greater the following is logged:
// "Error: Oh no!"
避免承诺链的嵌套反模式!
.then
方法可以链式调用。我看到很多新手最终陷入了 Promise 内部的某种回调地狱,而这完全没有必要。
//The wrong way
getSomedata.then(data => {
getSomeMoreData(data).then(newData => {
getSomeRelatedData(newData => {
console.log(newData);
});
});
});
//The right way
getSomeData
.then(data => {
return getSomeMoreData(data);
})
.then(data => {
return getSomeRelatedData(data);
})
.then(data => {
console.log(data);
});
您可以看到第二种形式更容易阅读,并且使用 ES6 隐式返回我们甚至可以进一步简化它:
getSomeData
.then(data => getSomeMoreData(data))
.then(data => getSomeRelatedData(data))
.then(data => console.log(data));
因为传递给 .then 的函数将被调用,并接收 Promise 中 resolve 方法的结果,所以我们可以完全省略创建匿名函数的步骤。这相当于上面的代码:
getSomeData
.then(getSomeMoreData)
.then(getSomeRelatedData)
.then(console.log);
异步等待
一旦你掌握了 JavaScript 的 Promise,你可能会喜欢async await
它,它只是 Promise 之上的“语法糖”。在下面的例子中,我们创建了一个async
函数,并在其中使用了await
Promise greeter
。
const greeter = new Promise((res, rej) => {
setTimeout(() => res('Hello world!'), 2000);
});
async function myFunc() {
const greeting = await greeter;
console.log(greeting);
}
myFunc();
// 'Hello world!'
异步函数返回一个承诺
这里要注意的一件重要的事情是,函数的结果async
是一个承诺。
const greeter = new Promise((res, rej) => {
setTimeout(() => res('Hello world!'), 2000);
});
async function myFunc() {
return await greeter;
}
console.log(myFunc()); // => Promise {}
myFunc().then(console.log); // => Hello world!
DOM 操作
创建您自己的查询选择器简写
在浏览器中使用 JS 时,您可以执行以下操作,而不必多次编写document.querySelector()
/ :document.querySelectorAll()
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
// Usage
const demo = $('#demo');
// Select all the `a` tags
[...$$("a[href *='#']")].forEach(console.log);
面试问题
遍历链表
以下是一个经典软件开发面试题的 JavaScript 解决方案:遍历链表。你可以使用 while 循环递归地遍历链表,直到没有值为止!
const linkedList = {
val: 5,
next: {
val: 3,
next: {
val: 10,
next: null
}
}
};
const arr = [];
let head = linkedList;
while (head !== null) {
arr.push(head.val);
head = head.next;
}
console.log(arr);
// [5, 3, 10]
各种各样的
增加和减少
i++
有没有想过和之间有什么区别++i
?你知道两者都是选项吗?i++
先返回i
,然后增加值,而先++i
增加i
,然后返回值。
let i = 0;
console.log(i++);
// 0
let i = 0;
console.log(++i);
// 1
贡献
欢迎通过相关的Github 仓库贡献代码!我只需要您提交一个问题,然后我们先讨论您提出的修改建议。
文章来源:https://dev.to/nas5w/a-collection-of-javascript-tips-based-on-common-areas-of-confusion-or-misunderstanding-42j5