VueBloghyhero6

类型(类型判断,类型转换,源码分析等),函数参数引用,相关面试题目分析等(笔记)

2019-08-06 / 2019-08-06 / 129次浏览

JavaScript 具有七中内置数据类型,它们分别是:
null
undefined
boolean
number
string
object
symbol
其中,前面5种为基本类型。第六种 object 类型又具体包含了 function、array、date 等。

对于这些类型的判断,我们常用的方法有:
typeof
instanceof
Object.prototype.toString
constructor

使用typeof来判断:

typeof 5 // 'number'
typeof 'lucas' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean

但是也存在着一些特例,比如用typeof 判断 null 时:

typeof null // 'object'

我们再看使用typeof 判断复杂类型时的表现:

const foo = () => 1
typeof foo //  'function'

const foo = {}
typeof foo //  'object'

const foo = []
typeof foo //  'object'

const foo = new Date()
typeof foo //  'object'

const foo = Symbol('foo')
typeof foo  //  'symbol'

因此,我们可以总结出:

结论 使用 typeof 可以准确判断出除 null 以外的基本类型,以及 function 类型、 symbol 类型;null 会被 typeof 判断为 object。

使用 instanceof 判断类型
再来看看 instanceof:
使用 a instanceof B 判断的是:a 是否为 B 的实例,即 a 的原型链上是否存在B构造函数。因此如果我们使用:

function Person (name) {
    this.name = name
}
const  p = new Person('lucas')
p instanceof Person  // true

这里 p 是 Person 构造出来的实例。同时,顺着 p 的原型链,也能找到 Object 构造函数:

p._proto_._proto_ == object.prototype

因此:

p instanceof Object // true

另外需要注意的一个细节:

5 instanceof Number // false

返回 false,是因为 5 是基本类型,它并不是 Number 构造函数构造出来的实例对象,如果:

new Number(5) instanceof Number // true

结果返回 true。
我们使用以下代码来模拟 instanceof 原理:

// L 表示左表达式,R 表示右表达式
const instanceofMock = (L, R) => {
    if (type of L !== 'object') {
        return false
    }
    while (true) {
        if (L === null) {
            // 已经遍历到了最顶端
            return false
        }
        if (R.prototype === L.__proto__){
            return true
        }
        L = L.__proto__
    }
}

L 表示左表达式,R表示右表达式,我们可以如此使用:

instanceofMock('', String) // false

function Person(name) {
    this.name = name
}

const p = new Person('lucas')

instanceofMock(p, Person) // true

使用 constructor 和 Object.prototype.toString 判断类型

使用 Object.prototype.toString 判断类型,我们称之为“万能方法”,“终极方法”:

console.log(Object.prototype.toString.call(1)) // [object Number]

console.log(Object.prototype.toString.call('lucas')) // [object String]

console.log(Object.prototype.toString.call('undefined')) // [object Undefined]

console.log(Object.prototype.toString.call(true)) // [object Boolean]

console.log(Object.prototype.toString.call({})) //  [object Object]

console.log(Object.prototype.toString.call([])) // [object Array]

console.log(Object.prototype.toString.call(function(){})) // [object Function]

console.log(Object.prototype.toString.call(null)) // [object Null]

console.log(Object.prototype.toString.call(Symbol('lucas'))) // [object Symbol]

使用 constructor 可以查看目标的构造函数,这也可以进行类型判断,但也存在着问题,具体请看:

var foo = 5
foo.constructor
// f Number() { [native code] }

var foo = 'Lucas'
foo.constructor
// f String() { [native code] }

var foo = true
foo.constructor
// f Boolean() { [native vode] }

var foo = []
foo.constructor
// f Array() { [native code] }

var foo = {}
foo.constructor
// f Object() { [native code] }

var foo = () => 1
foo.constructor
// f function() { [native code] }

var foo = new Date()
foo.constructor
// f Date() { [native code] }

var foo = Symbol('foo')
foo.constructor
// f Symbol() { [native code] }

var foo = undefined
foo.constructor
// VM257:1 Uncaught TypeError: Cannot read property 'constructor' of undefined
    at <anonymous>:1:5

var foo = null
foo.constuctor
// VM334:1 Uncaught TypeError: Cannot read property 'constructor' of null
    at <anonymous>:1:5

