JavaScript数据类型细节点: 用简单的方式让你理解概念

前言

一直以来由于浮躁的内心, 导致了我JavaScript基础并不牢靠, 年前决定沉下心来, 把JavaScript所有的重难点搞定, 目前已完成大部分的内容学习. 于是打算通过写文章的形式, 再次复习一边.

本文分享的内容为: JavaScript数据类型的细节点, 我将通过各种脑图面试题作为基准点, 梳理数据类型中遇到的问题. 观看本文可以复习基础内容, 还可以练习各种面试题哦! 我将会在题目放出的下方, 放答案, 方便大家自己练习👇👇

我将把这段时间关于JavaScript学习的笔记上传到个人Github/Learn-Javascript, 欢迎关注.

目录

基本数据类型

Number

Number类型难点在于数据类型的转换以及小数问题, 掌握这两点也就没有太大问题了.

目录:

数据类型转换:
1. 显式转换:

显式转换并非一个难点只是为了隐式转换做一个铺垫, 姑且以看.通过调用Number([val])方法或者在值加一个+号, 可以将其他类型转换为数字类型:

  • String类型 转数字:

    • 数字字符串 则转换为 数字
    • 空字符 转换为 0
    • 其余类型全部转换为 NaN
  • Boolean类型 转数字:

    • false 转换为 0
    • true 转换为 1
  • nullundefined 转数字:

    • null 转换为 0
    • undefined 转换为 NaN
  • SymbolBigInt:

    • Symbol 会直接报错
    • BigInt 正常转换
2. 隐式转换:

隐式转换是数字类型与字符串类型共有的一种转换形式, 面试题最常出现的地方, 需要牢记.

什么时候会出现隐式转换?

当我们做相等性测试(==), 或者对象类型与数字相加时,可能就会出现隐式转换, 可能有同学会疑惑, 对象类型与数字相加一般发生的不都是字符串拼接吗? 其实不是的, 如:

console.log( 10 + new Number(10) ) // 20

上述例子中, 后者是一个非标准特殊对象, 因为在实例对象Number {10}中通过隐式转换得到了对象的原始值10,得以正常相加, 这样的说法可能转牛角尖了, 但是更加严谨.

隐式转换过程:

  1. 访问对象的 [Symbol.toPrimitive] 属性, 如不存在则

  2. 调用对象的 ValueOf() 方法获取对象的原始值, 如不存在则

  3. 调用对象的 toString() 方法转换为字符串, 如需要转换为Number类型

  4. 调用对象的 Number() 处理

因为在 步骤3 中会将对象转换为字符串类型, 这也是为什么很多时候让数字类型去加一个对象, 会莫名变成字符串的原因, 当然还会有些问题, 我将在字符串拼接的时候,详细阐述

console.log(10 + {}); // 10[object Object]
3. 数字类型检测:

isNaN() 用来判断一个值是否为 非有效数字, 它在内部会对值进行类型转换, 同样对于对象类型也遵循隐式转换规则

上题:

请让条件体内的 “ok” 正常输出

var a = {};
if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
} 

答案:

  1. 你可以重新写 [Symbol.toPrimitive] , 也可以是ValueOf(), 随你喜欢~
  var a = {
    i: 0,
    [Symbol.toPrimitive]() {
      return ++this.i
    }
  }
  if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
  }
  1. 通过数据劫持的方式, 这个方法十分有趣, 我会在日后的文章详细阐述
  let i = 0
  Object.defineProperty(window, 'a', {
    get() {
      return ++i
    }
  })
  if (a == 1 && a == 2 && a == 3) {
    console.log('OK')
  }
数字类型的细节点:

parseInt(string, radix):

  1. 将其他类型转换为数字类型, 必须保证[value]是一个字符串,如不是则调用toString()

  2. 第二参数指定的是数字的基数(采用什么进制), 安全值2~36, 该值为0则按照十进制进行解析, 其他的情况直接返回NaN

  3. parseInt(0…) 以0开头,在浏览器输出的时候.会被判定为八进制,此时是浏览器做了转换, 而非方法.(node环境下同理)

  4. parseInt(0x…) 以0x开头,则会默认为十六进制


