行动起来,夯实JS内功基础(一)

308 阅读10分钟

大家好,我是小十三,最近在找下一份工作,所以抽时间对自己一年来工作和学习的过程进行了一些总结和回顾,恰好看到了ConardLi的《一名“合格”的前端工程师的自检清单》,于是我就以此来作为现阶段的目标进行学习和总结了,有错漏的地方,希望大家不吝指正。

JS篇

变量和类型

1.javascript有哪几种数据类型

ECMA定义了7种,分为两大阵营,一个是基本数据类型,一个是引用数据类型

(1)基本数据类型:

  • string--字符串,一个字符串一个值,具有不可变性,存放在栈中,每次的改变都会新创建一个栈内存去存储它
  • boolean--包含两个值true和false
  • number--数字类型,包括浮点数,但是有一定的精度丢失,相关的parseInt等api有科学计数法相关的坑,经典0.1+0.2 !== 0.3
  • undefined-- 空值,指未定义
  • symbol--ES6新出的数据类型,不可更改
  • null--也是空值,但是与undefined不同,它是定义了但未赋值

(2)引用数据类型

  • object--一个人占一个大类,就是那么豪横,无论是Function,Object还是Array都是Object

ES7中新增了1个数据类型为BigInt,最新的Chrome已经支持

2.JavaScript对象的底层数据结构是什么

是堆结构,由于对象的大小是不确定的,所以使用堆结构来存储对象

内存中分为栈内存与堆内存,栈内存存储基本数据类型,堆内存存储引用类型的真实值,

const obj = {'abc'}

这样定义一个对象,首先栈内存开辟一个存储空间用于存储obj所指向的堆内存的地址,而堆内存中存储的值即为'abc',堆内存操作比栈内存操作的效率要低,正是因为这样的存储方式,js的深浅拷贝才是常考的要点

3.Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol

用作唯一的不可变的类型,symbol的唯一性是最高的,即使值完全相同,也都是互相唯一的,类似于数据库中唯一标识的主键ID

Symbol是ES6中新出的一个类型,创建一个Symbol不需要new,直接

const sym1 = Symbol()

即可,创建出来的symbol会被typeof识别为symbol类型,而Symbol会被识别为Function

Symbol用来作为引用类型的唯一属性,换言之它也只有这个存在的价值(一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。)

Symbol的两个特征:

  • 第一点是唯一性,创建出来的Symbol类型将不可变,且在全局范围内唯一,由于Symbol的唯一性,常常也用来防止属性污染,比如要为对象添加一个属性,此时就有可能造成属性覆盖,这个时候如果用Symbol来作为属性的话,绝对不会出现这样的情况
  • 第二点是不可枚举,诸如Object.keys等方法都是不能够获取到Symbol作为键的属性的,所以这个特性可以用来防范XSS攻击,因为JSON无法存储Symbol类型的变量,如果要获取Symbol属性,就需要Object.getOwnPropertySymbols()来专门获取

4.JavaScript中的变量在内存中的具体存储形式

见第二条,原始数据类型都是不可变的,都存储在栈内存中,内存形式是栈的形式,引用类型存储既要用到栈内存,也要用到堆内存,其中栈内存用于存储对象的堆内存地址,堆内存地址对应存储的是对象的实际值,堆内存中的数据都是可变的,和栈内存存储的不同,所以会有深浅拷贝与变量互相赋值后污染的问题存在

5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作

有时候我们会遇到下面的情况

let str = 'abc'
console.log(str.slice(1)) // bc

这看起来并无不妥,但实际上str是通过字面量定义的,从直觉上来思考他应该是个字符串,不应该像对象一样可以调用方法才对,这样的情况就是装箱。

装箱分为两种:

  • 隐式装箱,如同代码所示的,这个装箱的过程会在代码执行的瞬间完成,将let str = 'abc' 变成

    var c = new String('abc')
    c.slice(1)
    c = null
    
  • 显式装箱,通过内置对象Boolean,String与Number来直接对基本类型进行显式装箱

    let a = 1
    let aObj = new Number(a)
    

拆箱就是指把引用类型转变成基本类型

let obj1 = {name: zk}
console.log(obj1 + '1') // [object Object]1
let arr1 = ['1', '2', '3']
console.log(arr1 + 1) // 1,2,31

如同代码所示,将含有name属性的obj1对象直接+字符串'1',他会把对象转为一个[object Object]的字符串后,加上字符串‘1’,而数组arr1就会把最后一个元素拿出来,将1变成字符串后与其相加。

拆箱操作比装箱操作要复杂,首先是会看看对象转为基础类型的对象中是否有valueOf方法,如果有就调用valueOf方法,如果没有就调用Object原型上的toString方法,如果没有toString方法就会报TypeError错误

6.理解值类型和引用类型