我们发现对于undefined 和 null,如果尝试读取其 constructor 属性,将会进行报错。并且 constructor
返回的是构造函数本身,一般使用它来判断类型的情况并不多见。

JavaScript 类型及其转换

JavaScript的一个显著特点就是'灵活'。然而'灵活'的反面就是猝不及防的'坑'多,其中一个典型的例子就是被诟病的类型'隐式转换'。先看一个极端的例子:

(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]] 
// "sb"

这就是'隐式转换'的'成果'。为什么会有这样的输出,这里不过多研究,先从基础入手来进行分析。
MDN 这样介绍过JavaScript 的特点:

JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。

我们再来看一些基本例子,在使用加号进行运算时:

console.log(1 + '1')  // 1

console.log(1 + true)  // 2

console.log(1 + false)  // 1

console.log(1 + undefined) // NaN

console.log('lucas' + true) // lucastrue

我们发现:

结论 当使用 + 运算符计算 string 和其他类型相加时,都会转换为 string 类型;其他情况,都会转换为 number 类型,但是 undefined 会转换为 NaN,相加结果也是 NaN

比如布尔值转换为 number 类型: true 为 1, false 为 0,因此:

console.log(1 + true) // 2

console.log(1 + false) // 1

再看代码:

console.log({} + true)
// [object Object] true

在 + 号俩侧,如果存在复杂类型,比如对象,那么到底是怎样的一套转换规则呢?

结论 当使用 + 运算符计算时,如果存在复杂类型,那么复杂类型将会转换为基本类型,再进行运算

这就涉及到'对象类型转基本类型'这个过程。具体规则:

结论 对象在转换基本类型时,会调用该对象上 valueOf 或 toString 这两个方法,该方法的返回值是转换为基本类型的结果

那具体调用 valueOf 还是 toString 呢? 这是ES 规范所决定的,实际上这取决内置的 toPrimitive 调用结果。主观上来说,这个对象倾向于转换成什么,就会优先调用哪个方法,如果倾向于转换为Number类型,就优先调用 valueOf;如果倾向于转换为String类型,就只调用toString。这里我建议大家了解一些常用的转换结果,对于其他特例情况会查找规范即可。
很多经典"教科书"中,比如《JavaScript 高级程序设计》以及《你不知道的JavaScript》介绍到对象转为基本类型时,会先调用 valueof, 再调用 toString,这里引入了“这个对象倾向于转换成什么,就会优先调用哪个方法”其实取自规范当中的“PreferredType”概念,这个概念在这些书目并没有提到。事实上,浏览器对 PreferredType 的理解比较一致,按照“对象转为基本类型时,会先调用valueof,在调用toString”也没有问题。感兴趣或者更加严谨的读者可以翻阅相关规范的相关内容。

valueOf 以及 toString 是可以被开发者重写的。比如:

const foo = {
    toString () {
        return 'lucas'
    },
    valueOf () {
        return 1
    }
}

我们对 foo 对象的 valueOf 以及 toString 进行了重写, 这时候调用:

alert('foo')

输出:lucas。这里就涉及到"隐式转换",在调用 alert 打印输出时,js 更倾向与转换成toString 也就是更倾向于转变为字符串。
然而:

console.log(1 + foo)

输出:2,这时候的隐式转换“倾向于使用foo对象的valueOf方法,将其转换为基本类型的数字类型”,得以进行相加。

我们再全面总结一下,对于加法操作,如果加号两边都是Number 类型,其规则为:
如果 + 号俩边存在NaN,则结果为NaN(typeof NaN 是 'number')
如果是 Infinity + Infinity ,结果是 Infinity
如果是 -Infinity + (-Infinity) ,结果是 -Infinity
如果是 Infinity + (-Infinity) , 结果是NaN

如果加号俩边有至少一个是字符串,其规则为:
如果 + 号 俩边都是字符串,则执行字符串拼接
如果 + 号俩边只有一个值字符串,则将另外的值转换为字符串,再执行字符串拼接
如果 + 号两边有一个是对象,则调用 valueof() 或者 toString() 方法取的值,转换为基本类型在进行字符串拼接。

对于其他操作符也是类似的。

当然也可以进行显式转换,我们往往使用类似 Number、Boolean、String、parselnt 等方法,进行显式类型转换,这里不再展开。

