高频前端面试题汇总之JavaScript篇(上)

726 阅读28分钟

一、数据类型

1. JavaScript有哪些数据类型,它们的区别?

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object(包括普通Object、Function、Array、Date、RegExp、Math)、Symbol、BigInt。

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:基本数据类型(Undefined、Null、Boolean、Number、String、Symbol)
  • 堆:引用数据类型(对象、数组和函数)

2. 两种数据类型的区别

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,可以直接按值访问
var a = 10
var b = a
b = 20

image.png

  • 引用数据类型是存储在堆(heap)中的对象,占据空间大、大小不固定。引用类型数据在栈中保存的实际上是对象在堆内存中的引用地址,通过这个引用地址可以查找到保存中堆内存中的对象。
var obj1 = new Object()
var obj2 = obj1
obj2.name = "我有名字了"

image.png

3. 堆和栈的概念

(1)内存操作场景下,堆与栈表示两种内存的管理方式:程序运行的时候,需要内存空间存放数据,系统会划分出两种不同的内存空间(堆和栈),它们的主要区别是:栈是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;堆是没有结构的,数据可以任意存放,栈的寻址速度要快于堆。

(2)数据结构场景下,堆与栈表示两种常用的数据结构:栈是一种线性表,允许在表的一端(栈顶)进行插入和删除操作;堆其实是一种优先队列,也就是说队列中存在优先级,比如队列中有很多待执行任务,执行时会根据优先级找优先度最高的先执行。

4. undefined和undeclared的区别

undefined 是 Javascript 中的语言类型之一,而 undeclared 是 Javascript 中的一种语法错误。undefined: 已声明,未赋值。尝试访问一个 undefined 的变量时,浏览器不会报错并会返回 undefined;undeclared: 未声明,未赋值。尝试访问一个 undeclared的变量时,浏览器会报错,JS执行会中断。

5. 数据类型判断的方式有哪些

(1)typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

typeof 是一个操作符而不是函数,其中数组、对象、null都会被判断为object,其他判断都正确。

注意事项:

  1. typeof 返回值为字符串格式,比如: typeof(typeof(undefined)) -> "string"
  2. typeof 未定义的变量不会报错,返回 "undefiend"
  3. typeof(NaN) -> "number"

思考一个问题:typeof('abc')和 typeof 'abc'都是 string, 那么 typeof 是操作符还是函数?

答案:如果 typeof 为 function,那么 typeof(typeof) 会返回'function',但是经测试,上述代码浏览器会抛出错误。因此可以证明 typeof 并非函数。其实 typeof 后面的括号的作用是进行分组而非函数的调用。

(2)instanceof

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

instanceof返回的是一个布尔值,instanceof 是用来判断 A 是否为 B 的实例,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。其内部运行机制是判断构造函数(右边)的 prototype 属性是否出现在对象(左边)的原型链中的任何位置。

(3) constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

每个对象都有一个constructor属性,可以得知某个实例对象,到底是哪一个构造函数产生的, constructor属性表示原型对象与构造函数之间的关联关系。注意:null 和 undefined 是没有 constructor 存在的。

当一个函数F被定义时,JS会为其添加prototype原型,然后在prototype上添加一个constructor属性,并让其指向F的引用。当F作为构造函数创建对象时,原型上的constructor属性被遗传到了新创建的对象上,从原型链角度讲,构造函数F就是新对象的类型。

(4)Object.prototype.toString.call()

let a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

image.png 对于 Object.prototype.toString() 方法,会返回一个形如 "[object XXX]" 的字符串。如果对象的 toString() 方法未被重写,就会返回如上面形式的字符串。但是大多数对象, toString() 方法都是重写了的,这时需要用 call() 来调用。

优点:精准判断数据类型 缺点:写法繁琐不容易记

6. 判断数组的方式有哪些

1. Object.prototype.toString.call(arr).slice(8,-1) === 'Array'

2. arr.__proto__ === Array.prototype

3. Array.isArrray(arr)

4. arr instanceof Array

但是不推荐使用 instanceof 来判断数组,如果网页中存在多个 iframe,那便会存在多个 Array 构造函数,此时判断是否是数组会存在问题。

image.png

7. typeof 的原理

不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息

000: 对象
010: 浮点数
100:字符串
110: 布尔