数字注意点:

  1. 小数计算, 应该避免相等性检查, 这与计算机存储小数的方式有关, 二进制没办法精确的存储0.1这样的小数.
  2. 使用小数的时候, 要记得避免小数的精确度问题.
  3. isFinite() 用于判断一个变量是否为有效数字(非infinity,-infinity,NaN)
  4. NaN 与任何数都不相等(非有效数字指的就是除了有效数字外的任何值, 所以他可以任何值)

题1:

console.log(0.1 + 0.2 == 0.3) // 输出 ? 为什么?

题2:

let arr = [27.2, 0, '0013', '14px', 123]
arr = arr.map(parseInt)
console.log(arr);

题1答案:

什么, 0.3 - 0.2 ≠ 0.1 ? 这篇文章对题目的成因做了详细的阐述,由于篇幅问题, 我就不在这里解释啦

题2答案:

如果你不了解parseInt()与数组的map方法, 这题就没办法做对了.
由于map的特性, 他将会把数组的索引作为参数传递给回调函数, 因此根据下图, 我们就可以得到这样的执行过程

let arr = [27.2, 0, '0013', '14px', 123]
arr = arr.map(parseInt(item, index)) // item 为数组的值, index为索引
console.log(arr);

//-----------------------
arr.map(parseInt(27.2, 0)) // 第一次循环
arr.map(parseInt(0, 1))  // 第二次
arr.map(parseInt('0013', 2)) // 第三次
//....以此类推

String

字符串的细节点在于字符串拼接以及隐式转换规则, 它与转换为数字的隐式转换规则同理, 但没有步骤4

字符串拼接:
  1. 遇到对象类型会遵循隐式转换规则进行转换, 但是空数组[] 会比较特殊, 因为他在步骤3的时候会转换为空字符串"", 所以拼接的时候,不会有任何的效果.

  2. 代码块拼接根据位置结果也将会有所不同:

{} + 10 // {} 会被当做代码块直接执行, 所以不会有任何效果
10 + {} // 10[object Object]
( {} + 10 ) // [object Object]10

总结:
这类题目其实非常简单, 我们只需要记住隐式转换的规则, 以及发生字符串拼接的时候留意引用数据类型, 基本上是不会做错的

let result = true + 21.2 + null + undefined + "2" + [] + null + 9 + false;
console.log(result);

Boolean

  1. 0, null, NaN, undefined, 空字符串(' ') 会被转换为false 其余全是true
  2. ![value] 取反
  3. !![value] 相当于 Boolean([value])

Null与undefined

null和undefined的特性已经在文章中穿插, 所以这里仅对他们本身的相等性测试做一下阐述

  1. 尽量使用三等号进行判断
  2. 字符串==对象时, 会把对象转换为字符串再进行比较
  3. 在其他情况下,都将其他数据类型转换为数字进行比较

nullundefined 与任何值都不相等, 仅在双等号下与彼此相等

NaN == NaN  // => false 因为非有效数字可以是很多种值, 所以不相等
null == undefined // => true 
null === undefined // => false

Symbol

作用:
  1. Symbol类型是唯一值,字面量中使用是给键添加一个[]
  2. Symbol 能够起到唯一标识的作用且创建的属性会被隐藏,即在循环时,该属性将不会被访问到,或者该库内的脚本,被第三方应用的时候,该属性也不会被访问到,不会引起冲突.
  3. 宏观管理标识:保证标志唯一性(vuex/redux)
注意点:

由于在循环中Symbol的属性是不可被访问的, 我们在对一个对象的全部属性进行遍历时, 最严谨的做法应该是要把Symbol属性考虑进去的.此外, 还需要考虑Symbol属性的兼容性问题.

获取一个对象全部属性的正确方式:

  let obj = {
      name: 'link',
      age: 12,
      [Symbol('AA')]: '男'
  }
  let arr = []
  for (item in obj) {
      arr.push(item)
  }
  if(typeof Symbol !== 'undefined') { // 判断当前浏览器是否支持Symbol
      arr = arr.concat(Object.getOwnPropertySymbols(obj))
  }

BigInt

BigInt类型还没有碰到什么面试题, 日后补充!

引用数据类型

Object:

由于对象类型大部分考核内容与数据类型本身无关,如原型, 原理链. 如何进行对象的深浅合并,深浅克隆.我将在其他文章中进行详细阐述.

常用API

如果你还对String,数组,Math的API还不太熟悉, 欢迎关注我的公众号😝前端Link, 回复 “API”, 我将一些常用的API整理成了脑图, 并且做了详细的阐述, 对萌新十分友好,也十分方便记忆

Function(重点)

目录:

函数创建过程

函数使用很常见, 但是我相信仔细研究他的创建和执行过程的人还不多, 但是这有助于我们理解函数的特性.比如构造函数和普通函数在初始化this时会有什么不同?

  1. 开辟一个堆内存, 有一个16进制的内存用于地址指向.

  2. 存储内容: 在代码执行前, 上下文的代码是以代码字符串的形式存储在对应的堆内存中, 就像一个普通对象.

  3. 创建[[scope]]声明作用域,(就是函数创建的执行上下文)

  4. 把地址赋值给某个变量, 供其指向

function name (){
	console.log(1)
}

一个上述的函数, 他的堆就大概长这样.

函数堆0x001
[[scope]]
‘console.log(1)’
name: 'abc', prototype,__proto__
函数执行过程

this指向问题

正常的普通函数执行:看函数执行前是否有“点”,有,“点”前面是谁this就是谁,没有“点”,this是window「严格模式下是undefined」

在什么情况下this指向会被初始化?
  1. 事件绑定

  2. 构造函数

  3. 执行函数(普通函数, 成员访问, 匿名函数, 回调函数)

  4. 箭头函数(generator函数)

  5. 基于call/apply/bind 改变this指向

事件绑定

当按钮被点击后, 函数执行时, (*)(**) 都是指向当前节点对象, (***)而兼容IE678的方法 则指向window对象, 严格模式下指向为undefined
事件绑定其实比较特别, 因为它可以被看成一个"异步任务", 尽管它不存在.去调用函数, 但是在click事件被触发的时候, 其实是由DOM元素执行了这个函数.

<button id="btn">click</button>

// Dom0 :
btn.onclick = function () { 			//  (*)
	console.log(this) // 当前节点
}
// Dom2 :
btn.addEventListener('click',function() {	//  (**)
	console.log(this) // 当前节点
})
btn.attachEvent('click', function () {    	// (***)
	console.log(this) // window/undefined
})
回调函数

回调函数的情况, 很容易把人搞晕, 到其实只要注意函数是否存在调用者, 也就是是否有.就可以判断回调函数的this

var i = 1
function fn(callback) {
  var i = 2
  // callback -> 匿名函数
  callback()
}
fn(function () {
  console.log(this.i) // 1 this指向了window
})

arr.forEach(
  function (item, index) {
    console.log(this) //->forEach第二个参数「对象」  forEach内部做处理了
  },
  { xxx: 'xxx' }
)
函数形参默认值

先看题:

// 请问下列代码执行会输出什么?
var x = 1;
function func(x, y = function anonymous1(){x = 2}){
    var x = 3; 
    y();
    console.log(x); 
}
func(5)
console.log(x);

答案:

var x = 1;
function func(x, y = function anonymous1(){x = 2}){
    var x = 3; 
    y();
    console.log(x);  // => 3
}
func(5)
console.log(x); // => 1

你可能觉得奇怪为什么是3?

作用机制: 如果当前函数使用了形参默认值, 并且函数中有基于let/const/var声明的变量(无论变量名是否与形参一致),则在函数执行时除了生产一个私有的上下文,还会在{}块内生产一个块级上下文(该块级作用域链指向函数的上下文)

注意: (如果函数体中的变量名与形参一致,则最开始会把形参变量的值同步一份给同名的私有变量)

总结: 尽量不要在函数中使用形参默认值, 容易造成各种各样奇奇怪怪的问题, 而且这样做会开辟一个新的块级上下文, 十分占用内存

解决办法:

function foo (x) {
	if (typeof x === 'undefined') {
		...  // 对形参做处理
	}
}

(完)

感谢😘


如果觉得文章内容对你有帮助:

  • ❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
  • 个人公众号: 前端Link
  • 联系作者: linkcyd 😁
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值