我们来看看参数传递有什么讲究

let foo = 1
const bar = value => {
    value = 2
    console.log(value)
}
bar(foo)
console.log(foo)

两处输出分别为 2、 1;也就是说在 bar 函数中,参数为基本类型时, 函数体内复制了一份参数值,而且不会影响参数实际值。

let foo = {bar: 1}
const func = obj => {
    obj.bar = 2
    console.log(obj.bar)
}
func(foo)
console.log(foo)

两处输出分别为2、{bar: 2}; 也就是说如果函数参数是一个引用类型,当在函数体内修改这个引用类型的某个参数时,将会对参数进行修改。因为这时候函数体内的引用地址指向了原来的参数

但是如果在函数体内,直接修改对参数的引用,则情况又不一样:

let foo = {bar: 1}
const func = obj => {
    obj = 2
    console.log(obj)
}
func(foo)
console.log(foo)

两处输出分别为2、{bar: 1}; 这样的情况理解起来比较晦涩,其实总结下拉就是:

参数为基本类型时,函数体内复制了一份参数值,对于任何操作不会影响参数实际值
函数参数是一个引用类型时,当在函数体内修改这个值的某个属性值时,将会对参数进行修改
函数参数是一个引用类型时,如果我们直接修改了这个值的引用地址,则相当于函数体内新创建了一份引用,对于任何操作不会影响原参数实际值

cannot read property of undefined 问题解决方案

这里我们分析一个常见的JavaScript细节:cannot read property of undefined 是一个常见的错误,如果意外的得到了一个空对象或者空值,这样恼人的问题在所难免。

考虑这样的一个数据结构:

const obj = {
    user:{
        post:[
            { title: 'Foo', comments: ['Good one!', 'Intering...'] },
            { title: 'Bar', comments: ['Ok'] },
            { title: 'Baz', comments: [] }
        ],
        comments:[]
    }
}

为了在对象中相关取值的过程,需要验证对象每一个 key 的存在性。常见的处理方案有:
&& 短路运算符进行可访问性嗅探

obj.user &&
obj.user.posts &&
obj.user.posts[0] &&    
obj.user.posts[0].comments

|| 单元设置默认保底值

(((obj.user || {}).post || {})[0] || {}).comments

try...catch

var result
try {
    result = obj.user.posts[0].comments
}
catch {
    result = null
}

lodash 等库 get API
当然,我们也可以自己编码:

const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x]: null, o)
console.log(get(['user', 'posts', 0, 'comments'], obj)) // ['Good one!', 'Interesting']

console.log(get(['user', 'post', 0, 'comments'], obj)) // null

我们实现的方法中,接收两个参数,第一个参数表示获取值的路径(path);另外一个参数表示目标对象。
同样,为了设计上的更加灵活和抽象,我们可以 curry 化 方法:

const get = p => o => 
p.reduce((xs, x) => (xs && xs[x])? xs[x]: null, o)

const getUserComments = get(['user', 'posts', 0, 'comments'])
console.log(getUserComments(obj))
// ['Good one!', 'Interesting...']
console.log(getUserComments({user:{posts: []}}))
// null

大佬讲到了,TC39 提案中有一个新提案,支持:

console.log(obj?.user?.post[0]?.comments)

由此可见JavaScript 语言也在不断演进中
综合以上,我们来看一道网红题目
或者说有什么方法能实现

a == 1 && a == 2 && a == 3 可能为true吗?

从直观上分析,如果变量 a 是一个基本 Number 类型, 这是不可能为 true,解题思路从 变量 a 的类型及(对象)转换 (基本类型) 上来考虑。

方案一:

const a = {
    value: 1,
    toString: function () {
        return a.value++
    }
}
console.log(a == 1 && a == 2 && a == 3) // true

每一次 == 都是一次隐式转换,触发了 toString() 这个方法不断进行累计,所以返回的值也就是true 了。

方案二:

大佬居然使用到了属性劫持,vue 的一个核心实现属性就是这个

let value = 0
Object.defineProperty(window, 'a',{
    get: function () {
        return ++value
    }
})
console.log(a == 1 && a == 2 && a ==3) // true

这里我们将a 作为属性,挂载在 window 对象 当中,重写其 getter 方法。
当然,大佬以上俩种方法还不是唯一的。