8. null和undefined区别

console.log(null == undefined)   // true
console.log(null === undefined)  // false

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义就会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。

在 JS 中可以使用 undefined作为一个变量名,但是这样会影响对 undefined 值的判断。当对这两种类型使用 typeof 进行判断时,null 类型化会返回 “object”,这是一个历史遗留的问题。

9. typeof null 的结果是什么,为什么?

console.log(typeof null)  // Object

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的类型标签(1-3 bits) 以及当前要存储值的真实数据。

000: object   - 当前存储的数据指向一个对象
  1: int      - 当前存储的数据是一个 31 位的有符号整数
100: string   - 当前存储的数据指向一个字符串
110: boolean  - 当前存储的数据是布尔值

由于 null 的值是机器码 NULL 指针(null 指针的值全是 0),也就是说 null 的类型标签也是 000,和Object的类型标签一样,所以会被判定为Object。

10. intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数(右值)的 prototype 属性是否出现在对象(左值)的原型链中的任何位置。

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

11. typeof NaN 的结果是什么?

NaN 指不是一个数字,是一个“警戒值”,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,即 x === x 不成立)的值。

typeof NaN; // "number"

12. 其他值到字符串的转换规则?

1. NullUndefined 类型:null -> "null"undefined -> "undefined"

2. Booleantrue -> "true"false -> "false"

3. Number:直接转换,不过那些极小和极大的数字会使用指数形式

4. 对象,除非自行定义 toString() 方法,否则会调用Object.prototype.toString(),返回值
类似"[object Object]"。如果有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

13. 其他值到数字值的转换规则?

1. Undefined:转换为 NaN

2. Null:转换为 0

3. Booleantrue ->为 1false -> 0

4. String:如同使用 Number() 进行转换,如果包含非数字值则转为 NaN,空字符串为 0

5. 对象(包括数组):-> 基本类型值 -> 强制转换为数字(非数字值)

关于对象(包括数组)被转换为基本类型值:首先会检查该值是否有 valueOf() 方法。如果有并且返回基本类型值;如果没有就使用 toString() 的返回值;如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

14. 其他值到布尔类型的值的转换规则?

以下这些是假值: • undefinednullfalse • +0、-0NaN""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

15. || 和 && 操作符的返回值?

|| 和 && 先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。|| 和 && 会返回它们其中一个操作数的值。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

16. Object.is() 与比较操作符 “===”、“==” 的区别?

1. ==:如果两边的类型不一致,则会进行强制类型转化后再进行比较

2. ===:如果两边的类型不一致时,不会做强制类型准换,直接返回 false

3. Object.is:一般情况下和三等号的判断相同,它处理了一些特殊的情况,
比如 -0 和 +0 不再相等,两个 NaN 是相等的。

17. 什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时, JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。

JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

let a = 'abc'
Object(a) // String {"abc"}

看看如下代码会打印出什么:

let a = new Boolean( false );
if (!a) {
	console.log( "Oops" ); // never runs
}

答案是什么都不会打印,因为虽然包裹的基本类型是 false ,但是 false 被包裹成包装类型后就成了对象,所以其非值为 false ,所以循环体中的内容不会运行。

18. 如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)  // type: number | string

(1)当type为number时规则如下:

  • 调用 obj 的 valueOf 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj 的 toString 方法,后续同上;
  • 抛出 TypeError 异常。

(2)当type为string时规则如下:

  • 调用 obj 的 toString 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj 的 valueOf 方法,后续同上;
  • 抛出 TypeError 异常。

可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。

而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

1. +操作符

+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。

1 + '23' // '123'
1 + false // 1 
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1

2. * /操作符

1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN

3. ==操作符

操作符两边的值都尽量转成number

3 == true // false, 3 转为number为3true转为number为1
'0' == false //true, '0'转为number为0false转为number为0
'0' == 0 // '0'转为number为0

19. 为什么会有BigInt的提案?

JavaScript 中 Number.MAX_SAFE_INTEGER 表示最⼤安全数字,计算结果是 9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,JS 就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了 BigInt 来解决此问题。

20. isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

21. number能表示的整数的最大范围

  • 安全的整数范围:15位数以下
  • 能表示的最大整数是Number.MAX_SAFE_INTEGER(9007199254740991)
  • 能表示的最小整数是Number.MIN_SAFE_INTEGER(-9007199254740991)
  • 很多 ID 是超出这个范围的,所以 ID 最好是用 string
  • 超出会失准
