1、es5和es6的区别,说一下你所知道的es6
ECMAScript5,即ES5,是ECMAScript的第五次修订,于2009年完成标准化
ECMAScript6,即ES6,是ECMAScript的第六次修订,于2015年完成,也称ES2015
ES6是继ES5之后的一次改进,相对于ES5更加简洁,提高了开发效率
ES6新增的一些特性:
1)let声明变量和const声明常量,两个都有块级作用域
ES5中是没有块级作用域的,并且var有变量提升,在let中,使用的变量一定要进行声明
2)箭头函数
ES6中的函数定义不再使用关键字function(),而是利用了()=>来进行定义
3)模板字符串
模板字符串是增强版的字符串,用反引号(`)标识,可以当作普通字符串使用,也可以用来定义多行字符串
4)解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值
5)for of循环
for...of循环可以遍历数组、Set和Map结构、某些类似数组的对象、对象,以及字符串
6)import、export导入导出
ES6标准中,Js原生支持模块(module)。将JS代码分割成不同功能的小块进行模块化,将不同功能的代码分别写在不同文件中,各模块只需导出公共接口部分,然后通过模块的导入的方式可以在其他地方使用
7)set数据结构
Set数据结构,类似数组。所有的数据都是唯一的,没有重复的值。它本身是一个构造函数
8)... 展开运算符
可以将数组或对象里面的值展开;还可以将多个值收集为一个变量
9)修饰器 @
decorator是一个函数,用来修改类甚至于是方法的行为。修饰器本质就是编译时执行的函数
10)class 类的继承
ES6中不再像ES5一样使用原型链实现继承,而是引入Class这个概念
11)async、await
使用 async/await, 搭配promise,可以通过编写形似同步的代码来处理异步流程, 提高代码的简洁性和可读性
async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成
12)promise
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理、强大
13)Symbol
Symbol是一种基本类型。Symbol 通过调用symbol函数产生,它接收一个可选的名字参数,该函数返回的symbol是唯一的
14)Proxy代理
使用代理(Proxy)监听对象的操作,然后可以做一些相应事情
2、var、let、const之间的区别
var声明变量可以重复声明,而let不可以重复声明
let 和 const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
var是不受限于块级的,而let是受限于块级
var会与window相映射(会挂一个属性),而let不与window相映射
var可以在声明的上面访问变量,而let有暂存死区,在声明的上面访问变量会报错
const声明之后必须赋值,否则会报错
const定义不可变的量,改变了就会报错
const和let一样不会与window相映射、支持块级作用域、在声明的上面访问变量会报错
3、使用箭头函数应注意什么?
(1)用了箭头函数,this就不是指向window,而是父级(指向是可变的)
(2)不能够使用arguments对象
(3)不能用作构造函数,这就是说不能够使用new命令,否则会抛出一个错误
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数
4、ES6的模板字符串有哪些新特性?并实现一个类模板字符串的功能
基本的字符串格式化。将表达式嵌入字符串中进行拼接。用${}来界定
在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接。ES6反引号(``)就能解决
类模板字符串的功能
let name = 'web';
let age = 10;
let str = '你好,${name} 已经 ${age}岁了' str = str.replace(/\$\{([^}]*)\}/g,function(){
returneval(arguments[1]);
})
console.log(str);//你好,web 已经 10岁了
5、介绍下 Set、Map的区别?
应用场景Set用于数据重组,Map用于数据储存
Set:
(1)成员不能重复
(2)只有键值没有键名,类似数组
(3)可以遍历,方法有add, delete,has
Map:
(1)本质上是健值对的集合,类似集合
(2)可以遍历,可以跟各种数据格式转换
6、ECMAScript 6 怎么写 class ,为何会出现 class?
ES6的class可以看作是一个语法糖,它的绝大部分功能ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法
//定义类class Point {
constructor(x,y) {
//构造方法this.x = x;//this关键字代表实例对象this.y = y;
} toString() {
return'(' +this.x + ',' +this.y + ')';
}
}
7、Promise构造函数是同步执行还是异步执行,那么 then 方法呢?
promise构造函数是同步执行的,then方法是异步执行的
8、setTimeout、Promise、Async/Await 的区别
事件循环中分为宏任务队列和微任务队列
其中setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行
promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行
async函数表示函数里面可能会有异步方法,await后面跟一个表达式
async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行
9、promise有几种状态,什么时候会进入catch?
三个状态:pending、fulfilled、reject
两个过程:padding -> fulfilled、padding -> rejected
当pending为rejectd时,会进入catch
10、下面的输出结果是多少
const promise =newPromise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
})
promise.then(() => {
console.log(3);
})
console.log(4);//1243
Promise 新建后立即执行,所以会先输出 1,2,而Promise.then()内部的代码在 当次 事件循环的 结尾 立刻执行 ,所以会继续输出4,最后输出3
11、使用结构赋值,实现两个变量的值的交换
let a = 1;let b = 2;
[a,b] = [b,a];
12、设计一个对象,键名的类型至少包含一个symbol类型,并且实现遍历所有key
let name = Symbol('name');
let product = {
[name]:"洗衣机",
"price":799 };
Reflect.ownKeys(product);
13、下面Set结构,打印出的size值是多少
let s =new Set();
s.add([1]);
s.add([1]);console.log(s.size);
答案:2
两个数组[1]并不是同一个值,它们分别定义的数组,在内存中分别对应着不同的存储地址,因此并不是相同的值
都能存储到Set结构中,所以size为2
14、Promise 中reject 和 catch 处理上有什么区别
reject 是用来抛出异常,catch 是用来处理异常
reject 是 Promise 的方法,而 catch 是 Promise 实例的方法
reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch
网络异常(比如断网),会直接进入catch而不会进入then的第二个回调
15、使用class 手写一个promise
//创建一个Promise的类
class Promise{
constructor(executer){//构造函数constructor里面是个执行器
this.status = 'pending';//默认的状态 pending
this.value = undefined//成功的值默认undefined
this.reason = undefined//失败的值默认undefined
//状态只有在pending时候才能改变
let resolveFn = value =>{
//判断只有等待时才能resolve成功
if(this.status == pending){
this.status = 'resolve';
this.value = value;
}
}
//判断只有等待时才能reject失败
let rejectFn = reason =>{
if(this.status == pending){
this.status = 'reject';
this.reason = reason;
}
}
try{
//把resolve和reject两个函数传给执行器executer
executer(resolve,reject);
}catch(e){
reject(e);//失败的话进catch
}
}
then(onFufilled,onReject){
//如果状态成功调用onFufilled
if(this.status = 'resolve'){
onFufilled(this.value);
}
//如果状态失败调用onReject
if(this.status = 'reject'){
onReject(this.reason);
}
}
}
16、如何使用Set去重
let arr = [12,43,23,43,68,12];
let item = [...new Set(arr)];
console.log(item);//[12, 43, 23, 68]
17、将下面for循环改成for of形式
let arr = [11,22,33,44,55];
let sum = 0;
for(let i=0;i<arr.length;i++){
sum += arr[i];
}
答案:
let arr = [11,22,33,44,55];
let sum = 0;
for(value of arr){
sum += value;
}
18、理解 async/await以及对Generator的优势
async await 是用来解决异步的,async函数是Generator函数的语法糖
使用关键字async来表示,在函数内部使用 await 来表示异步
async函数返回一个 Promise 对象,可以使用then方法添加回调函数
当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句
async较Generator的优势:
(1)内置执行器。Generator 函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,调用方式跟普通函数的调用一样
(2)更好的语义。async 和 await 相较于 * 和 yield 更加语义化
(3)更广的适用性。yield命令后面只能是 Thunk 函数或 Promise对象,async函数的await后面可以是Promise也可以是原始类型的值
(4)返回值是 Promise。async 函数返回的是 Promise 对象,比Generator函数返回的Iterator对象方便,可以直接使用 then() 方法进行调用
19、forEach、for in、for of三者区别
forEach更多的用来遍历数组
for in 一般常用来遍历对象或json
for of数组对象都可以遍历,遍历对象需要通过和Object.keys()
for in循环出的是key,for of循环出的是value
20、说一下es6的导入导出模块
导入通过import关键字
// 只导入一个
import {sum} from "./example.js"
// 导入多个
import {sum,multiply,time} from "./exportExample.js"
// 导入一整个模块
import * as example from "./exportExample.js"
导出通过export关键字
//可以将export放在任何变量,函数或类声明的前面
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
//也可以使用大括号指定所要输出的一组变量
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
//使用export default时,对应的import语句不需要使用大括号
let bosh = function crs(){}
export default bosh;
import crc from 'crc';
//不使用export default时,对应的import语句需要使用大括号
let bosh = function crs(){}
export bosh;
import {crc} from 'crc';
21、ES6 深拷贝
arr=[1,2,3,4]
let […arr1]=arr;
arr.splice(0,1);
console.log(arr1); //[1,2,3,4]
22、数组去重的几个方法:
1.兼容Set 和 Array.from() 的环境下:
let orderedArray = Array.from(new Set(myArray));
var myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd'];
var myOrderedArray = myArray.reduce(function (accumulator, currentValue) {
if (accumulator.indexOf(currentValue) === -1) {
accumulator.push(currentValue);
}
return accumulator
}, [])
console.log(myOrderedArray);
2.利用arr.reduce()去重
//简单去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
3.扩展运算符数组去重
var arr = [1,2,3,4,5,2,3,1];
var set = new Set(arr);
var newArr = [...set ];
4.去重数组并排序
let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];
let result = arr.sort().reduce((init, current) => {
if(init.length === 0 || init[init.length-1] !== current) {
init.push(current);
}
return init;
}, []);
console.log(result); //[1,2,3,4,5]
23、arr.reduce()方法:
1.数组求和,求乘积
var arr = [1, 2, 3, 4];
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log( sum ); //求和,10
console.log( mul ); //求乘积,24
2.计算数组中每个元素出现的次数
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
3.将二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]
4.将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
5.对象里的属性求和
var result = [
{
subject: 'math',
score: 10
},
{
subject: 'chinese',
score: 20
},
{
subject: 'english',
score: 30
}
];
var sum = result.reduce(function(prev, cur) {
return cur.score + prev;
}, 0);
console.log(sum) //60
6.找出一个数组中的最大/最小值
var numbers = [5, 6, 2, 3, 7];
/* 应用(apply) Math.min/Math.max 内置函数完成 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers)
/* 应用最新的扩展语句spread operator完成 */
var arr = [1, 2, 3];
var max = Math.max(...arr);
var min = Math.min(...arr);
/* 应用简单循环完成 */
max = -Infinity, min = +Infinity;
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] > max)
max = numbers[i];
if (numbers[i] < min)
min = numbers[i];
}
/**在b中找出包含a的所有数据*/
a = [1,2,3];
b = [{id:1,name:1},{id:2,name:2}];
const c = b.filter(item => a.includes(item.id));
console.log(c,’c’)
24、ES6 entries(),keys(),values() 数组遍历
ES6提供了entries(),keys(),values()方法返回数组的遍历器,对于遍历器(Iterator)可以使用for...of进行便利,也可是使用entries()返回的遍历器Iterator.next()方法进行遍历。
1.使用keys()遍历。
keys()返回的是数组元素索引号的遍历器。
const arr1 = ['a', 'b', 'c']
for (let index of arr1.keys()) {
console.log(index);
}
可以看到输出的都是每个数组元素的index。
0
1
2
2.使用values()遍历。
values()返回的是数组元素值的遍历器。
输出
a
b
c
3.使用entries()遍历。
配合解构使用,可以拿到元素的index和value。
for (let [index, val] of arr1.entries()) {
console.log(index, val);
}
结果
0 'a'
1 'b'
2 'c'
4.使用Iterator.next()遍历。
基于entries()返回的遍历器,调用遍历器的next()的方法可以获取每一个元素的访问入口,该入口有一个done属性可以表明是否便利结束。通过入口可以拿到value属性,其就是元素的索引和值的数组。
let arrEntries=arr1.entries();
let entry=arrEntries.next();
while(!entry.done){
console.log(entry.value);
entry=arrEntries.next();
}
结果
25、ES6常用特性
变量定义(let和const,可变与不可变,const定义对象的特殊情况)
解构赋值
模板字符串
数组新API(例:Array.from(),entries(),values(),keys())
箭头函数(rest参数,扩展运算符,::绑定this)
Set和Map数据结构(set实例成员值唯一存储key值,map实例存储键值对(key-value))
Promise对象(前端异步解决方案进化史,generator函数,async函数)
Class语法糖(super关键字)
26、es6中的Module
ES6 中模块化语法更加简洁,使用export抛出,使用import from 接收,
如果只是输出一个唯一的对象,使用export default即可
// 创建 util1.js 文件,内容如
export default {
a: 100
}
// 创建 index.js 文件,内容如
import obj from './util1.js’
如果想要输出许多个对象,就不能用default了,且import时候要加{...},代码如下
// 创建 util2.js 文件,内容如
export function fn1() {
alert('fn1')
}
export function fn2() {
alert('fn2')
}
// 创建 index.js 文件,内容如
import { fn1, fn2 } from './util2.js’
27、ES6 class 和普通构造函数的区别
class 其实一直是 JS 的关键字(保留字),但是一直没有正式使用,直到 ES6 。 ES6 的 class 就是取代之前构造函数初始化对象的形式,从语法上更加符合面向对象的写法
1. class 是一种新的语法形式,是class Name {...}这种形式,和函数的写法完全不一样
2. 两者对比,构造函数函数体的内容要放在 class 中的constructor函数中,constructor即构造器,初始化实例时默认执行
3. class 中函数的写法是add() {...}这种形式,并没有function关键字
而且使用 class 来实现继承就更加简单了
在class中直接extends关键字就可以实现继承,而不像之前的继承实现有多种不同的实现方式,在es6中就只有一种
注意以下两点:
使用extends即可实现继承,更加符合经典面向对象语言的写法,如 Java
子类的constructor一定要执行super(),以调用父类的constructor
28、ES6 中新增的数据类型有哪些?
Set 和 Map 都是 ES6 中新增的数据结构,是对当前 JS 数组和对象这两种重要数据结构的扩展。由于是新增的数据结构
1. Set 类似于数组,但数组可以允许元素重复,Set 不允许元素重复
2. Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型
29、箭头函数的作用域上下文和 普通函数作用域上下文 的区别
箭头函数其实只是一个密名函数的语法糖,区别在于普通函数作用域中的this有特定的指向,一般指向window,而箭头函数中的this只有一个指向那就是指当前函数所在的对象,其实现原理其实就是类似于之前编程的时候在函数外围定义that一样,用了箭头函数就不用定义that了直接使用this
30、es6如何转为es5?
使用Babel 转码器,Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。
兼容与优化
1,页面重构怎么操作?
网站重构:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。
也就是说是在不改变UI的情况下,对网站进行优化,在扩展的同时保持一致的UI。
对于传统的网站来说重构通常是:
表格(table)布局改为DIV+CSS
使网站前端兼容于现代浏览器(针对于不合规范的CSS、如对IE6有效的)
对于移动平台的优化
针对于SEO进行优化
深层次的网站重构应该考虑的方面
减少代码间的耦合
让代码保持弹性
严格按规范编写代码
设计可扩展的API
代替旧有的框架、语言(如VB)
增强用户体验
通常来说对于速度的优化也包含在重构中
压缩JS、CSS、image等前端资源(通常是由服务器来解决)
程序的性能优化(如数据读写)
采用CDN来加速资源加载
对于JS DOM的优化
HTTP服务器的文件缓存
31、箭头函数
1. 箭头函数?
2. 箭头函数与普通函数有什么区别?
1.箭头函数相当于匿名函数,是不能作为构造函数的,不能使用new
2.箭头函数不绑定arguments,取而代之用rest参数…解决
3.箭头函数会捕获其所在上下文的this值,作为自己的this值。即箭头函数的作用域会继承自外围的作用域。
4.箭头函数当方法使用的时候没有定义this的绑定
obj = {
a:10,
b:()=>{
console.log(this.a);//undefined
console.log(this);//window
},
c:function(){
return ()=>{
console.log(this.a);//10
}
}
}
obj.b();
obj.c();
b箭头函数运行时的外围环境是全局作用域,this指向了window
c内部返回的箭头函数运行在c函数内部,其外围的作用域是外部函数的作用域,外部函数的this值指向调用它的obj,所以输出的值是10
5.使用call()和apply()调用
通过call()或者apply()调用一个函数时,只是传入参数而已,对this并没有影响。
var obj = {
a:10,
b:function(n){
var f = (v) => v + this.a;
var c = {a:20};
return f.call(c,n);
}
}
console.log(obj.b(1));//11
6.箭头函数没有函数原型
var a = ()=>{
return 1;
}
function b(){
return 2;
}
console.log(a.prototype);//undefined;
console.log(b.prototype);//object{...}
7.箭头函数不能当做Generator函数,不能使用yield关键字
8.箭头函数不能换行
var a = ()
=> 1;//SyntaxError:Unexpected token
3. 反引号 ` 标识?
用一对反引号(`)标识,它可以当作普通字符串使用,也可以用来定义多行字符串,也可以在字符串中嵌入变量,js表达式或函数,变量、js表达式或函数需要写在${ }中。
var str = `abc
def
gh`;
console.log(str);
let name = "小明";
function a() {
return "ming";
}
console.log(`我的名字叫做${name},年龄${17+2}岁,性别${'男'},游戏ID:${a()}`);
4. 使用 ES6 改下面的模板?
let iam = "我是";
let name = "Oli";
let str = "大家好," + iam + name + ",多指教。";
// let str =`大家好${iam}${name}多指教。`
5. 属性简写、方法简写?
属性简写:
使用一个{}将一个已经复制的变量括起来就相当于是给一个对象添加一个变量名为名字的属性及一个变量值为值得属性值
var foo='hhhhh';
var b={foo};
上面的写法相当于下面的语句
var b={foo:'hhhhh'};
下面打印结果为键值对对象。
var [name,age]=['dale',29];
var obj={name,age};
console.log(obj);
var obj={name,age} 这就是对象中属性的简写
对象的简写:
当要定义的对象键和对象名是一样的是时候就可以使用简写方式。
var obj={name:'dale',age:29};
var o={
name:'liny'
,obj
}
console.log(o.obj.name); //dale
函数属性的简写
function fun(){
console.log(this.a+this.b);
}
var obj={
a:1,
b:2,
fun:fun
}
obj.fun();
当对象中的属性和方法名一致时,就可以使用简写形式fun:fun =》fun
属性及方法的简写应用
在模块导出函数时:
以前的写法
module.exports={
getOne:function(){},
getAll:function(){}
}
现在的写法:
function getOne(){};
function getAll(){};
module.exports={
getOne,getAll
}
后面写法更加的简洁,把函数的申明和模块导出分开。
6. JS中for,for...in,for...of以及foreach循环的用法?
1.for()循环
// for循环的表达式之间用的是;号分隔的,千万不要写成,
for (初始化表达式1; 判断表达式2; 自增表达式3) {
// 循环体4
}
2.for...in索引遍历
var obj1 = {
name:'张三',
age : 17,
sex : '男',
}
for(var k in obj1){
console.log(k);
console.log(obj1[k]);
}
注意:使用for …in语法,同样可以遍历数组
注意:如果属性名或方法名,是一个变量,则使用对象[变量名] 语法
3.for...of值遍历
//遍历数组
var team = ["师父", "大师兄", "二师兄", "沙师弟", "小白龙"];
for(var v of team){
console.log(v);
}
//也可以遍历字符串
var str = "zhangsan";
for(var v of str){
console.log(v);
}
注意:不能遍历对象
4.数组.forEach方法
array.forEach(v=>{
console.log(v);
});
array.forEach(function(v){
console.log(v);
});
7. 字符串新增方法
1.String.fromCodePoint()
Es6 提供了 String.fromCodePoint() 方法,可以识别大于 0xFFFF 的字符,补充了 String.fromCharCode() 方法的不足。
String.fromCodePoint(0x20BB7)
// '告'
String.fromCodePoint(0x78,0x1f680,0x79) === 'x\uD83D\uDE80y'
// true
如果 String.fromCodePoint() 有多个参数,则会合成一个字符串返回。
注意:fromCodePoint() 方法定义在 String 对象上,而 codePointAt() 方法定义在字符串实例对象上。
2.String.raw()
Es6 还为原生的 String 对象听哦你了 raw 方法。该方法返回一个斜杆都被转义(即斜杠前面再加一个斜杠)的字符串。往往用于模板字符串的处理方法。
String.raw'Hi\n${2+3}!';
// 返回 "Hi\\n5!"
如果原字符串的已经转义,那么 String.raw() 会再次进行转义。
String.raw'Hi\\n'
// 返回 "Hi\\\\n"
String.raw() 方法可以作为处理模板字符串的基本方法,它会将素有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。
3.codePointAt()
JavaScript 内部,字符以 UTF-16 的格式存储,每个字符固定为 2 个字节。对于那些需要 4 个字节存储的字符(Unicode 码点大于 0xFFFF 的字符),JavaScript 会认为它们是两个字符。
4.normalize()
Es6 提供了字符串实例的 normalize() 方法用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。
'\u01D1'.normalize() === '\u004f\u030c'.normalize()
// true
5.includes() startsWith() endsWith()
传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。Es 6 又提供了三种新的方法。
includes():返回布尔值,表示是否找到了参数字符串
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
let str = 'hello world !';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
这三个方法都支持第二个参数,表示搜索的位置。
let s = 'Hello world!';
s.startsWith('world',6)// true
s.endsWith('Hello',5)// true
s.includes('Hello',6)// false
从上面我们可以看出来,使用第二个参数的时候,endsWith 的行为与其他两个方法有所不同,它针对前 n 个字符,而其他两个方法,针对从第 n 个位置知道字符串结束。
6.repeat()
repeat() 方法返回一个新的字符串,表示将原字符串重复 n 次。
'x'.repeat(3) // "xxx"
'haha'.repeat(0) // "" (空字符串。)
如果是小数,则会取整。
'na'.repeat(2.9);// "nana"
如果参数是负数或者 Infinity,会报错。
'str'.repeat(Infinity)
// RangeError
'str'.repeat(-1)
// RangError
但是如果是参数是 0 到 -1 之间的小数,则等同于 0 ,这是因为会先进行取整运算。
参数 NaN 等同于 0。
如果 repeat() 参数是字符串,则会先转成数字
'str'.repeat('nan'); // ""
'str'.repeat('3'); // "nanana"
7.padStart() padEnd()
Es2017 引入了字符串补全长度的功能,如果某个字符串不够指定长度,会在头部或尾部补全。padStart() 用于头部补全,padEnd() 用于尾部补全。
'x'.padStart(5, 'st') // 'ststx'
'x'.padStart(4, 'st') // 'stsx'
padStart() 和 padEnd() 一共接受两个参数,第一是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
'xxx'.padStart(2,'s'); // 'xxx'
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截取超出位数的补全字符串。
'abc'.padStart(10,'123456789');
// '1234567abc'
如果省略掉第二个参数,默认使用空格补全长度。
'x'.padStart(4); // ' x'
padStart() 的常用用途是为数值补全指定位数。下面代码生成 10 位数值字符串。
'1'.padStart(10, '0') // 0000000001
'12'.padStart(10, '0') // 0000000012
另一个用途是提示字符串格式。
'12'.padStart(10, 'YYYY-MM-DD') // YYYY-MM-12
'09-12'.padStart(10, 'YYYY-MM-DD') // YYYY-09-12
8.trimStart() trimEnd()
Es2019 对字符串实例新增了 trimStart() 和 trimEnd() 这两个方法。它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格,trimEnd() 消除尾部的空格,它们返回都是新字符串,不会修改原来的字符串。
const s = ' str ';
s.trim() // 'str'
s.trimStart() // 'str '
s.trimEnd() // ' str'
9.matchAll()
matchAll() 方法返回一个正则表达式在当前字符串的所有匹配。
7.2彻底弄懂各种情况下的 this 指向
1. 如何改变函数内部的 this 指针的指向?
每个函数都包含两个非继承来的方法call()和apply();
使用call()或者apply(),可以改变this的指向;
假设要改变fn函数内部的this的指向,指向obj,那么可以fn.call(obj);或者fn.apply(obj);
call和apply的区别:
call和apply的区别在于参数,他们两个的第一个参数都是一样的,表示调用该函数的对象;
apply的第二个参数是数组,是[arg1,arg2,arg3]这种形式,而call是arg1,arg2,arg3这样的形式。
另外还可以用bind函数:
var bar=fn.bind(obj);
那么fn中的this就指向obj对象了,bind函数返回新的函数,这个函数内的this指针指向obj对象。
2. 如何判断 this?箭头函数的 this 是什么?
this 是很多人会混淆的概念,但是其实它一点都不难,只是网上很多文章把简单的东西说复杂了。在这里,你一定会彻底明白 this 这个概念的。
我们先来看几个函数调用的场景:
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
接下来我们一个个分析上面几个场景
对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
说完了以上几种情况,其实很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window。另外对箭头函数使用 bind 这类函数是无效的。
最后一种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window。
那么说到 bind,不知道大家是否考虑过,如果对一个函数进行多次 bind,那么上下文会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window。
let a = { name: 'qwe' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'qwe'
以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
如果你还是觉得有点绕,那么就看以下的这张流程图吧,图中的流程只针对于单个规则。
3. call、apply 以及 bind 函数内部实现是怎么样的?
call, apply, bind都是改变函数执行的上下文,说的直白点就是改变了函数this的指向。不同的是:call和apply改变了函数的this,并且执行了该函数,而bind是改变了函数的this,并返回一个函数,但不执行该函数。
看下面的例子1:
var doThu = function(a, b) {
console.log(this)
console.log(this.name)
console.log([a, b])
}
var stu = {
name: 'xiaoming',
doThu: doThu,
}
stu.doThu(1, 2) // stu对象 xiaoming [1, 2]
doThu.call(stu, 1, 2) // stu对象 xiaoming [1, 2]
由此可见,在stu上添加一个属性doThu,再执行这个函数,就将doThu的this指向了stu。而call的作用就与此相当,只不过call为stu添加了doThu方法后,执行了doThu,然后再将doThu这个方法从stu中删除。
下面来看call函数的内部实现原理:
Function.prototype.call = function(thisArg, args) {
// this指向调用call的对象
if (typeof this !== 'function') { // 调用call的若不是函数则报错
throw new TypeError('Error')
}
thisArg = thisArg || window
thisArg.fn = this // 将调用call函数的对象添加到thisArg的属性中
const result = thisArg.fn(...[...arguments].slice(1)) // 执行该属性
delete thisArg.fn // 删除该属性
return result
}
apply的实现原理和call一样,只不过是传入的参数不同而已。
Function.prototype.apply = function(thisArg, args) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
thisArg = thisArg || window
thisArg.fn = this
let result
if(args) {
result = thisArg.fn(...args)
} else {
result = thisArg.fn()
}
delete thisArg.fn
return result
}
bind的实现原理比call和apply要复杂一些,bind中需要考虑一些复杂的边界条件。bind后的函数会返回一个函数,而这个函数也可能被用来实例化:
Function.prototype.bind = function(thisArg) {
if(typeof this !== 'function'){
throw new TypeError(this + 'must be a function');
}
// 存储函数本身
const _this = this;
// 去除thisArg的其他参数 转成数组
const args = [...arguments].slice(1)
// 返回一个函数
const bound = function() {
// 可能返回了一个构造函数,我们可以 new F(),所以需要判断
if (this instanceof bound) {
return new _this(...args, ...arguments)
}
// apply修改this指向,把两个函数的参数合并传给thisArg函数,并执行thisArg函数,返回执行结果
return _this.apply(thisArg, args.concat(...arguments))
}
return bound
}
32、数组、对象、函数的解构赋值
1. 使用解构,实现两个变量的值的交换?
let a=5;
let b=3;
[a,b]=[b,a]
2. 解构赋值?
数组模型的解构(Array)
基本
let [a, b, c] = [1, 2, 3];// a = 1// b = 2// c = 3
可嵌套
let [a, [[b], c]] = [1, [[2], 3]];// a = 1// b = 2// c = 3
可忽略
let [a, , b] = [1, 2, 3];// a = 1// b = 3
不完全解构
let [a = 1, b] = []; // a = 1, b = undefined
剩余运算符
let [a, ...b] = [1, 2, 3];//a = 1//b = [2, 3]
字符串等
在数组的解构中,解构的目标若为可遍历对象,皆可进行解构赋值。可遍历对象即实现 Iterator 接口的数据。
let [a, b, c, d, e] = 'hello';// a = 'h'// b = 'e'// c = 'l'// d = 'l'// e = 'o'
解构默认值
let [a = 2] = [undefined]; // a = 2
当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。
let [a = 3, b = a] = []; // a = 3, b = 3
let [a = 3, b = a] = [1]; // a = 1, b = 1
let [a = 3, b = a] = [1, 2]; // a = 1, b = 2
a 与 b 匹配结果为 undefined ,触发默认值:a = 3; b = a =3
a 正常解构赋值,匹配结果:a = 1,b 匹配结果 undefined ,触发默认值:b = a =1
a 与 b 正常解构赋值,匹配结果:a = 1,b = 2
对象模型的解构(Object)
基本
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };// foo = 'aaa'// bar = 'bbb'
let { baz : foo } = { baz : 'ddd' };// foo = 'ddd'
可嵌套可忽略
let obj = {p: ['hello', {y: 'world'}] };
let {p: [x, { y }] } = obj;
// x = 'hello'// y = 'world'
let obj = {p: ['hello', {y: 'world'}] };
let {p: [x, { }] } = obj;
// x = 'hello'
不完全解构
let obj = {p: [{y: 'world'}] };let {p: [{ y }, x ] } = obj;// x = undefined// y = 'world'
剩余运算符
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};// a = 10// b = 20// rest = {c: 30, d: 40}
解构默认值
let {a = 10, b = 5} = {a: 3};// a = 3; b = 5;let {a: aa = 10, b: bb = 5} = {a: 3};// aa = 3; bb = 5;
3. 函数默认参数?
函数默认参数
在ES6中,可以为函数的参数指定默认值。函数默认参数允许在没有值或undefined被传入时使用默认形参。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
默认参数使用注意点:参数变量是默认声明的,所以不能用let或const再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
使用参数默认值时,函数不能有同名参数。
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
显式传入undefined或不传值时使用函数默认参数值;传入''或null时使用传入的参数值。
function test(num = 1) {
console.log(typeof num);
}
test(); // 'number' (num is set to 1)
test(undefined); // 'number' (num is set to 1 too)
// test with other falsy values:
test(''); // 'string' (num is set to '')
test(null); // 'object' (num is set to null)
参数默认值不是传值的,而是在函数被调用时,参数默认值才会被解析。
function append(value, array = []) {
array.push(value);
return array;
}
append(1); //[1]
append(2); //[2], not [1, 2]
位置在前的默认参数可用于后面的默认参数。
function greet(name, greeting, message = greeting + ' ' + name) {
return [name, greeting, message];
}
greet('David', 'Hi'); // ["David", "Hi", "Hi David"]
greet('David', 'Hi', 'Happy Birthday!'); // ["David", "Hi", "Happy Birthday!"]
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。后文的 rest 参数也不会计入length属性。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
剩余(rest)参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
rest 参数使用注意点:rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错
function f(a, ...b, c) {
// ...
}
函数的length属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
rest参数可以被解构,这意味着他们的数据可以被解包到不同的变量中。
function f(...[a, b, c]) {
return a + b + c;
}
f(1) // NaN (b and c are undefined)
f(1, 2, 3) // 6
f(1, 2, 3, 4) // 6 (the fourth parameter is not destructured)
rest参数和 arguments对象的区别:
rest参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参。
arguments对象不是一个真正的数组,而rest参数是真正的Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
arguments对象还有一些附加的属性 (如callee属性)。
4. JavaScript 中什么是变量提升?什么是暂时性死区?
变量提升
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上面代码中,变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。
暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
下面的代码也会报错,与var的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
import和require都是被模块化使用
1. require是CommonJs的语法(AMD规范引入方式),CommonJs的模块是对象。
import是es6的一个语法标准(浏览器不支持,本质是使用node中的babel将es6转码为es5再执行,import会被转码为 require),es6模块不是对象
2. require是运行时加载整个模块(即模块中所有方法),生成一个对象,再从对象上读取它的方法(只有运行时才能得到这个对象,不能在编译时做到静态化),理论上可以用在代码的任何地方
import是编译时调用,确定模块的依赖关系,输入变量(es6模块不是对象,而是通过export命令指定输出代码,再通过import输入,只加载import中导的方法,其他方法不加载),import具有提升效果,会提升到模块的头部(编译时执行)export和import可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,会报错es6这样的设计可以提高编译器效率,但没法实现运行时加载
3. require是赋值过程,把require的结果(对象,数字,函数等),默认是export的一个对象,赋给某个变量(复制或浅拷贝)
import是解构过程(需要谁,加载谁)
写法:
1. require/exports(仅有下面的三种简单写法)
const a=require('a') //真正被require出来的是来自module.exports指向的内存块内容
exports.a=a //exports 只是 module.exports的引用,辅助module.exports操作内存中的数据
module.exports=a
2. import / export
import a from 'a'
import { default as a } from 'a'
import * as a from 'a'
import { fun1,fun2 } from 'a'
import { fun1 as myfunction } from 'a'
import a, { fun1 } from 'a'
---------------------------------------------------------
export default a
export const a=1
export functon a{ }
export { fun1,fun2 }