前端进阶高薪必看-JS篇(上)

2,043 阅读14分钟

思维导图链接(高清无码)

图中1, 2, 3, 4 表示优先级 img

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

JavaScript共有八种数据类型,分别是 UndefinedNullBooleanNumberStringObjectSymbolBigInt, 其中 SymbolBigIntES6+ 中新增的数据类型

Symbol: 代表创建后独一无二不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。

BigInt: 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

栈: 原始数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt) 堆: 引用数据类型(对象、数组和函数)

栈: 占据空间小、大小固定,属于被频繁使用数据; 堆: 占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。

数据类型检测的方式有哪些

typeof

能判断基础类型, 数组、对象、null 都会被判断为 object

typeof 2               // number
typeof true            // boolean
typeof 'str'           // string
typeof undefined       // undefined
typeof function(){}    // function

typeof []              // object    
typeof {}              // object
typeof null            // object

instanceof

可以正确判断对象的类型,不能判断基本数据类型, 其内部运行机制是判断在其原型链中能否找到该类型的原型。

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

constructor

虽然简单复杂数据类型都能判断,但是构造函数可以被手动改变,所以也不是百分之百准确

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

Object.prototype.toString.call()

使用 Object 对象的原型方法 toString 来判断数据类型:

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

同样是检测对象 obj 调用 toString 方法,obj.toString() 的结果和 Object.prototype.toString.call(obj) 的结果不一样,这是为什么?

这是因为 toStringObject 的原型方法,而 Arrayfunction 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString() 不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。

判断数组的方式有哪些

通过 Object.prototype.toString.call() 做判断

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

通过原型链做判断

obj.__proto__ === Array.prototype;

通过 ES6 的 Array.isArray() 做判断

Array.isArray(obj);

通过 instanceof 做判断

obj instanceof Array

通过 Array.prototype.isPrototypeOf

Array.prototype.isPrototypeOf(obj)

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);
  }
}

为什么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为四舍五入

toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。

不等于 0.3 的原因是二进制相加的结果

如何获取安全的 undefined 值?

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

typeof NaN 的结果是什么?

NaN“不是一个数字”,NaN 是一个“警戒值”,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反的值。 NaN !== NaN 为 true

isNaN 和 Number.isNaN 函数的区别?

函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

== 操作符的强制类型转换规则?

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:

  • 首先会判断两者类型是否 相同, 相同的话就比较两者的大小;
  • 类型不相同的话,就会进行类型转换
  • 会先判断是否在对比 nullundefined,是的话就会返回 true
  • 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number
  • 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
  • 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断

其流程图如下:

img

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

NullUndefined 类型 ,null 转换为 "null"undefined 转换为 "undefined"

Boolean 类型,true 转换为 "true"false 转换为 "false"

Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。

Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。

对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString() 来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己toString() 方法,字符串化时就会调用该方法并使用其返回值。

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

Undefined 类型的值转换为 NaN

Null 类型的值转换为 0

Boolean 类型的值,true 转换为 1false 转换为 0

String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0

Symbol 类型的值不能转换为数字,会报错。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作会首先检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值来进行强制类型转换。

如果 valueOf()toString() 均不返回基本类型值,会产生 TypeError 错误。

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

以下这些是假值

  • undefined
  • null
  • +0、-0 和 NaN
  • ""

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

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

||,第一个参数为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。

&&,则相反, true 返回第二个操作数的值,false 返回第一个操作数的值。

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

== 进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。

=== 进行相等判断时,类型不一致时,不会做强制类型准换,直接返回 false

使用 Object.is 来进行相等判断时,一般情况下和 === 的判断相同,它处理了一些特殊的情况,比如 -0+0 不再相等,两个 NaN 是相等的。

什么是 JavaScript 中的包装类型?

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

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

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

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

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

也可以使用 valueOf 方法将包装类型倒转成基本类型:

var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'

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

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

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

JavaScript 中如何进行隐式类型转换?

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

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

type 的值为 number 或者 string

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

可以看出两者的主要区别在于调用 toStringvalueOf 的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则 type 默认为 string
  • 其他情况下,type 默认为 number

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN

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

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

  • +操作符+操作符的两边有至少一个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
  • -、*、\操作符NaN也是一个数字
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
  • 对于**==**操作符 操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
  • 对于**<和>**比较符 如果两边都是字符串,则比较字母表顺序:
'ca' < 'bd' // false
'a' < 'b' // true

其他情况下,转换为数字再比较:

'12' < 13 // true
false > -1 // true

以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:

var a = {}
a > 2 // false

其对比过程如下:

a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果

又比如:

var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"

运算过程如下:

a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"

为什么会有BigInt的提案?

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

object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别

结论: 都为浅拷贝

扩展运算符:

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

Object.assign():

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

let、const、var的区别

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

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

变量提升: var 存在变量提升,letconst 不存在变量提升,即在变量只能在声明之后使用,否在会报错。

给全局添加属性: 浏览器的全局对象是 windowNode 的全局对象是 globalvar 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 letconst 不会。

重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。constlet 不允许重复声明变量。

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

初始值设置: 在变量声明时,varlet 可以不用设置初始值。而 const 声明变量必须设置初始值。

指针指向: letconst都是 ES6+ 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。

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

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

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

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

  • 创建一个对象
  • 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  • 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  • 返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的。

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

(1)箭头函数比普通函数更加简洁

  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号
  • 如果有多个参数,用逗号分割
  • 如果函数体的返回值只有一句,可以省略大括号
  • 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doesNotReturn();

(2)箭头函数没有自己的this

箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。

(3)箭头函数继承来的this指向永远不会改变

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

(5)箭头函数不能作为构造函数使用

构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(6)箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

(7)箭头函数没有prototype

(8)箭头函数不能用作Generator函数,不能使用yeild关键字



完结,撒花 ✿✿ヽ(°▽°)ノ✿



点赞 o( ̄▽ ̄)d 不迷路

收藏 ✋🏻 + 关注

谢谢老铁 ♪(・ω・)ノ