let a = 9007199254740995
console.log(a)  // 9007199254740996

22. 2.toFixed() 会输出什么结果

会报错 Uncaught SyntaxError: Invalid or unexpected token,. 会被看做小数点,正确调用方式 (2).toFixed()

23. 为什么0.1+0.2 ! == 0.3,如何让其相等  

在开发过程中遇到类似这样的问题:

let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004

这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:

(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入

那为什么会出现这样的结果呢?原因如下:

image.png

24. 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined,因此可以用 void 0 来获得 undefined。

image.png

25. set 与 weakSet 区别

Set 对象是值的集合, Set 中的元素是唯一的; WeakSet 对象是一些对象值的集合,与 Set 类似,其中的每个对象值都只能出现一次。区别如下:

image.png

26. 参数传递(值传递和引用传递)

值传递针对基本数据类型,引用传递针对引用数据类型,传递可以理解为复制变量值。 (1)值传递: 传递完后两个变量各不相干

   <script>
        let a = 10;
        function add(num) {
            num += 100;
            return num;
        }
        let ret = add(a);//值传递  num=a
        console.log(a);//10 无变化
        console.log(ret);//110
    </script>

(2)引用传递: 地址传递、两个变量共享堆地址、相互影响

// 第一种情况
let foo = { bar: 1}
const func = obj => {
    obj.bar = 2
    console.log(obj.bar)
}
func(foo) // 2
console.log(foo.bar) // 2

// 第二种情况
let foo = { bar: 1}
const func = obj => {
    obj = 2
    console.log(obj)
}
func(foo) // 2
console.log(foo) // { bar: 1 }

image.png

二、ES6+ 新特性

1. let、const、var的区别

(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升: var存在变量提升,let和const不存在变量提升

(3)给全局添加属性: var声明的变量会被添加为全局对象window的属性,但是let和const不会。

(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️

2. const声明的变量可以修改吗

const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本数据类型(数值、字符串、布尔值),等同于常量,此时是不能修改的。

但对于引用类型的数据(主要是对象和数组),变量指向数据的内存地址(指针),const 只能保证这个指针是不变的,它指向的数据是可以改变的。

3. 如果new一个箭头函数的会怎么样

箭头函数它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

  1. 创建一个对象
  2. 将对象的__proto__属性指向构造函数的prototype属性
  3. 让函数的 this 指向这个对象,执行构造函数的代码
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

4. 箭头函数与普通函数的区别

1)箭头函数比普通函数更加简洁
(2)箭头函数没有自己的this,其this是父级作用域的this3call()、apply()、bind()等方法不能改变箭头函数中this的指向
(4)箭头函数没有prototype
(5)箭头函数不能使用new操作符
(6)箭头函数不能作为构造函数使用
(7)箭头函数没有自己的arguments参数
(8)箭头函数不支持 new.target(普通函数new.target返回构造实例的构造函数)

5. 扩展运算符

(1)对象扩展运算符

对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

上述方法实际上等价于:

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

Object.assign 方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,则后面的属性会覆盖前面的属性。

(2)数组扩展运算符

数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5

数组扩展运算符的应用:

1)合并数组
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five']; // ["one", "two", "three", "four", "five"]2)将字符串转为数组
[...'hello']    // [ "h", "e", "l", "l", "o" ]

当扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组:

function mutiple(...args) {
  let result = 1;
  for (var val of args) {
    result *= val;
  }
  return result;
}
mutiple(1, 2, 3, 4)

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

6. 解构赋值

解构赋值:可以将属性/值从对象/数组中取出,赋值给其他变量。

(1)数组的解构

// 变量声明并且赋值
arr = ['孙','悟','空']
let [d,e,f]= arr
console.log(d,e,f)  // 孙 悟 空

// 默认值
let [g,h,i,j]=[1,2,3]
console.log(g,h,i,j)  // 1 2 3 undefined

// 剩余数组赋值给一个变量
let [n1,n2,...n3]=[1,2,3,4,5,6,7]
console.log(n1,n2,n3)  // 1 2 [3, 4, 5, 6, 7]

// 解构交换变量  
let a1 = 10;
let a2 =20;
[a1,a2]=[a2,a1]

(2)对象的解构

// 基本用法
const obj = {name:'qb',age:19,gender:'男'}
let {age,name,gender} = obj
console.log(name,age,gender)  // qb 19 男

// 给新的变量名赋值
const obj = {name:'qb',age:19,gender:'男'}
let {name:a,age:b,gender:c} = obj
console.log(a,b,c)  // qb 19 男

7. 模板字符串

在 ES6 以前,拼接字符串是很麻烦的事情:

let name = 'css'   
let career = 'coder' 
let hobby = ['coding', 'writing']
let finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]