除去1,2所提到的值类型与引用类型在内存存储与数据结构相关的区别之外,两个的区别还有:

  • 在比较上面的区别,== 与===是不同的,==情况下会触发值类型的隐式转换,比较的是转换后的值是否相同,===就不会进行隐式转换,即值和基本类型都要一致才能返回true,由于==所引发的隐式转换往往不可控且较为复杂,从逻辑上来讲,应该尽量使用===来进行值的比较

  • 引用类型的比较就比较简单,都是比较栈内存中所存储对象的堆地址,如果指向同一个地址就返回true

  • 值类型无法通过类似对象的形式去添加值,比如

    let a = 1
    a.b = 2
    console.log(a) //1
    

7.nullundefined的区别

null 和undefined都表示空,如果转换成boolean类型都会是false,但是他们本身的意义是不一样的,null代表着已经在内存中开辟了一块空间给该变量,但是该变量尚未赋值,undefined是指这个值没有找到,定义域中没有这个值的定义。

由于历史遗留问题,typeof关键字并不能够准确地得到null的数据类型,typeof null的结果会是object,这是js语言本身的bug

8. 至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

三种判断数据类型的方式:

  • typeof,除了null以外,能够基本判断出来数据类型
  • instanceOf instanceOf运算符会检测实例的原型链上是否存在运算符后面传入的constructor,所以只要实例的原型链存在就会返回true,不存在就返回false。缺点就是对于null和undefined是没法判断的,因为这两个没有原型链,是无效的对象,null本身是作为原型链的最后一个环节。并且instanceOf只能做到判断实例原型链上是否有这个构造函数,但是无法判断是什么具体类型
  • constructor,构造函数,直接通过要检测的值用.引用即可
let b = 1
b.constructor // Number

缺点是无法调用null与undefined的constructor,因为这俩是无效的对象,并且null本身是各种原型链的最后一环或者说是最初始的一环

还有一个细节要注意就是,通过constructor来检测对象类型是基于不自定义对象的类型来说的,如果重写了对象的prototype,那么原来的constructor就丢失了,constructor就默认变为Object

  • toString, toString是有Object构造方法的情况下直接调用,会输出一个字符串为**[object Object]**,后面是大写代表着类型,如果数字类型与字符串类型需要调用这个方法需要使用call或者apply
Object.prototype.toString.call(1)
// "[object Number]"
Object.prototype.toString.call('1')
// "[object String]"
Object.prototype.toString.call(function a(){})
// "[object Function]"

9.可能发生隐式转换的场景以及转换原则,应该如何避免或者巧妙运用

大多数的隐式转换出现在if else条件表达式中,比如null和undefined转换为false,数组下标为0时也会被转换为false这样的细节等等,还有可能出现在运算中,比如加减字符串和数字,当我们在使用+号时尤其特殊,它遵循以下几个原则

  • 当+号两边都是数字的时候,他们会按照数学方式来进行计算
  • 当+号两边有一个是字符串时,两者都会转化为字符串进行相加
  • 当+号两边都不是数字且也不是字符串时,两边都会转化为数字进行相加
  • 不是+号,而是其他类型的运算,-,*,/等,都会转化为数字类型数字

除此之外,还有一项制约着隐式转换,那就是运行环境,比如说浏览器的alert函数

alert(value)

value要求是一个可以输出的值也就是字符串,所以如果传入的是一个对象或是其他类型,就会被转化成字符串类型,这也就是运行环境所产生的影响

10.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

出现小数精度丢失的问题是由于js设计初期,对于补码反码设计的问题

JS的数字类型是双精度浮点型,即java里面的double类型,使用64位的编码形式来存储数字

  • 一位符号位:0正1负
  • 十一位指数为
  • 52位尾数

由于计算过程中会把整数与小数统一转化为二进制进行计算,所以有些小数部分的精度会在转为二进制时会丢失,

这些小数使用二进制将会是一个无限循环的二进制,所以在尾数的限制下会进行截取

最大数字(整数)是(2^53),最大安全数字是(2^53) - 1,其安全范围也是在-(2^53 - 1)到(2^53) - 1

避免精度丢失的方法
  • 将所有的小数都转化为整数进行计算,然后最后除以10的倍数进行处理,因为整数不会丢失精度
  • 使用相关工具类或工具库,其实际也是不走浮点数的计算,而是转化为字符串,自己实现运算逻辑,性能上会变弱但是不会有误差
处理大数字的方法

使用BigInt进行计算,Chrome67以上已经兼容,BigInt已经成为了一个基本类型,使用BigInt不需要new,类似于symbol的定义即可

但是BigInt不可以与Number类型进行混合使用,两者必须转化为同一个类型后进行计算,但是如果计算的是极大的数字,BigInt转为Number可能会有精度丢失