做者:Mark Ajavascript
译者:前端小智前端
来源:devjava
点赞再看,养成习惯git
本文
GitHub
github.com/qq449245884… 上已经收录,更多往期高赞文章的分类,也整理了不少个人文档,和教程资料。欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。github
因为篇幅过长,我将此系列分红上中下三篇,上、中篇:面试
看完这几道 JavaScript 面试题,让你与考官对答如流(中)编程
看完这几道 JavaScript 面试题,让你与考官对答如流(上)json
async/await
及其如何工做?%
模运算符的状况下检查一个数字是不是偶数?Iterator
是什么,有什么做用?Generator
函数是什么,有什么做用?async/await
及其如何工做?async/await
是 JS 中编写异步或非阻塞代码的新方法。它创建在Promises之上,让异步代码的可读性和简洁度都更高。api
async/await
是 JS 中编写异步或非阻塞代码的新方法。 它创建在Promises
之上,相对于 Promise 和回调,它的可读性和简洁度都更高。 可是,在使用此功能以前,咱们必须先学习Promises
的基础知识,由于正如我以前所说,它是基于Promise
构建的,这意味着幕后使用仍然是Promise。数组
使用 Promise
function callApi() {
return fetch("url/to/api/endpoint")
.then(resp => resp.json())
.then(data => {
//do something with "data"
}).catch(err => {
//do something with "err"
});
}
复制代码
使用async/await
在async/await
,咱们使用 tru/catch 语法来捕获异常。
async function callApi() {
try {
const resp = await fetch("url/to/api/endpoint");
const data = await resp.json();
//do something with "data"
} catch (e) {
//do something with "err"
}
}
复制代码
注意:使用 async
关键声明函数会隐式返回一个Promise。
const giveMeOne = async () => 1;
giveMeOne()
.then((num) => {
console.log(num); // logs 1
});
复制代码
注意:await
关键字只能在async function
中使用。在任何非async function的函数中使用await
关键字都会抛出错误。await
关键字在执行下一行代码以前等待右侧表达式(多是一个Promise)返回。
const giveMeOne = async () => 1;
function getOne() {
try {
const num = await giveMeOne();
console.log(num);
} catch (e) {
console.log(e);
}
}
// Uncaught SyntaxError: await is only valid in async function
async function getTwo() {
try {
const num1 = await giveMeOne(); // 这行会等待右侧表达式执行完成
const num2 = await giveMeOne();
return num1 + num2;
} catch (e) {
console.log(e);
}
}
await getTwo(); // 2
复制代码
展开运算符(spread)是三个点(...
),能够将一个数组转为用逗号分隔的参数序列。说的通俗易懂点,有点像化骨绵掌,把一个大元素给打散成一个个单独的小元素。
剩余运算符也是用三个点(...
)表示,它的样子看起来和展开操做符同样,可是它是用于解构数组和对象。在某种程度上,剩余元素和展开元素相反,展开元素会“展开”数组变成多个元素,剩余元素会收集多个元素和“压缩”成一个单一的元素。
function add(a, b) {
return a + b;
};
const nums = [5, 6];
const sum = add(...nums);
console.log(sum);
复制代码
在本例中,咱们在调用add
函数时使用了展开操做符,对nums
数组进行展开。因此参数a
的值是5
,b
的值是6
,因此sum
是11
。
function add(...rest) {
return rest.reduce((total,current) => total + current);
};
console.log(add(1, 2)); // 3
console.log(add(1, 2, 3, 4, 5)); // 15
复制代码
在本例中,咱们有一个add
函数,它接受任意数量的参数,并将它们所有相加,而后返回总数。
const [first, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2,3,4,5]
复制代码
这里,咱们使用剩余操做符提取全部剩余的数组值,并将它们放入除第一项以外的其余数组中。
默认参数是在 JS 中定义默认变量的一种新方法,它在ES6或ECMAScript 2015版本中可用。
//ES5 Version
function add(a,b){
a = a || 0;
b = b || 0;
return a + b;
}
//ES6 Version
function add(a = 0, b = 0){
return a + b;
}
add(1); // returns 1
复制代码
咱们还能够在默认参数中使用解构。
function getFirst([first, ...rest] = [0, 1]) {
return first;
}
getFirst(); // 0
getFirst([10,20,30]); // 10
function getArr({ nums } = { nums: [1, 2, 3, 4] }){
return nums;
}
getArr(); // [1, 2, 3, 4]
getArr({nums:[5,4,3,2,1]}); // [5,4,3,2,1]
复制代码
咱们还可使用先定义的参数再定义它们以后的参数。
function doSomethingWithValue(value = "Hello World", callback = () => { console.log(value) }) {
callback();
}
doSomethingWithValue(); //"Hello World"
复制代码
咱们如今复习一下JS的数据类型,JS数据类型被分为两大类,基本类型和引用类型。
基本类型:Undefined
,Null
,Boolean
,Number
,String
,Symbol
,BigInt
引用类型:Object
,Array
,Date
,RegExp
等,说白了就是对象。
其中引用类型有方法和属性,可是基本类型是没有的,但咱们常常会看到下面的代码:
let name = "marko";
console.log(typeof name); // "string"
console.log(name.toUpperCase()); // "MARKO"
复制代码
name
类型是 string
,属于基本类型,因此它没有属性和方法,可是在这个例子中,咱们调用了一个toUpperCase()
方法,它不会抛出错误,还返回了对象的变量值。
缘由是基本类型的值被临时转换或强制转换为对象,所以name
变量的行为相似于对象。 除null
和undefined
以外的每一个基本类型都有本身包装对象。也就是:String
,Number
,Boolean
,Symbol
和BigInt
。 在这种状况下,name.toUpperCase()
在幕后看起来以下:
console.log(new String(name).toUpperCase()); // "MARKO"
复制代码
在完成访问属性或调用方法以后,新建立的对象将当即被丢弃。
隐式强制转换是一种将值转换为另外一种类型的方法,这个过程是自动完成的,无需咱们手动操做。
假设咱们下面有一个例子。
console.log(1 + '6'); // 16
console.log(false + true); // 1
console.log(6 * '2'); // 12
复制代码
第一个console.log
语句结果为16
。在其余语言中,这会抛出编译时错误,但在 JS 中,1
被转换成字符串,而后与+运
算符链接。咱们没有作任何事情,它是由 JS 自动完成。
第二个console.log
语句结果为1
,JS 将false
转换为boolean
值为 0
,,true
为1
,所以结果为1
。
第三个console.log
语句结果12
,它将'2'
转换为一个数字,而后乘以6 * 2
,结果是12。
而显式强制是将值转换为另外一种类型的方法,咱们须要手动转换。
console.log(1 + parseInt('6'));
复制代码
在本例中,咱们使用parseInt
函数将'6'
转换为number
,而后使用+
运算符将1
和6
相加。
NaN
表示**“非数字”**是 JS 中的一个值,该值是将数字转换或执行为非数字值的运算结果,所以结果为NaN
。
let a;
console.log(parseInt('abc')); // NaN
console.log(parseInt(null)); // NaN
console.log(parseInt(undefined)); // NaN
console.log(parseInt(++a)); // NaN
console.log(parseInt({} * 10)); // NaN
console.log(parseInt('abc' - 2)); // NaN
console.log(parseInt(0 / 0)); // NaN
console.log(parseInt('10a' * 10)); // NaN
复制代码
JS 有一个内置的isNaN
方法,用于测试值是否为isNaN值,可是这个函数有一个奇怪的行为。
console.log(isNaN()); // true
console.log(isNaN(undefined)); // true
console.log(isNaN({})); // true
console.log(isNaN(String('a'))); // true
console.log(isNaN(() => { })); // true
复制代码
全部这些console.log
语句都返回true
,即便咱们传递的值不是NaN
。
在ES6
中,建议使用Number.isNaN
方法,由于它确实会检查该值(若是确实是NaN
),或者咱们可使本身的辅助函数检查此问题,由于在 JS 中,NaN是惟一的值,它不等于本身。
function checkIfNaN(value) {
return value !== value;
}
复制代码
咱们可使用Array.isArray
方法来检查值是否为数组。 当传递给它的参数是数组时,它返回true
,不然返回false
。
console.log(Array.isArray(5)); // false
console.log(Array.isArray("")); // false
console.log(Array.isArray()); // false
console.log(Array.isArray(null)); // false
console.log(Array.isArray({ length: 5 })); // false
console.log(Array.isArray([])); // true
复制代码
若是环境不支持此方法,则可使用polyfill
实现。
function isArray(value){
return Object.prototype.toString.call(value) === "[object Array]"
}
复制代码
固然还可使用传统的方法:
let a = []
if (a instanceof Array) {
console.log('是数组')
} else {
console.log('非数组')
}
复制代码
%
模运算符的状况下检查一个数字是不是偶数?咱们能够对这个问题使用按位&
运算符,&
对其操做数进行运算,并将其视为二进制值,而后执行与运算。
function isEven(num) {
if (num & 1) {
return false
} else {
return true
}
}
复制代码
0
二进制数是 000
1
二进制数是 001
2
二进制数是 010
3
二进制数是 011
4
二进制数是 100
5
二进制数是 101
6
二进制数是 110
7
二进制数是 111
以此类推...
与运算的规则以下:
a | b | a & b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
所以,当咱们执行console.log(5&1)
这个表达式时,结果为1
。首先,&
运算符将两个数字都转换为二进制,所以5
变为101
,1
变为001
。
而后,它使用按位怀运算符比较每一个位(0
和1
)。 101&001
,从表中能够看出,若是a & b
为1
,因此5&1
结果为1
。
101 & 001 |
---|
101 |
001 |
001 |
1&0
,结果是0
。0&0
,结果是0
。1&1
,结果是1
。001
,对应的十进制数,即1
。由此咱们也能够算出console.log(4 & 1)
结果为0
。知道4
的最后一位是0
,而0 & 1
将是0
。若是你很难理解这一点,咱们可使用递归函数来解决此问题。
function isEven(num) {
if (num < 0 || num === 1) return false;
if (num == 0) return true;
return isEven(num - 2);
}
复制代码
检查对象中是否存在属性有三种方法。
第一种使用 in
操做符号:
const o = {
"prop" : "bwahahah",
"prop2" : "hweasa"
};
console.log("prop" in o); // true
console.log("prop1" in o); // false
复制代码
第二种使用 hasOwnProperty
方法,hasOwnProperty()
方法会返回一个布尔值,指示对象自身属性中是否具备指定的属性(也就是,是否有指定的键)。
console.log(o.hasOwnProperty("prop2")); // true
console.log(o.hasOwnProperty("prop1")); // false
复制代码
第三种使用括号符号obj["prop"]
。若是属性存在,它将返回该属性的值,不然将返回undefined
。
console.log(o["prop"]); // "bwahahah"
console.log(o["prop1"]); // undefined
复制代码
即异步的 JavaScript 和 XML,是一种用于建立快速动态网页的技术,传统的网页(不使用 AJAX)若是须要更新内容,必需重载整个网页面。使用AJAX则不须要加载更新整个网页,实现部份内容更新
用到AJAX的技术:
使用对象字面量:
const o = {
name: "前端小智",
greeting() {
return `Hi, 我是${this.name}`;
}
};
o.greeting(); // "Hi, 我是前端小智"
复制代码
使用构造函数:
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function () {
return `Hi, 我是${this.name}`;
}
const mark = new Person("前端小智");
mark.greeting(); // "Hi, 我是前端小智"
复制代码
使用 Object.create 方法:
const n = {
greeting() {
return `Hi, 我是${this.name}`;
}
};
const o = Object.create(n);
o.name = "前端小智";
复制代码
Object.freeze()
Object.freeze()
方法能够冻结一个对象。一个被冻结的对象不再能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
Object.seal()
Object.seal()方法封闭一个对象,阻止添加新属性并将全部现有属性标记为不可配置。当前属性的值只要可写就能够改变。
复制代码
方法的相同点:
方法不一样点:
Object.seal
方法生成的密封对象,若是属性是可写的,那么能够修改属性值。 * Object.freeze
方法生成的冻结对象,属性都是不可写的,也就是属性值没法更改。in
运算符和 Object.hasOwnProperty
方法有什么区别?hasOwnPropert方法
hasOwnPropert()
方法返回值是一个布尔值,指示对象自身属性中是否具备指定的属性,所以这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.phone= '15345025546';
let obj = {
name: '前端小智',
age: '28'
}
console.log(obj.hasOwnProperty('phone')) // false
console.log(obj.hasOwnProperty('name')) // true
复制代码
能够看到,若是在函数原型上定义一个变量phone
,hasOwnProperty
方法会直接忽略掉。
in 运算符
若是指定的属性在指定的对象或其原型链中,则in
运算符返回true
。
仍是用上面的例子来演示:
console.log('phone' in obj) // true
复制代码
能够看到in
运算符会检查它或者其原型链是否包含具备指定名称的属性。
看下面的例子:
hoistedFunc();
notHoistedFunc();
function hoistedFunc(){
console.log("注意:我会被提高");
}
var notHoistedFunc = function(){
console.log("注意:我没有被提高");
}
复制代码
notHoistedFunc
调用抛出异常:Uncaught TypeError: notHoistedFunc is not a function
,而hoistedFunc
调用不会,由于hoistedFunc
会被提高到做用域的顶部,而notHoistedFunc
不会。
在 JS 中有4种方法能够调用函数。
做为函数调用——若是一个函数没有做为方法、构造函数、apply
、call
调用时,此时 this
指向的是 window
对象(非严格模式)
//Global Scope
function add(a,b){
console.log(this);
return a + b;
}
add(1,5); // 打印 "window" 对象和 6
const o = {
method(callback){
callback();
}
}
o.method(function (){
console.log(this); // 打印 "window" 对象
});
复制代码
做为方法调用——若是一个对象的属性有一个函数的值,咱们就称它为方法。调用该方法时,该方法的this
值指向该对象。
const details = {
name : "Marko",
getName(){
return this.name;
}
}
details.getName(); // Marko
复制代码
做为构造函数的调用-若是在函数以前使用new
关键字调用了函数,则该函数称为构造函数
。构造函数里面会默认建立一个空对象,并将this
指向该对象。
function Employee(name, position, yearHired) {
// 建立一个空对象 {}
// 而后将空对象分配给“this”关键字
// this = {};
this.name = name;
this.position = position;
this.yearHired = yearHired;
// 若是没有指定 return ,这里会默认返回 this
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
复制代码
使用apply
和call
方法调用——若是咱们想显式地指定一个函数的this
值,咱们可使用这些方法,这些方法对全部函数均可用。
const obj1 = {
result:0
};
const obj2 = {
result:0
};
function reduceAdd(){
let result = 0;
for(let i = 0, len = arguments.length; i < len; i++){
result += arguments[i];
}
this.result = result;
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // reduceAdd 函数中的 this 对象将是 obj1
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // reduceAdd 函数中的 this 对象将是 obj2
复制代码
缓存是创建一个函数的过程,这个函数可以记住以前计算的结果或值。使用缓存函数是为了不在最后一次使用相同参数的计算中已经执行的函数的计算。这节省了时间,但也有不利的一面,即咱们将消耗更多的内存来保存之前的结果。
function memoize(fn) {
const cache = {};
return function (param) {
if (cache[param]) {
console.log('cached');
return cache[param];
} else {
let result = fn(param);
cache[param] = result;
console.log(`not cached`);
return result;
}
}
}
const toUpper = (str ="")=> str.toUpperCase();
const toUpperMemoized = memoize(toUpper);
toUpperMemoized("abcdef");
toUpperMemoized("abcdef");
复制代码
这个缓存函数适用于接受一个参数。 咱们须要改变下,让它接受多个参数。
const slice = Array.prototype.slice;
function memoize(fn) {
const cache = {};
return (...args) => {
const params = slice.call(args);
console.log(params);
if (cache[params]) {
console.log('cached');
return cache[params];
} else {
let result = fn(...args);
cache[params] = result;
console.log(`not cached`);
return result;
}
}
}
const makeFullName = (fName, lName) => `${fName} ${lName}`;
const reduceAdd = (numbers, startingValue = 0) => numbers.reduce((total, cur) => total + cur, startingValue);
const memoizedMakeFullName = memoize(makeFullName);
const memoizedReduceAdd = memoize(reduceAdd);
memoizedMakeFullName("Marko", "Polo");
memoizedMakeFullName("Marko", "Polo");
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
复制代码
typeof null == 'object'
老是返回true
,由于这是自 JS 诞生以来null
的实现。曾经有人提出将typeof null == 'object'
修改成typeof null == 'null'
,可是被拒绝了,由于这将致使更多的bug。
咱们可使用严格相等运算符===
来检查值是否为null
。
function isNull(value){
return value === null;
}
复制代码
new
关键字与构造函数一块儿使用以建立对象:
function Employee(name, position, yearHired) {
this.name = name;
this.position = position;
this.yearHired = yearHired;
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
复制代码
new
关键字作了4
件事:
{}
this
值__proto__
指向构造函数的prototype
return
语句,则返回this
看下面事例:
function Person() { this.name = '前端小智' }
根据上面描述的,new Person()
作了:
var obj = {}
this
值:this = obj__proto__
指向构造函数的prototype
:this.__proto__ = Person().prototype
this
:return this
不该该使用箭头函数一些状况:
this/arguments
时,因为箭头函数自己不具备this/arguments
,所以它们取决于外部上下文this
即对象自己。const
和Object.freeze
是两个彻底不一样的概念。
const
声明一个只读的变量,一旦声明,常量的值就不可改变:
const person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
person = animal; // ERROR "person" is read-only
复制代码
Object.freeze
适用于值,更具体地说,适用于对象值,它使对象不可变,即不能更改其属性。
let person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
Object.freeze(person);
person.name = "Lima"; //TypeError: Cannot assign to read only property 'name' of object
console.log(person);
复制代码
若是我们想要确保对象被深冻结,就必须建立一个递归函数来冻结对象类型的每一个属性:
没有深冻结
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
Object.freeze(person);
person.profession.name = "doctor";
console.log(person); //output { name: 'Leonardo', profession: { name: 'doctor' } }
复制代码
深冻结
function deepFreeze(object) {
let propNames = Object.getOwnPropertyNames(object);
for (let name of propNames) {
let value = object[name];
object[name] = value && typeof value === "object" ?
deepFreeze(value) : value;
}
return Object.freeze(object);
}
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
deepFreeze(person);
person.profession.name = "doctor"; // TypeError: Cannot assign to read only property 'name' of object
复制代码
Iterator
是什么,有什么做用?遍历器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)。
Iterator
的做用有三个:
for...of
循环,Iterator 接口主要供for...of
消费。遍历过程:
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来讲,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
//obj就是可遍历的,由于它遵循了Iterator标准,且包含[Symbol.iterator]方法,方法函数也符合标准的Iterator接口规范。
//obj.[Symbol.iterator]() 就是Iterator遍历器
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
复制代码
Generator
函数是什么,有什么做用?若是说 JavaScrip 是 ECMAScript 标准的一种具体实现、Iterator
遍历器是Iterator
的具体实现,那么Generator
函数能够说是Iterator
接口的具体实现方式。
执行Generator
函数会返回一个遍历器对象,每一次Generator
函数里面的yield都至关一次遍历器对象的next()
方法,而且能够经过next(value)
方法传入自定义的value
,来改变Generator
函数的行为。
Generator
函数能够经过配合Thunk 函数更轻松更优雅的实现异步编程和控制流管理。
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
原文:
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。