但是有了模板字符串,拼接难度直线下降:

let name = 'css'   
let career = 'coder' 
let hobby = ['coding', 'writing']
let finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`

这就是模板字符串的第一个优势——允许用${}的方式嵌入变量,模板字符串的关键优势:在模板字符串中,空格、缩进、换行都会被保留;可以在${}里完成一些计算。

所以可以在模板字符串里无障碍地直接写 html 代码:

let list = `
	<ul>
		<li>列表项1</li>
		<li>列表项2</li>
	</ul>
`;
console.log(message); // 正确输出,不存在报错

8. 对 Reflect 的了解?

Reflect(反射) 是一个内置的对象,它提供拦截 js 操作的方法。它是不可构造的,所以不能通过 new 运算符对其进行调用。只要 Proxy 对象具有的代理方法, Reflect 对象全部具有,以静态方法的形式存在。

(1)Reflect 有什么用呢? 它主要提供了很多操作 js 对象的方法,有点像 Object 中操作对象的方法

(2)我们有 Object 可以做这些操作,那么为什么还需要有 Reflect 呢? 这是因为在早期的 ECMA 规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些 API 放到了 Object 上面;但是 Object 作为一个构造函数,这些操作实际上放到它身上并不合适,让 JS 看起来是会有一些奇怪。所以在 ES6 中新增了 Reflect,让我们这些操作都集中到了 Reflect 对象上。

9. Proxy 结合Reflect实现数据劫持

const objProxy = new Proxy(obj,{
    get:function(target,key){
        console.log(`监听到访问${key}属性`,target)
        return Reflect.get(target,key)    //改为Reflect.get
    },
    set:function(target,key,newValue){
        console.log(`监听到给${key}属性设置值`,target)
        Reflect.set(target,key,newValue)    //改为Reflect.set
    }
})
 
console.log(objProxy.name)
 
objProxy.name = 'wx'

用 Reflect 的好处是什么呢?

  • 之前的方式是说到底还是在操作原对象,因为都是在用 target、key 等直接去操作,改用 Reflect 就真正意义上不直接操作原对象。
  • 在有的时候 Reflect 会更加有用。比如:使用 Object.freece(obj) 将对象冻结后,之前的方式就无法判断出设置值到底是设置成功了还是失败了。而 Reflect 可以有返回值,代表是设置成功还是失败。 比如:
const istrue = Reflect.set(target,key,newValue)
const result = istrue?'设置成功':"设置失败"

10. 字符串的新增方法

includes:返回布尔值,表示是否找到了参数字符串。

startsWith:返回布尔值,表示参数字符串是否在原字符串的头部。

endsWith:返回布尔值,表示参数字符串是否在原字符串的尾部。

11. 数组新增的方法

1Array.from():将类数组或者可迭代对象创建为一个新的数组,不改变原数组并返回这个新数组

(2Array.of():创建一个具有可变数量参数的新数组实例
       Array.of(1) // [1]
       Array.of(true, 1, '刘逍') // [true, 1, '刘逍']3)findIndex:根据给定的回调函数,找到匹配的第一个元素的索引,找不到返回-14)find:根据给定的回调函数,找到匹配的第一个元素,找不到返回undefined5)fill:将给定值填充数组

(6)keys:返回一个可迭代的对象,其内容为数组的 key
        const arr = [1, true, '逍']
        const keys = arr.keys()
        for (const i of keys) {
          console.log(i) // 遍历结果 0 1 2
        }

(7)values:返回一个可迭代的对象,其内容为数组的value
        const arr = [1, true, '逍']
        const values = arr.values()
        for (const i of values) {
          console.log(i) // 遍历结果 1 true 逍
        }
        
(8)entries:返回一个可迭代的对象,其内容是一个数组,索引0为原数组的元素,1为原数组该位置的值
        const arr = [1, true, '逍']
        const iterator = arr.entries()
        console.log(Array.from(iterator)) // [ [ 0, 1 ], [ 1, true ], [ 2, '逍' ] ]

12. 对象新增方法

1Object.is():用于比较两个值是否相等,用于解决NaN ≠= NaN,+0 === -0的问题
        console.log(NaN === NaN) // false
        console.log(+0 === -0) // true
        console.log(Object.is(NaN, NaN)) // true
        console.log(Object.is(+0, -0)) // false2Object.assign():将所有可枚举属性的值从一个或多个源对象复制到目标对象,并返回目标对象
        const person = Object.assign({}, { name: '刘逍' }, { age: 18 })
        console.log(person) // { name: '刘逍', age: 18 }3Object.getPrototypeOf():获取原型对象

(4Object.setPrototypeOf():设置原型对象

13. JS中class类的用法

JS里的类就是构造函数的语法糖

基本用法

  • 类里面有个constructor函数,可以接收传递过来的参数,同时返回实例对象
  • constructor函数只要new生成实例时,就会自动调用这个函数,如果我们不写也会自动生成
  • 公共属性放在constructor中,公共方法直接在类里面写函数声明,会自动添加至原型对象中
  • class类没有变量提升,所以必须先定义类,才能通过类实例化对象
  • super()调用父类里的constructor方法,可以向里面传参
  • class里面的方法的this指向的是调用者,如果调用者不是类的实例,就需要改变this的指向
  • 如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承
class Person {
  constructor(age) {
    // 属性
    this.myName = '刘逍'
    this.age = age
  }
  // 静态方法
  static print() {
    console.log()
  }
  // 访问器
  get myName() {
    console.log('getter')
    return this.myName
  }
  set myName(v) {
    console.log('setter' + v)
  }
  setName(v) {
    this.myName = v
  }
}

14. 指数运算符

ES2016中新增指数**,也叫幂运算符,与Math.pow()有着一样的功能:

console.log(2 ** 10 === Math.pow(2, 10)) // true

三、原型与继承

1. 说说面向对象的特点

  • 封装性
  • 继承性
  • 多态性

2. 说说你对工厂模式的理解

工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂

其就像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品

3. 创建对象有哪几种方式?

  1. 字面量的形式直接创建对象

  2. 函数方法

    • 工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。
    • 构造函数模式
    • 原型模式
    • 构造函数模式+原型模式,这是创建自定义类型的最常见方式。
    • 动态原型模式
    • 寄生构造函数模式
  3. class创建

4. Js实现继承的方法

(1)原型链继承

关键: 子类构造函数的原型为父类构造函数的实例对象

缺点:1、子类构造函数无法向父类构造函数传参。

   2、所有的子类实例共享着一个原型对象,一旦原型对象的属性发生改变,所有子类的实例对象都会收影响

   3、如果要给子类的原型上添加方法,必须放在Son.prototype = new Father()语句后面

    function Father(name) {
      this.name = name
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(age) {
      this.age = 20
    }
    // 原型链继承,将子函数的原型绑定到父函数的实例上,子函数可以通过原型链查找到复函数的原型,实现继承
    Son.prototype = new Father()
    // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
    Son.prototype.constructor = Son
    Son.prototype.showAge = function () {
      console.log(this.age);
    }
    let son = new Son(20, '刘逍') // 无法向父构造函数里传参
    // 子类构造函数的实例继承了父类构造函数原型的属性,所以可以访问到父类构造函数原型里的showName方法
    // 子类构造函数的实例继承了父类构造函数的属性,但是无法传参赋值,所以是this.name是undefined
    son.showName() // undefined
    son.showAge()  // 20

(2)借用构造函数继承

关键: 用 .call() 和 .apply()方法,在子类构造函数中,调用父类构造函数

缺点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。

   2、无法实现函数复用,如果父类构造函数里面有一个方法,会导致每一个子类实例上面都有相同的方法。

    function Father(name) {
      this.name = name
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(name, age) {
      Father.call(this, name) // 在Son中借用了Father函数,只继承了父类构造函数的属性,没有继承父类原型的属性。
      // 相当于 this.name = name
      this.age = age
    }
    let s = new Son('刘逍', 20) // 可以给父构造函数传参
    console.log(s.name); // '刘逍'
    console.log(s.showName); // undefined

(3)组合继承

关键: 原型链继承+借用构造函数继承

缺点:1、使用组合继承时,父类构造函数会被调用两次,子类实例对象与子类的原型上会有相同的方法与属性,浪费内存。

    function Father(name) {
      this.name = name
      this.say = function () {
        console.log('hello,world');
      }
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(name, age) {
      Father.call(this, name) //借用构造函数继承
      this.age = age
    }
    // 原型链继承
    Son.prototype = new Father()  // Son实例的原型上,会有同样的属性,父类构造函数相当于调用了两次
    // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
    Son.prototype.constructor = Son
    Son.prototype.showAge = function () {
      console.log(this.age);
    }
    let p = new Son('刘逍', 20) // 可以向父构造函数里传参
    // 也继承了父函数原型上的方法
    console.log(p);
    p.showName() // '刘逍'
    p.showAge()  // 20

(4)原型式继承

关键: 创建一个函数,将要继承的对象通过参数传递给这个函数,最终返回一个对象,它的隐式原型指向传入的对象。(Object.create()方法的底层就是原型式继承)

缺点:只能继承父类函数原型对象上的属性和方法,无法给父类构造函数传参

    function createObj(obj) {
      function F() { }   // 声明一个构造函数
      F.prototype = obj   //将这个构造函数的原型指向传入的对象
      F.prototype.construct = F   // construct属性指回子类构造函数
      return new F        // 返回子类构造函数的实例
    }
    function Father() {
      this.name = '刘逍'
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    const son = createObj(Father.prototype)
    son.showName() // undefined  继承了原型上的方法,但是没有继承构造函数里的name属性

(5)寄生式继承

关键: 在原型式继承的函数里,给继承的对象上添加属性和方法,增强这个对象

缺点:只能继承父类函数原型对象上的属性和方法,无法给父类构造函数传参

    function createObj(obj) {
      function F() { }
      F.prototype = obj
      F.prototype.construct = F
      F.prototype.age = 20  // 给F函数的原型添加属性和方法,增强对象
      F.prototype.showAge = function () {
        console.log(this.age);
      }
      return new F
    }
    function Father() {
      this.name = '刘逍'
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    const son = createObj(Father.prototype)
    son.showName() // undefined
    son.showAge()  // 20

(6)寄生组合继承

关键: 原型式继承 + 构造函数继承

Js最佳的继承方式,只调用了一次父类构造函数

    function Father(name) {
      this.name = name
      this.say = function () {
        console.log('hello,world');
      }
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(name, age) {
      Father.call(this, name)
      this.age = age
    }
    Son.prototype = Object.create(Father.prototype) // Object.create方法返回一个对象,它的隐式原型指向传入的对象。
    Son.prototype.constructor = Son
    const son = new Son('刘逍', 20)
    console.log(son.prototype.name); // 原型上已经没有name属性了,所以这里会报错

(7)混入继承

关键: 利用Object.assign的方法多个父类函数的原型拷贝给子类原型

  function Father(name) {
      this.name = name
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
​
    function Mather(color) {
      this.color = color
    }
    Mather.prototype.showColor = function () {
      console.log(this.color);
    }
​
    function Son(name, color, age) {
      // 调用两个父类函数
      Father.call(this, name)
      Mather.call(this, color)
      this.age = age
    }
    Son.prototype = Object.create(Father.prototype)
    Object.assign(Son.prototype, Mather.prototype)  // 将Mather父类函数的原型拷贝给子类函数
    const son = new Son('刘逍', 'red', 20)
    son.showColor()  // red

(8)class继承

关键: class里的extends和super关键字,继承效果与寄生组合继承一样

    class Father {
      constructor(name) {
        this.name = name
      }
      showName() {
        console.log(this.name);
      }
    }
    class Son extends Father {  // 子类通过extends继承父类
      constructor(name, age) {
        super(name)    // 调用父类里的constructor函数,等同于Father.call(this,name)
        this.age = age
      }
      showAge() {
        console.log(this.age);
      }
    }
    const son = new Son('刘逍', 20)
    son.showName()  // '刘逍'
    son.showAge()   // 20