你盼世界,我盼望你无bug
。Hello 你们好!我是霖呆呆!javascript
几个月前看过一遍三元大佬的《(建议收藏)原生JS灵魂之问, 请问你能接得住几个?》系列,当时是利用上下班公交的时间刷的。说下那时的感觉吧,有些知识点还真不知道,就感受好牛批,确实有一种被灵魂拷问的感受。最最可怕的是那时候尚未意识到本身的基础这么差,只是死记着一些零散的知识点,没几天可能就忘了,总想着何时好好理一下本身的知识体系却一直没有付出行动。html
在某一个点上,多是被某篇文章刺激的,忽然让个人心态发生了很大的改变。那种感受怎么形容呢...就像是我觉得本身都懂,可是我还不知道本身不懂,而后我还每天期盼着明天会更好。有点以为本身是井底之蛙吧。等我真正认清了本身以后才知道了沉淀
这个词的重要性。当我带着'为何会这样?'、'还能够怎样?'、'若是这样会怎样?'
的问题来回顾以前的一些知识,我发现本身要补充的真的还有不少...前端
这篇文章原本是本身近期再读《三元-JS灵魂之问》作的一些笔记,可是发现越记越多...也所以引出了我写的一系列文章,好比JS类型转换系列、JS继承系列、this等等。这里对三元提到的一些题作一些补充说明,使它们变得更适合初中级的小伙伴阅读吧,同时也是对本身这阶段学习的一个巩固,有写的不对的地方还请各位大佬指出。vue
写了一两年的掘金还没破Lv4
,看来我要放大招了。在此立个flag
,升Lv4
后爆女装"呆妹"
,把"她"
放到下篇文章安排一波。已经很卑微了...java
(秉承着对原做者神三元的感谢之情写的,还请三元的19177
位粉丝不要误会呀,我本身也是他的一名小粉丝...)git
全部文章均被收入gitHub「niubility-coding-js」中。github
咱们知道若是在代码中使用:面试
'1'.toString()
// 或者是
true.toString()
复制代码
都是能够正常调用的,这是由于toString
它是Object.prototype
上的方法,任何能访问到Object
原型的元素均可以调用它。数组
而在此处,对于'1'.toString()
至关因而作了一层转换,将其转为了一个"对象"
,这样就能够调用toString()
方法了。浏览器
也就是这样:
var s = new Object('1');
s.toString();
s = null;
复制代码
Object
实例,将s
变为了String{"1"}
对象Object.prototype
上的实例方法toString()
这一部分三元分析的已经挺多了,我主要是想补充一下1.toString()
为何就不行。
当咱们在代码中试图使用1.toString()
,发现编辑器已经报错不容许咱们这样作了。
最开始会有这么奇怪的想法是由于咱们都忽视了一件事,那就是.
它也是属于数字里的一部分啊 😂。
好比1.2
、1.3
。因此当你想要使用1.toString()
的时候,JavaScript
的解释器会把它做为数字的一部分,这样就至关于(1.)toString
了,很显然这是一段错误的代码。
既然这样的话,若是我还给代码一个.
是否是就能够了,因而我尝试了一下:
console.log(1.1.toString())
复制代码
发现它居然能正常打印出来:
"1.1"
复制代码
这也就再次证实了1.toString()
会将.
归给1
所属,而不是归给toString()
。
固然若是你用的一个变量来承载这个数字的话也是能够的:
var num = 1;
console.log(num.toString()) // "1"
// 或者
console.log((1).toString()) // "1"
复制代码
var num = new Number(1) // Number{1}
var str = new String('1') // String{'1'}
var bol = new Boolean(true) // Boolean{true}
var symbol = new Symbol(1) // TypeError
复制代码
向上面这种使用new Number、new String
等建立的基本数据类型被称之为:围绕原始数据类型建立一个显式包装器对象。
通俗点说就是:用new
来建立基本类型的包装类。
而这种作法在ES6
以后就不被支持了,从new Symbol(1)
报错就能够看出来,如今使用的是不带new
的方式:var symbol = Symbol(1)
,因此它做为构造函数来讲是不完整的。
可是由于历史遗留的缘由,new Number
仍然能够这样用,不过并不推荐。
若是你真的想建立一个 Symbol 包装器对象 (Symbol wrapper object
),你可使用 Object()
函数:
var sym = Symbol(1)
console.log(typeof sym) // "symbol"
var symObj = Object(sym)
console.log(typeof symObj) // "object"
复制代码
什么意思呢 🤔️?
正常来讲,instanceof
是用来判断某个对象的原型链上是否可以查找到某个构造函数的原型对象。
来看看通俗点的简介:
a instanceof B
实例对象a instanceof 构造函数B
检测a
的原型链(__proto__)
上是否有B.prototype
,有则返回true
,不然返回false
。
那么它能够用来判断基本数据类型吗?
也就是说我定义了一个var num = 1
,我能够用instanceof
来判断它是一个number
类型的变量吗?
若是你试图这样写:
var num = 1;
console.log(num instanceof Number) // false
复制代码
发现结果是false
。
此时你能够用Symbol.hasInstance
来实现一个自定义instanceof
的行为。
首先想一想咱们是要实现一个什么功能?
a instanceof B
复制代码
左侧的a
是一个变量,而右侧的B
是一个构造函数,而class
的本质也是一个构造函数。
因此在这个需求中,咱们能够定义一个叫作MyNumber
的类,在其里面封装一层,暴露一个静态方法用来判断数据类型:
class MyNumber {
static [Symbol.hasInstance](instance) {
return typeof instance === 'number'
}
}
var num = 1;
console.log(num instanceof MyNumber) // true
复制代码
MyNumber
中定义了一个名为Symbol.hasInstance
的静态方法instance
typeof
判断是不是number
类型这里比较难理解的就是Symbol.hasInstance
了,第一次接触它也不知道它是个啥 😂。找了一波MDN 上对它的介绍:
用于判断某对象是否为某构造器的实例。
而后试着写了几个案例,发现也不用把它想的那么复杂,你就简单理解,当咱们在使用instanceof
的时候,可以自定义右侧构造函数(类)它的instanceof
验证方式就能够了。
就像是上面👆那个案例同样,我在MyNumber
重写了静态方法Symbol.hasInstance
,让它的验证方式变成type instance === 'number'
。
那么为何是静态方法呢(也就是在方法前面加上static
),经过阅读《🔥【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》咱们知道,静态方法是挂载在MyNumber
这个类上的方法,所以咱们甚至能够把下面的内容换一种写法:
console.log(num instanceof MyNumber) // true
// 换成:
console.log(MyNumber[Symbol.hasInstance](num)) // true
复制代码
看到了吧,它其实就是一个方法名而已,而这个方法由于是静态的,在MyNumber
上的,所以咱们能够用MyNumber[Symbol.hasInstance]
这种方式调用。
想一想,若是没有static
这个关键字呢?
没有static
的话,定义在类里的方法就至关因而挂载到类的原型对象上,那么若是咱们想要使用它,一种就是直接用MyNumber.prototype
调用,还有一种就是使用new MyNumber()
生成一个实例来调用:
class MyNumber {
[Symbol.hasInstance](instance) { // 没有 static
return typeof instance === 'number'
}
}
var num = 1
console.log(num instanceof new MyNumber()) // true
console.log(num instanceof MyNumber.prototype) // true
// 转化为:
console.log(MyNumber.prototype[Symbol.hasInstance](num)) // true
console.log(new MyNumber()[Symbol.hasInstance](num)) // true
复制代码
因此如今回过头来看看:
class MyNumber {
static [Symbol.hasInstance](instance) {
return typeof instance === 'number'
}
}
var num = 1;
console.log(num instanceof MyNumber) // true
复制代码
是否是就好理解多了呢?
那么假如我如今想要你实现一个用instanceof
判断是否是数组的类MyArray
,该如何去写呢?
思考🤔...
唔...上答案:
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
复制代码
上面咱们说到了instanceof
是用来判断某个对象的原型链上是否可以查找到某个构造函数的原型对象。
而且是会沿着原型链一层一层的向上查找,直到到达原型链的末位。
那这个过程具体是怎样的呢?让咱们来看一个例子🌰:
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()
console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
console.log(child1 instanceof Object)
复制代码
结果为:
true
true
true
复制代码
这里其实用到了原型链继承,Chind
继承于Parent
,并且三个构造函数的原型对象都存在于child1
的原型链上。
也就是说,左边的child1
它会向它的原型链中不停的查找,看有没有右边那个构造函数的原型对象。
例如child1 instanceof Child
的查找顺序:
child1 -> child1.__proto__ -> Child.prototype
复制代码
child1 instanceof Parent
的查找顺序:
child1 -> child1.__proto__ -> Child.prototype
-> Child.prototype.__proto__ -> Parent.prototype
复制代码
还不理解?
不要紧,我还有大招:
我在上面👆原型链继承的思惟导图上加了三个查找路线。
被⭕️标记的一、二、3
分别表明的是Child、Parent、Object
的原型对象。
好滴,一张图简洁明了。之后再碰到instanceof
这种东西,按照我图上的查找路线来查找就能够了 😁 ~
既然说到了instanceof
,那么就不得不提一下isPrototypeOf
这个方法了。
它属于Object.prototype
上的方法,这点你能够将Object.prototype
打印在控制台中看看。
isPrototypeOf()
的用法和instanceof
相反。
它是用来判断指定对象object1
是否存在于另外一个对象object2
的原型链中,是则返回true
,不然返回false
。
例如仍是上面👆这道题,咱们将要打印的内容改一下:
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()
console.log(Child.prototype.isPrototypeOf(child1))
console.log(Parent.prototype.isPrototypeOf(child1))
console.log(Object.prototype.isPrototypeOf(child1))
复制代码
这里输出的依然是三个true
:
true
true
true
复制代码
判断的方式只要把原型链继承instanceof查找思惟导图这张图反过来查找便可。
更多关于instanceOf
的内容能够戳这里👇:
💦【何不三连】作完这48道题完全弄懂JS继承(1.7w字含辛整理-返璞归真)
+0
和-0
的判断不一样NaN
和NaN
的判断不一样console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
复制代码
其实不须要特地的去记,你只须要理解,Object.is()
在===
的基础上修复了这些特殊状况的失误。
也就是说:+0
和-0
本就该不相等的,这点从1 / +0
为Infinity
,1 / -0
为-Infinity
上能够看出。
(可是+0
和0
是没有区别的)
而NaN
表示的都是非数字,因此应该是相等的。
所以Object.is()
的内部实际上是作了一些修复的处理:
function is (x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
复制代码
这里能够怎样理解呢 🤔️?
x === y
的时候,就是用来处理+0, -0, 0
的特殊状况。在这个判断中,首先会判断出x和y
是否是都是+0, -0, 0
中的其中一个(由于咱们知道不管是这三个中的哪个和0
进行全等比较,结果都会是true
,因此x !== 0
就会是false
),而根据||
的短路原则,前面一项为false
,那么最终的结果取决于后面一项。
因此x !== 0
至关因而把x, y
为+0, -0, 0
的状况推给了1 / x === 1 / y
,用它的结果来决定最后的结果。
(在三元的原文中这段判断是这样写的return x !== 0 || y !== 0 || 1 /x === 1 / y
,比我这里多了一个|| y !== 0
。经评论区小伙伴matteokjh提示,以为能够把|| y !== 0
的判断省略掉,另外MDN上对它的polyfill
也是没有这一步的,我想了一下:由于能进入到x === y
里,那么若是x !== 0
成立的话,那么y !== 0
确定也成立了,貌似是能够省去。另外我把省去的代码执行了一下,和没有省去时的执行结果同样的,因此我感受可行。)
并且咱们又知道1 / +0
为Infinity
,1 / -0
为-Infinity
,因此若是x, y
为+0或者-0
的时候是不相等的,以此来实现Object.is(+0, -0)的结果为false
。
x !== y
的时候,就是用来处理NaN, NaN
的特殊状况。由于咱们知道NaN !== NaN
的,那么若是x
和y
都不和它们本身相等的话,说明两个都是NaN
了,而若是都是NaN
的话,Object.is(NaN, NaN)
的结果为true
。
在此以前,我翻了不少关于toString()
的资料,大多都是介绍了它的用法,可是它真正存在于哪里呢?
可能比较常见的一种说法是它存在于Object
的原型对象中,也就是Object.prototype
上,那么对于基本数据类型,Number、String、Boolean、 Symbol、BigInt
呢?它们自身有这个方法吗?或者它们的原型对象上有吗?
本着一探到底的精神,我打印出了Number
和Number.prototype
:
console.log(Number)
console.log(Number.prototype)
复制代码
而后我发现了几件事:
Number
只是一个构造函数,打印出来显示的会是源代码Number.prototype
上确实也有toString()
Number.prototype.__proto__
也就是Object.prototype
上也有toString()
而后我又试了一下String、Boolean、Symbol
发现结果也和上面同样。
其实不难理解,看过《💦【何不三连】作完这48道题完全弄懂JS继承(1.7w字含辛整理-返璞归真)》的小伙伴都知道,全部对象的原型链到最后都会指向Object.prototype
,算是都"继承"了Object
的对象实例,所以都能使用toString()
方法,可是对于不一样的内置对象为了能实现更适合自身的功能需求,都会重写该方法,因此你能够看到Number.prototype
上也会有该方法。
因此咱们能够先得出第一个结论:
null、undefined
之外的其它数据类型(基本数据类型+引用数据类型),它们构造函数的原型对象上都有toString()
方法toString()
会覆盖Object
原型对象上的toString()
方法(固然,等你看到后面你就会发现这种说法其实并不太准确,可是大多数时候咱们都只是关心谁能够用它,而不是它存在于哪里)
这个问题,其实在上面👆已经给出答案了,全部对象除了null、undefined
之外的任何值均可以调用toString()
方法,一般状况下它的返回结果和String
同样。
其实这里,咱们最容易搞混的就是String
和toString
。
以前老是为了将某个类型转为字符串胡乱的用这两个属性。
String
是一个相似于Function
这样的对象,它既能够当成对象来用,用它上面的静态方法,也能够当成一个构造函数来用,建立一个String
对象toString
它是除了null、undefined
以外的数据类型都有的方法,一般状况下它的返回结果和String
同样。可能你们看的比较多的一种用法是这样的:
Object.prototype.toString.call({ name: 'obj' }) // '[object Object]'
复制代码
先来点硬知识,Object.prototype.toString
这个方法会根据这个对象的[[class]]
内部属性,返回由 "[object " 和 class 和 "]"
三个部分组成的字符串。
啥意思?[[class]]
内部属性是个啥 🤔️?
这里你还真别想多,你就按字面意思来理解它就行了,想一想,class
英文单词的意思->类
。
那好,我就认为它表明的是一类事物就好了。
就好比
[[class]]
是Array
[[class]]
是String
arguments
是一类,它的[[class]]
是Arguments
另外,关于[[class]]
的种类是很是多的,你也不须要记住所有,只须要知道一些经常使用的,基本的,好理解的就能够了。
因此回到Object.prototype.toString.call()
这种调用方式来,如今你能够理解它的做用了吧,它可以帮助咱们准确的判断某个数据类型,也就是辨别出是数组仍是数字仍是函数,仍是NaN
。😊
另外鉴于它的返回结果是"[object Object]"
这样的字符串,并且前面的"[object ]"
这八个字符串都是固定的(包括"t"
后面的空格),因此咱们是否是能够封装一个方法来只拿到"Object"
这样的字符串呢?
很简单,上代码:
function getClass (obj) {
let typeString = Object.prototype.toString.call(obj); // "[object Object]"
return typeString.slice(8, -1);
}
复制代码
能够看到,我给这个函数命名为getClass
,这也就呼应了它本来的做用,是为了拿到对象的[[class]]
内部属性。
另外,在拿到了"[object Object]"
字符串以后,是用了一个.slice(8, -1)
的字符串截取功能,去除了前八个字符"[object ]"
和最后一个"]"
。
如今让咱们来看看一些常见的数据类型吧:
function getClass(obj) {
let typeString = Object.prototype.toString.call(obj); // "[object Array]"
return typeString.slice(8, -1);
}
console.log(getClass(new Date)) // Date
console.log(getClass(new Map)) // Map
console.log(getClass(new Set)) // Set
console.log(getClass(new String)) // String
console.log(getClass(new Number)) // Number
console.log(getClass(true)) // Boolean
console.log(getClass(NaN)) // Number
console.log(getClass(null)) // Null
console.log(getClass(undefined)) // Undefined
console.log(getClass(Symbol(42))) // Symbol
console.log(getClass({})) // Object
console.log(getClass([])) // Array
console.log(getClass(function() {})) // Function
console.log(getClass(document.getElementsByTagName('p'))) // HTMLCollection
console.log(getClass(arguments)) // Arguments
复制代码
"霖呆呆,这么多,这是人干的事吗?"
"性平气和,记住一些经常使用的就好了..."
"啪!"
好滴👌,经过刚刚的学习,咱们了解到了,toString.call
这种方式是为了获取某个变量更加具体的数据类型。
咦~说到数据类型,咱们原来不是有一个typeof
吗?它和toString.call()
又啥区别?
首先帮你们回顾一下typeof
它的显示规则:
number、string
这种),除了null
均可以显示正确的类型null
由于历史版本的缘由被错误的判断为了"object"
object、array
这种),除了函数都会显示为"object"
function
因此呀,typeof
的缺点很明显啊,我如今有一个对象和一个数组,或者一个日期对象,我想要仔细的区分它,用typeof
确定是不能实现的,由于它们获得的都是"object"
。
因此,采用咱们封装的getClass()
显然是一个很好的选择。
在不一样的数据类型调用toString()
会有什么不一样呢?
这里我主要是分为两大块来讲:
对于基本数据类型来调用它,超级简单的,你就想着就是把它的原始值换成了字符串而已:
console.log('1'.toString()) // '1'
console.log(1.1.toString()) // '1.1'
console.log(true.toString()) // 'true'
console.log(Symbol(1).toString()) // 'Symbol(1)'
console.log(10n.toString()) // '10'
复制代码
因此对于基本数据类型:
比较难的部分是引用类型调用toString()
,并且咱们知道引用类型根据[[class]]
的不一样是分了不少类的,好比有Object
、Array
、Date
等等。
那么不一样类之间的toString()
是否也不一样呢 🤔️?
没错,不一样版本的toString
主要是分为:
toString
方法是将每一项转换为字符串而后再用","
链接{name: 'obj'}
这种)转为字符串都会变为"[object Object]"
函数(class)、正则
会被转为源代码字符串日期
会被转为本地时区的日期字符串toString
会返回原始值的字符串Symbol.toStringTag
内置属性的对象在调用时会变为对应的标签"[object Map]"
(Symbol.toStringTag
属性下一题会问到)
例如🌰:
console.log([].toString()) // ""
console.log([1, 2].toString()) // "1,2"
console.log({}.toString()) // "[object Object]"
console.log({name: 'obj'}.toString()) // "[object Object]"
console.log(class A {}.toString()) // "class A {}"
console.log(function () {}.toString()) // "function () {}"
console.log(/(\[|\])/g.toString()) // "/(\[|\])/g1"
console.log(new Date().toString()) // "Fri Mar 27 2020 12:33:16 GMT+0800 (中国标准时间)"
console.log(new Object(true).toString()) // "true"
console.log(new Object(1).toString()) // "1"
console.log(new Object(BigInt(10)).toString()) // "10"
console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
复制代码
《Symbol.toStringTag》上是这样描述它的:
该Symbol.toStringTag
公知的符号是在建立对象的默认字符串描述中使用的字符串值属性。它由该Object.prototype.toString()
方法在内部访问。
看不懂不要紧,你这样理解就能够了,它其实就是决定了刚刚咱们提到全部数据类型中[[class]]
这个内部属性是什么。
好比数字,咱们前面获得的[[class]]
是Number
,那我就能够理解为数字这个类它的Symbol.toStringTag
返回的就是Number
。
只不过在以前咱们用到的Number、String、Boolean
中并无Symbol.toStringTag
这个内置属性,它是在咱们使用toString.call()
调用的时候才将其辨别返回。
而刚刚咱们刚刚在第五问看到的new Map()
,让咱们把它打印出来看看。
console.log(new Map())
复制代码
能够看到Symbol.toStringTag
它是确确实实存在于Map.prototype
上的,也就是说它是Map、Set
内置的一个属性,所以当咱们直接调用toString()
的时候,就会返回"[object Map]"
了。
额,咱们是否是就能够这样理解呢?
Symbol.toStringTag
内置属性的类型在调用toString()
的时候至关因而String(obj)
这样调用转换为相应的字符串Symbol.toStringTag
内置属性的类型在调用toString()
的时候会返回相应的标签(也就是"[object Map]"
这样的字符串)咱们经常使用的带有Symbol.toStringTag
内置属性的对象有:
console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
console.log(Promise.resolve().toString()) // "[object Promise]"
复制代码
而它最主要的功能就是和Symbol.hasInstance
同样,能够容许咱们自定义标签。
(Symbol.hasInsance
的做用是自定义instanceof
的返回值)
什么是自定义标签呢 🤔️?
也就是说,假如咱们如今建立了一个类,而且用toString.call()
调用它的实例对象是会有以下结果:
class Super {}
console.log(Object.prototype.toString.call(new Super())) // "[object Object]"
复制代码
很好理解,由于产生的new Super()
是一个对象嘛,因此打印出的会是"[object Object]"
。
可是如今有了Symbol.toStringTag
以后,咱们能够改后面的"Object"
。
好比我重写一下:
class Super {
get [Symbol.toStringTag] () {
return 'Validator'
}
}
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
复制代码
这就是Symbol.toStringTag
的厉害之处,它可以容许咱们自定义标签。
可是有一点要注意了,Symbol.toStringTag
重写的是new Super()
这个实例对象的标签,而不是重写Super
这个类的标签,也就是说这里有区别的:
class Super {
get [Symbol.toStringTag] () {
return 'Validator'
}
}
console.log(Object.prototype.toString.call(Super)) // "[object Function]"
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
复制代码
由于Super
它自己仍是一个函数,只有Super
产生的实例对象才会用到咱们的自定义标签。
总结一下Symbol.toStringTag
:
Map、Set、Promise
Object.prototype.toString.call()
的返回结果更多关于toString
和Symbol.toStringTag
的内容能够戳这里👇:
【精】从206个console.log()彻底弄懂数据类型转换的前世此生(上)
由于类型转换算是让人比较头疼的一部分,因此对于这一块我也专门写了系列文章,基本上覆盖了面试可能会问到的知识点,传送门:
【精】从206个console.log()彻底弄懂数据类型转换的前世此生(上)
【精】从206个console.log()彻底弄懂数据类型转换的前世此生(下)
(下篇写的挺好的没人看难受😣)
valueOf()
默认是返回它自己1970 年 1 月 1 日以来的毫秒数
(相似于1585370128307
)。例子🌰:
console.log('1'.valueOf()) // '1'
console.log(1.1.valueOf()) // 1.1
console.log([].valueOf()) // []
console.log({}.valueOf()) // {}
console.log(['1'].valueOf()) // ['1']
console.log(function () {}.valueOf()) // ƒ () {}
console.log(/(\[|\])/g.valueOf()) // /(\[|\])/g
console.log(new Date().valueOf()) // 1585370128307
复制代码
当咱们在将对象转换为原始类型或者进行==比较的时候,会调用内置的ToPrimitive
函数。
好比:
console.log(String({})) // 对象转字符串,结果为 "[object Object]"
console.log(Number([1, 2])) // 对象转数字,结果为 NaN
console.log([] == ![]) // true
复制代码
以上结果的由来都通过了ToPrimitive
函数。
先让咱们来看看它的函数语法:
ToPrimitive(input, PreferredType?)
复制代码
参数:
input
,表示要处理的输入值PerferredType
,指望转换的类型,能够看到语法后面有个问号,表示是非必填的。它只有两个可选值,Number
和String
。而它对于传入参数的处理是比较复杂的,让咱们来看看流程图:
根据流程图,咱们得出了这么几个信息:
(总结来源《冴羽-JavaScript深刻之头疼的类型转换(上)》)
上面👆的图其实只是看着很复杂,细心的小伙伴可能会发现,在图里红框裱起来的地方,只有toString()
和valueOf()
方法的执行顺序不一样而已。
若是 PreferredType 是 String 的话,就先执行 toString()
方法
若是 PreferredType 是 Number 的话,就先执行 valueOf()
方法
(霖呆呆建议你先本身在草稿纸上将这幅流程图画一遍)
来点例子巩固一下:
console.log(String({})) // "[object Object]"
复制代码
对于这个简单的转换咱们能够把它换成toPrimitive
的伪代码看看:
toPrimitive({}, 'string')
复制代码
OK👌,来回顾一下刚刚的转换规则:
input
是{}
,是一个引用类型,PerferredType
为string
toString()
方法,也就是{}.toString()
{}.toString()
的结果为"[object Object]"
,是一个字符串,为基本数据类型,而后返回,到此结束。哇~
是否是一切都说得通了,好像不难吧 😁。
没错,当使用String()
方法的时候,JS
引擎内部的执行顺序确实是这样的,不过有一点和刚刚提到的步骤不同,那就是最后返回结果的时候,其实会将最后的基本数据类型再转换为字符串返回。
也就是说上面👆的第三步咱们得拆成两步来:
{}.toString()
的结果为"[object Object]"
,是一个字符串,为基本数据类型"[object Object]"
字符串再作一次字符串的转换而后返回。(由于"[object Object]"
已是字符串了,因此原样返回,这里看不出有什么区别)将最后的结果再转换为字符串返回这一步,其实很好理解啊。你想一想,我调用String
方法那就是为了获得一个字符串啊,你要是给我返回一个number、null
啊什么的,那不是隔壁老王干的事嘛~
刚刚咱们说了对象转字符串也就是toPrimitive(object, 'string')
的状况,
那么对象转数字就是toPrimitive(object, 'number')
。
区别就是转数字会先调用valueOf()
后调用toString()
。
例子🌰:
console.log(Number({}))
console.log(Number([]))
console.log(Number([0]))
console.log(Number([1, 2]))
console.log(Number(new Date()))
复制代码
对于Number({})
:
{}
,所以调用valueOf()
方法,该方法在题7.1
中已经提到过了,它除了日期对象的其它引用类型调用都是返回它自己,因此这里仍是返回了对象{}
valueOf()
返回的值仍是对象,因此继续调用toString()
方法,而{}
调用toString()
的结果为字符串"[object Object]"
,是一个基本数据类型"[object Object]"
转为数字为NaN
,因此结果为NaN
对于Number([])
:
[]
,所以调用valueOf()
方法,返回它自身[]
[]
继续调用toString()
方法,而空数组转为字符串是为""
""
转为数字0
返回对于Number([0])
:
[0]
转为字符串是为"0"
,最后在转为数字0
返回对于Number([1, 2])
:
[1, 2]
,因此调用valueOf()
方法返回的是数组自己[1,2]
toString()
方法,此时被转换为了"1,2"
字符串"1,2"
字符串最后被转为数字为NaN
,因此结果为NaN
对于Number(new Date())
:
new Date()
,所以调用valueOf()
,在题目7.2
中已经说了,日期类型调用valueOf()
是会返回一个毫秒数1585413652137
结果:
console.log(Number({})) // NaN
console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2])) // NaN
console.log(Number(new Date())) // 1585413652137
复制代码
咱们都知道,当数组在进行转字符串的时候,会把里面的每一项都转为字符串而后再进行","
拼接返回。
那么为何会有","
拼接这一步呢?难道toString()
在调用的时候还会调用join()
方法吗?
为了验证个人想法💡,我作了一个实验,重写了一个数组的join()
方法:
var arr = [1, 2]
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr))
复制代码
重写的join
函数中,this
表示的就是调用的这个数组arr
。
而后将返回值改成"~"
拼接,结果答案居然是:
"1~2"
复制代码
也就是说在String(arr)
的过程当中,它确实是隐式调用了join
方法。
可是当咱们重写了toString()
以后,就不会管这个重写的join
了:
var arr = [1, 2]
arr['toString'] = function () {
let target = [...this]
return target.join('*')
}
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr)) // "1*2"
复制代码
能够看出toString()
的优先级仍是比join()
高的。
如今咱们又能够得出一个结论:
对象若是是数组的话,当咱们不重写其toString()
方法,在转换为字符串类型的时候,默认实现就是将调用join()
方法的返回值做为toString()
的返回值。
当咱们在进行对象转原始值的时候,会隐式调用内部的ToPrimitive
方法,按照前面说的那种方式进行转换。
而Symbol.toPrimitive
属性是能帮助咱们重写toPrimitive
,以此来更改转换的结果。
让咱们来看看它的总结:
toString、valueOf、Symbol.toPrimitive
方法,Symbol.toPrimitive
的优先级是最高的Symbol.toPrimitive
函数返回的值不是基础数据类型(也就是原始值),就会报错Symbol.toPrimitive
接收一个字符串参数hint
,它表示要转换到的原始值的预期类型,一共有'number'、'string'、'default'
三种选项String()
调用时,hint
为'string'
;使用Number()
时,hint
为'number'
hint
参数的值从开始调用的时候就已经肯定了来看个例子🌰:
var b = {
toString () {
console.log('toString')
return '1'
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
[Symbol.toPrimitive] (hint) {
console.log('symbol')
if (hint === 'string') {
console.log('string')
return '1'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'default') {
console.log('default')
return 'default'
}
}
}
console.log(String(b))
console.log(Number(b))
复制代码
这道题重写了toString、valueOf、Symbol.toPrimitive
三个属性,经过上面👆的题目咱们已经知道了只要有Symbol.toPrimitive
在,前面两个属性就被忽略了,因此咱们不用管它们。
而对于Symbol.toPrimitive
,我将三种hint
的状况都写上了,若是按照个人设想的话,在调用String(b)
的时候应该是要打印出string
的,调用Number(b)
打印出number
,结果也正如我所预想的同样:
'string'
'1'
'number'
1
复制代码
其实在实际中咱们被考的比较多的可能就是用==
来比较判断两个不一样类型的变量是否相等。
而全等===
的状况比较简单,通常不太会考,由于全等的条件就是:若是类型相等值也相等才认为是全等,并不会涉及到类型转换。
可是==
的状况就相对复杂了,先给你们看几个比较眼熟的题哈:
console.log([] == ![]) // true
console.log({} == true) // false
console.log({} == "[object Object]") // true
复制代码
怎样?这几题是否是常常看到呀 😁,下面就让咱们一个一个来看。
首先,咱们仍是得清楚几个概念,这个是硬性规定的,不看的话咱无法继续下去啊。
当使用==
进行比较的时候,会有如下转换规则(判断规则):
2 == 3
确定是为false
的了null、undefined
,则另外一方必须为null或者undefined
才为true
,也就是null == undefined
为true
或者null == null
为true
,由于undefined
派生于null
String
,是的话则把String
转为Number
再来比较Boolean
,是的话则将Boolean
转为Number
再来比较ToNumber
的转换形式来进行比较(实际上它的hint
是default
,也就是toPrimitive(obj, 'default')
,可是default
的转换规则和number
很像)在一些文章中,会说道:
若是其中一方为Object,且另外一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
(摘自《神三元-(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上)》中的3. == 和 ===有什么区别?
)
这样认为其实也能够,由于想一想toPrimitive(obj, 'number')
的过程:
valueOf()
方法valueOf()
方法的返回值是基本数据类型则直接返回,若不是则继续调用toString()
toString()
的返回值是基本数据类型则返回,不然报错。能够看到,首先是会执行valueOf()
的,可是引用类型执行valueOf()
方法,除了日期类型,其它状况都是返回它自己,也就是说执行完valueOf()
以后,仍是一个引用类型而且是它自己。那么咱们是否是就能够将valueOf()
这一步给省略掉,认为它是直接执行toString()
的,这样作起题来也快了不少。
对于几种经常使用运算符的类型转换:
-、*、/、%
这四种都会把符号两边转成数字来进行运算+
因为不只是数字运算符,仍是字符串的链接符,因此分为两种状况:+b
这种状况至关于转换为数字)对象的+
号类型转换:
+
号字符串链接的时候,toPrimitive
的参数hint
是default
,可是default
的执行顺序和number
同样都是先判断有没有valueOf
,有的话执行valueOf
,而后判断valueof
后的返回值,如果是引用类型则继续执行toString
。(相似题4.5
和4.6
)+
号字符串链接的时候,优先调用toString()
方法。(相似题4.7
)这道题相信你们看的不会少,除了重写valueOf()
你还会哪些解法呢?
解法一:重写valueOf()
这个解法是利用了:当对象在进行==
比较的时候实际是会先执行valueOf()
,如果valueOf()
的返回值是基本数据类型就返回,不然仍是引用类型的话就会继续调用toString()
返回,而后判断toString()
的返回值,如果返回值为基本数据类型就返回,不然就报错。
如今valueOf()
每次返回的是一个数字类型,因此会直接返回。
// 1
var a = {
value: 0,
valueOf () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法二:重写valueOf()
和toString()
var a = {
value: 0,
valueOf () {
return {}
},
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
原理就是解法一的原理,只不过用到了当valueOf()
的返回值是引用类型的时候会继续调用toString()
。
这里你甚至均可以不用重写valueOf()
,由于除了日期对象其它对象在调用valueOf()
的时候都是返回它自己。
也就是说你也能够这样作:
var a = {
value: 0,
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法三:重写Symbol.toPrimitive
想一想是否是还能够用Symbol.toPrimitive
来解呢?
结合题3.10
咱们知道,当对象在进行==
比较的时候,Symbol.toPrimitive
接收到的参数hint
是"defalut"
,那么咱们只须要这样重写:
var a = {
value: 0,
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
return ++this.value
}
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
这样结果也是能够的。
解法四:定义class并重写valueOf()
固然你还能够用class
来写:
class A {
constructor () {
this.value = 0
}
valueOf () {
return ++this.value
}
}
var a = new A()
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法五:利用数组转为字符串会隐式调用join()
什么 ? 还有别的解法吗?并且我看解法五的题目有点没看懂啊。
让咱们回过头去看看题4.3
,那里提到了当数组在进行转字符串的时候,调用toString()
的结果其实就是调用join
的结果。
那和这道题有什么关系?来看看答案:
let a = [1, 2, 3]
a['join'] = function () {
return this.shift()
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
由于咱们知道,对象若是是数组的话,当咱们不重写其toString()
方法,在转换为字符串类型的时候,默认实现就是将调用join()
方法的返回值做为toString()
的返回值。
因此这里咱们重写了a
的join
方法,而此次重写作了两件事情:
a
执行a.shift()
方法,咱们知道这会影响原数组a
的,将第一项去除因此当咱们在执行a == 1
这一步的时候,因为隐式调用了a['join']
方法,因此会执行上面👆说的那两件事情,后面的a == 2
和a == 3
同理。
解法六:定义class继承Array并重写join()
对于解法五咱们一样能够用class
来实现
class A extends Array {
join = this.shift
}
var a = new A(1, 2, 3)
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
这种写法比较酷🆒,可是第一次看可能不太能懂。
A
这个类经过extends
继承于Array
,这样经过new A
建立的就是一个数组A
重写了join
方法,join = this.shift
就至关因而join = function () { return this.shift() }
a == xxx
的时候,都会隐式调用咱们自定义的join
方法,执行和解法五同样的操做。这道题看着和上面那道有点像,不过这里判断的条件是全等的。
咱们知道全等的条件:
false
,这点和==
不一样,==
会发生隐式类型转换而对于上面👆一题的解法咱们都是利用了==
会发生隐式类型转换这一点,显然若是再用它来解决这道题是不能实现的。
想一想当咱们在进行a === xxx
判断的时候,实际上就是调用了a
这个数据而已,也就是说咱们要在调用这个数据以前,作一些事情,来达到咱们的目的。
不知道这样说有没有让你想到些什么 🤔️?或许你和呆呆同样会想到Vue
大名鼎鼎的数据劫持 😁。
想一想在Vue2.x
中不就是利用了Object.defineProperty()
方法从新定义data
中的全部属性,那么在这里咱们一样也能够利用它来劫持a
,修改a
变量的get
属性。
var value = 1;
Object.defineProperty(window, "a", {
get () {
return this.value++;
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
复制代码
这里实际就作了这么几件事情:
Object.defineProperty()
方法劫持全局变量window
上的属性a
a
的时候将value
自增,并返回自增后的值(其实我还想着用Proxy
来进行数据劫持,代理一下window
,将它用new Proxy()
处理一下,可是对于window
对象好像没有效果...)
解法二
怎么办 😂,一碰到这种题我又想到了数组...
var arr = [1, 2, 3];
Object.defineProperty(window, "a", {
get () {
return this.arr.shift()
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
复制代码
中了shift()
的毒...固然,这样也是能够实现的。
解法三
还有就是EnoYao大佬那里看来的骚操做:
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ == 1 && a == 2 && ᅠa == 3) {
console.log("成立");
}
复制代码
说来惭愧...a
的先后隐藏的字符我打不来 😂...
这道有趣的题是从LINGLONG的一篇《【js小知识】[]+ {} =?/{} +[] =?(关于加号的隐式类型转换)》那里看来的。
(PS: pick一波玲珑,这位小姐姐的文章写的都挺好的,不过热度都不高,你们能够支持一下呀 😁)
OK👌,来看看题目是这样的:
在控制台(好比浏览器的控制台)输入:
{}+[]
复制代码
的结果会是什么 🤔️?
咦~这道题应该很简单吧,根据前面的类型转换原则,+
两边都转换为字符串,{}
转为"[object Object]"
,[]
转为""
,拼接的结果就是:
console.log({}+[]) // "[object Object]"
复制代码
可是注意这里的题目,是要在控制台输出哦。
此时我把这段代码在控制台输出结果发现答案居然和预期的不同:
{}+[]
0
复制代码
也就是说{}
被忽略了,直接执行了+[]
,结果为0
。
知道缘由的我眼泪掉了下来,原来它和以前提到的1.toString()
有点像,也是由于JS
对于代码解析的缘由,在控制台或者终端中,JS
会认为大括号{}
开头的是一个空的代码块,这样看着里面没有内容就会忽略它了。
因此只执行了+[]
,将其转换为0
。
若是咱们换个顺序的话就不会有这种问题:
(图片来源:juejin.im/post/5e6055…)
为了证明这一点,咱们能够把{}
当成空对象来调用一些对象的方法,看会有什么效果:
(控制台或者终端)
{}.toString()
复制代码
如今的{}
依旧被认为是代码块而不是一个对象,因此会报错:
Uncaught SyntaxError: Unexpected token '.'
复制代码
解决办法能够用一个()
将它扩起来:
({}).toString
复制代码
不过这东西在实际中用的很少,我能想到的一个就是在项目中(好比我用的vue
),而后定义props
的时候,若是其中一个属性的默认值你是想要定义为一个空对象的话,就会用到:
props: {
target: {
type: Object,
default: () => ({})
}
}
复制代码
对于JS
继承我也写了一个系列「封装|继承|多态」,这里是传送门:
具体的例子还有题目在文章中都已经说的很清楚了,这里我就只列举一下各个继承的优缺点以及伪代码。
伪代码:
Child.prototype = new Parent()
复制代码
思惟导图:
优势:
缺点:
Child.prototype = new Parent()
这样的语句后面伪代码:
function Child () {
Parent.call(this, ...arguments)
}
复制代码
思惟导图:
优势:
缺点:
伪代码:
// 构造继承
function Child () {
Parent.call(this, ...arguments)
}
// 原型链继承
Child.prototype = new Parent()
// 修正constructor
Child.prototype.constructor = Child
复制代码
思惟导图:
实现方式:
优势:
缺点:
伪代码:
// 构造继承
function Child () {
Parent.call(this, ...arguments)
}
// 原型式继承
Child.prototype = Object.create(Parent.prototype)
// 修正constructor
Child.prototype.constructor = Child
复制代码
思惟导图:
寄生组合继承算是ES6
以前一种比较完美的继承方式吧。
它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点。
因此它拥有了上述全部继承方式的优势:
instanceOf
和isPrototypeOf
方法伪代码:
var child = Object.create(parent)
复制代码
实现方式:
该方法的原理是建立一个构造函数,构造函数的原型指向对象,而后调用 new 操做符建立实例,并返回这个实例,本质是一个浅拷贝。
在ES5
以后能够直接使用Object.create()
方法来实现,而在这以前就只能手动实现一个了(如题目6.2
)。
优势:
缺点:
伪代码:
function createAnother (original) {
var clone = Object.create(original);; // 经过调用 Object.create() 函数建立一个新对象
clone.fn = function () {}; // 以某种方式来加强对象
return clone; // 返回这个对象
}
复制代码
实现方式:
优势:
缺点:
伪代码:
function Child () {
Parent.call(this)
OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
复制代码
思惟导图:
伪代码:
class Child extends Parent {
constructor (...args) {
super(...args)
}
}
复制代码
ES6中的继承:
extends
关键字来实现继承,且继承的效果相似于寄生组合继承extends
实现继承不必定要constructor
和super
,由于没有的话会默认产生并调用它们extends
后面接着的目标不必定是class
,只要是个有prototype
属性的函数就能够了ES5继承和ES6继承的区别:
ES5
中的继承(例如构造继承、寄生组合继承) ,实质上是先创造子类的实例对象this
,而后再将父类的属性和方法添加到this
上(使用的是Parent.call(this)
)。ES6
中却不是这样的,它实质是先创造父类的实例对象this
(也就是使用super()
),而后再用子类的构造函数去修改this
。知识无价,支持原创。
参考文章:
你盼世界,我盼望你无bug
。这篇文章就介绍到这里。
因为开篇已经说了太多话了这里就不说了🙊。
喜欢霖呆呆的小伙还但愿能够关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及本身的原创文章🎉
你的鼓励就是我持续创做的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》
《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》
《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》