1、什么是 `async/await` 及其如何工作,有什么优缺点
async/await 是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。
async 是异步的意思,而 await 是 async wait的简写,即异步等待。
所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}
使用 async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。
await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
优缺点:
async/await 的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
2、展开(spread )运算符和剩余(Rest) 运算符有什么区别?
展开运算符(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
3、什么是默认参数
默认参数指的是当 函数调用 中省略了实参时自动使用的一个值。
4、什么是包装对象
存取字符串、数字或布尔值的属性时,创建的临时对象称为包装对象。
5、显示和隐示转换有什么区别
隐式转换:说白了就是在转换时不给系统提示具体的显示模型,让其自动转换,但是要记住一条编译器一般只支持自下而上的类型转换。
显示转换:就是我们在c语言课程中学的,强制转换,是我们可以直接对其赋值的。
6、什么是NaN,如何检查是NaN
是Not a Numer 的缩写,不是一个数字 属性是number。检查方法value !== value,如果是NaN,答案只会是true
7、如何判断值为数组
方法一: instanceof
instanceof 用于判断一个变量是否某个对象的实例,左边操作数是一个对象,右边操作数是一个函数对象或者函数构造器。原理是通过判断左操作数的对象的原型链上是否具有右操作数的构造函数的prototype属性。a instanceof b?alert("true"):alert("false") //注意b值是你想要判断的那种数据类型,不是一个字符串,比如Array。
- var arr=[];
- console.log(arr instanceof Array) //返回true
方法二之 constructor
在W3C定义中的定义:constructor 属性返回对创建此对象的数组函数的引用,就是返回对象相对应的构造函数。从定义上来说跟instanceof不太一致,但效果都是一样的。
那么判断各种类型的方法:
- console.log([].constructor == Array);
- console.log({}.constructor == Object);
- console.log("string".constructor == String);
- console.log((123).constructor == Number);
- console.log(true.constructor == Boolean);
原因:
1、array属于引用型数据,在传递过程中,仅仅是引用地址的传递。
2、每个页面的Array原生对象所引用的地址是不一样的,在子页面声明的array,所对应的构造函数,是子页面的Array对象;父页面来进行判断,使用的Array并不等于子页面的Array。
方法三 最简单的方法 Object.prototype.toString.call(arr) === “[object Array]”
- function isArray(o) {
- return Object.prototype.toString.call(o) === ‘[object Array]‘;
- }
1. 定义对象方法
JS 中对象方法的定义方式是在对象上定义一个指向函数的属性,当方法被调用的时候,方法内的 this 就会指向方法所属的对象。
1.1 定义字面量方法
因为箭头函数的语法很简洁,可能不少同学会忍不住用它来定义字面量方法,比如下面的例子 JS Bin:
const calculator = {
array: [1, 2, 3],
sum: () => {
console.log(this === window); // => true
return this.array.reduce((result, item) => result + item);
}
};
console.log(this === window); // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculator.sum();
calculator.sum 使用箭头函数来定义,但是调用的时候会抛出 TypeError,因为运行时 this.array 是未定义的,调用 calculator.sum 的时候,执行上下文里面的 this 仍然指向的是 window,原因是箭头函数把函数上下文绑定到了 window 上,this.array 等价于 window.array,显然后者是未定义的。
解决的办法是,使用函数表达式或者方法简写(ES6 中已经支持)来定义方法,这样能确保 this 是在运行时是由包含它的上下文决定的,修正后的代码如下 JS Bin:
const calculator = {
array: [1, 2, 3],
sum() {
console.log(this === calculator); // => true
return this.array.reduce((result, item) => result + item);
}
};
calculator.sum(); // => 6
这样 calculator.sum 就变成了普通函数,执行时 this 就指向 calculator 对象,自然能得到正确的计算结果。
1.2 定义原型方法
同样的规则适用于原型方法(prototype method)的定义,使用箭头函数会导致运行时的执行上下文错误,比如下面的例子 JS Bin:
function Cat(name) {
this.name = name;
}
Cat.prototype.sayCatName = () => {
console.log(this === window); // => true
return this.name;
};
const cat = new Cat('Mew');
cat.sayCatName(); // => undefined
使用传统的函数表达式就能解决问题 JS Bin:
function Cat(name) {
this.name = name;
}
Cat.prototype.sayCatName = function () {
console.log(this === cat); // => true
return this.name;
};
const cat = new Cat('Mew');
cat.sayCatName(); // => 'Mew'
sayCatName 变成普通函数之后,被调用时的执行上下文就会指向新创建的 cat 实例。
2. 定义事件回调函数
this 是 JS 中很强大的特性,可以通过多种方式改变函数执行上下文,JS 内部也有几种不同的默认上下文指向,但普适的规则是在谁上面调用函数 this 就指向谁,这样代码理解起来也很自然,读起来就像在说,某个对象上正在发生某件事情。
但是,箭头函数在声明的时候就绑定了执行上下文,要动态改变上下文是不可能的,在需要动态上下文的时候它的弊端就凸显出来。比如在客户端编程中常见的 DOM 事件回调函数(event listenner)绑定,触发回调函数时 this 指向当前发生事件的 DOM 节点,而动态上下文这个时候就非常有用,比如下面这段代码试图使用箭头函数来作事件回调函数 JS Bin:
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
在全局上下文下定义的箭头函数执行时 this 会指向 window,当单击事件发生时,浏览器会尝试用 button 作为上下文来执行事件回调函数,但是箭头函数预定义的上下文是不能被修改的,这样 this.innerHTML 就等价于 window.innerHTML,而后者是没有任何意义的。
使用函数表达式就可以在运行时动态的改变 this,修正后的代码 JS Bin:
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
当用户单击按钮时,事件回调函数中的 this 实际指向 button,这样的 this.innerHTML = 'Clicked button' 就能按照预期修改按钮中的文字。
3. 定义构造函数
构造函数中的 this 指向新创建的对象,当执行 new Car() 的时候,构造函数 Car 的上下文就是新创建的对象,也就是说 this instanceof Car === true。显然,箭头函数是不能用来做构造函数, 实际上 JS 会禁止你这么做,如果你这么做了,它就会抛出异常。
换句话说,箭头构造函数的执行并没有任何意义,并且是有歧义的。比如,当我们运行下面的代码 JS Bin:
const Message = (text) => {
this.text = text;
};
// Throws "TypeError: Message is not a constructor"
const helloMessage = new Message('Hello World!');
构造新的 Message 实例时,JS 引擎抛了错误,因为 Message 不是构造函数。在笔者看来,相比旧的 JS 引擎在出错时悄悄失败的设计,ES6 在出错时给出具体错误消息是非常不错的实践。可以通过使用函数表达式或者函数声明 来声明构造函数修复上面的例子 JS Bin:
const Message = function(text) {
this.text = text;
};
const helloMessage = new Message('Hello World!');
console.log(helloMessage.text); // => 'Hello World!'
4. 追求过短的代码
箭头函数允许你省略参数两边的括号、函数体的花括号、甚至 return 关键词,这对编写更简短的代码非常有帮助。这让我想起大学计算机老师给学生留过的有趣作业:看谁能使用 C 语言编写出最短的函数来计算字符串的长度,这对学习和探索新语言特性是个不错的法子。但是,在实际的软件工程中,代码写完之后会被很多工程师阅读,真正的 write once, read many times,在代码可读性方面,最短的代码可能并不总是最好的。一定程度上,压缩了太多逻辑的简短代码,阅读起来就没有那么直观,比如下面的例子 JS Bin:
const multiply = (a, b) => b === undefined ? b => a * b : a * b;
const double = multiply(2);
double(3); // => 6
multiply(2, 3); // => 6
multiply 函数会返回两个数字的乘积或者返回一个可以继续调用的固定了一个参数的函数。代码看起来很简短,但大多数人第一眼看上去可能无法立即搞清楚它干了什么,怎么让这段代码可读性更高呢?有很多办法,可以在箭头函数中加上括号、条件判断、返回语句,或者使用普通的函数 JS Bin:
function multiply(a, b) {
if (b === undefined) {
return function (b) {
return a * b;
}
}
return a * b;
}
const double = multiply(2);
double(3); // => 6
multiply(2, 3); // => 6
为了让代码可读性更高,在简短和啰嗦之间把握好平衡是非常有必要的。
12、const和object.freeze()有什么区别
const的行为像 let。它们唯一的区别是, const定义了一个无法重新分配的变量。 通过 const声明的变量是具有块级作用域的,而不是像 var声明的变量具有函数作用域。
Object.freeze()接受一个对象作为参数,并返回一个相同的不可变的对象。这就意味着我们不能添加,删除或更改对象的任何属性。
13、如何在js中深冻结对象?
如果咱们想要确保对象被深冻结,就必须创建一个递归函数来冻结对象类型的每个属性:
没有深冻结
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'
14、Iterator`是什么,有什么作用?
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator
的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6 创造了一种新的遍历命令
for…of
循环,Iterator 接口主要供for…of
消费。
遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
15、Generator` 函数是什么,有什么作用?
如果说 JavaScrip 是 ECMAScript 标准的一种具体实现、Iterator
遍历器是Iterator
的具体实现,那么Generator
函数可以说是Iterator
接口的具体实现方式。
执行Generator
函数会返回一个遍历器对象,每一次Generator
函数里面的yield都相当一次遍历器对象的next()
方法,并且可以通过next(value)
方法传入自定义的value
,来改变Generator
函数的行为。
Generator
函数可以通过配合Thunk 函数更轻松更优雅的实现异步编程和控制流管理。
16、有什么办法可以处理js中都异步代码?
回调函数。回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。回调 …
事件监听。这种方式,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生
发布/订阅模式。我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish) …
promise
genator
async/await
17、函数表达式和函数声明之间有什么区别
1).以函数声明的方法定义的函数,函数名是必须的,而函数表达式的函数名是可选的。(函数声明整体会被提升到当前作用域的顶部,函数表达式也提升到顶部但是只有其变量名提升)
2).以函数声明的方法定义的函数,函数可以在函数声明之前调用,而函数表达式的函数只能在声明之后调用。
3).以函数声明的方法定义的函数并不是真正的声明,它们仅仅可以出现在全局中,或者嵌套在其他的函数中,但是它们不能出现在循环,条件或者try/catch/finally中,而函数表达式可以在任何地方声明。换句话说,函数声明不是一个完整的语句,所以不能出现在if-else,for循环,finally,try catch语句以及with语句中。
18、调用函数,可以使用哪些方法
一般形式函数调用,作为对象方法调用,new命令调用,call和apply调用