文章目录
1.原始类型有哪几种?null 是对象嘛?
原始类型即为基本数据类型:(6种)
- null
- undefined
- string
- number
- boolean
- symbol
基本数据类型存储的都是值,没有函数可以调用(例undefined.toString()
)
基本数据类型存在的一些bug
- number 为浮点类型
在使用中会遇到某些 Bug,比如 0.1 + 0.2 !== 0.3 - string 类型是不可变的
无论你在 string 类型上调用何种方法,都不会对值有改变。
null 不是对象类型
虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
2. 对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
基本数据类型为栈存储吗,基本数据类型为堆存储
- 基本数据类型可以操作保存在变量中实际的值(按值访问)
- 引用数据类型的值是保存在内存中的对象,实际上是在操作对象的引用而不是实际的对象(按引用访问:当复制保存某个对象的变量时,实际上操作的是对象的引用,但是在为对象添加属性时,操作的是实际的对象)
- 如果对象不被销毁或者属性不被删除,则这个属性将一直保存‘’
- 不能给基本类型的值添加属性(只能给引用类型值动态添加属性)
函数参数的传递(按值传递)
- 基本类型值的传递,如同基本类型的变量复制一样
被传递的值会被赋给一个局部变量,即arguments 对象中一个元素 - 引用类型值的传递,如同引用类型的变量复制一样
会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部
function test(person) {
person.age = 26
person = {
name: 'yyy',
age: 30
}
return person
}
const p1 = {
name: 'yck',
age: 25
}
// console.log(p1)// {name: "yck", age: 25}
// console.log(test(p1)) //{name: "yyy", age: 30}
const p2 = test(p1)
console.log(p1) // yck 26
console.log(p2) // yyy 30
- 首先要知道 p1为一个全局对象,所以当被赋值为26 时,则全局的p1.age也会被改了
- 当为p1 重新分配一个对象时,就会重新建立引用,则得到p2的值
3. typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
typeof 并不能准确判断变量到底是什么类型
- 对于基本数据类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
- typeof 对于对象来说,除了函数都会显示 object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
instanceof 能判断一个对象的正确类型,因为内部机制是通过原型链来判断的
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true string对象
对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的,当然我们还是有办法让 instanceof 判断原始类型的
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
Symbol.hasInstance,是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof ‘hello world’ === ‘string’,所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
4. 类型转换
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
1. 转换为boolean
在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。
2. 对象转基本数据类型
调用内置的[[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是基本数据类型了,就不需要转换了
- 调用
x.valueOf()
和x.toString()
重写 Symbol.toPrimitive ,该方法在转基本数据类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
在这里总结一下对于引用数据类型的toString和valueof 方法的结果:
引用数据类型 | toString | tolocalString | valueof |
---|---|---|---|
Array | 返回数组中每个值的字符串形式拼接成的一个以逗号分隔的字符串 | 创建一个数组值以逗号分隔的字符串 | 返回数组 |
Date | 返回带有时区信息的日期和时间 | 按照与浏览器设置的地区响应的格式返回日期和时间 | 不返回字符串,返回日期的毫秒表示 |
ReqExp | 返回正则表达式的字面量,与创建正则表达式的方式无关 | 返回正则表达式的字面量,与创建正则表达式的方式无关 | 返回正则表达式本身 |
function | 返回函数代码 | 返回函数代码 | 返回函数代码 |
总结一下基本包装类型的toString和valueof 方法的结果:
基本包装类型:
没当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而能够调用一些方法去操作这些数据。
基本包装类型的实例调用typeof 时返回“object”
引用类型和基本包装类型的区别:
对象的生存期
- 使用new 操作符创建的引用类型的实例,在执行流离开当前作用域的之前都一直保存在内存中
- 自动创建的基本包装类型对象,只存在于一行代码的执行瞬间,然后立即被销毁(表明不能在运行时为基本类型值添加属性和方法)
基本包装类型 | toString | tolocalString | valueof |
---|---|---|---|
Boolean | "true" "false" 字符串 | true,false 的基本类型值 | |
Number | 字符串形式的值 | 字符串形式的值 | 数值(基本类型) |
String | 基本字符串值 | 基本字符串值 | 基本字符串值 |
3. 四则运算符
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11' 转换为字符串
true + true // 2
4 + [1,2,3] // "41,2,3" 转换为字符串1,2,3
另外对于加法还需要注意这个表达式'a' + + 'b'
'a' + + 'b' // -> "aNaN"
因为+ 'b'
==NaN 所以结果为aNaN
在一些代码中通过 + ‘1’ 的形式来快速获取 number 类型。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
4. 比较运算符
- 如果是对象,就通过 toPrimitive 转换对象
- 如果是字符串,就通过 unicode 字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
装箱转换(把基本类型转换为对应的对象)
使用内置的object 函数,可以在js 内部代码中显式调用装箱能力
var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
每个对象都有class 属性,这些属性可以通过 Object.prototype.toString 获取
var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
在 JavaScript 中,没有任何方法可以更改私有的 class 属性,因此
Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,比instanceof 更加准确
但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型
拆箱转换(规定ToPrimitive函数,对象类型到基本类型)
对象到String 和 Number 的转换都遵循“先拆箱再转换”的规则,通过拆箱转换,把对象变成基本类型,再从基本类型转换为对于的String 和 Number
拆箱转换会尝试调用valueOf 和toString 来获得拆箱后的基本类型,如果 valueOf 和toString 都不存在,或者没有返回基本类型,则会产生错误 TypeError
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError
可以看到这个对象o 在进行o*2 运算的时候,先执行了了valueOf 然后执行了toString 最后抛出了错误,说明这个拆箱转换失败了。
string的拆箱转换会优先调用toString
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
String(o)
// toString
// valueOf
// TypeError
ES6中还允许对象通过显示指定@@toPrimitive Symbol 来覆盖原有的行为。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello
5. 如何正确判断 this?箭头函数的 this 是什么?
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
- 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
- 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
- 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
new 运算接受一个构造器和一组调用参数,实际上做的事情
- 以构造器的prototype 属性为原型,创建新对象
- 将this 和调用参数传给构造器
- 如果构造器返回的是对象,则返回第一步创建的对象
new 这样的行为,在客观上提供了两种方式:
- 在构造器中添加属性
- 在构造器中的prototype 的属性中添加属性
- 直接在构造器中修改了this ,给this 添加属性:
function c1(){
this.p1 = 1;
this.p2 = function(){
console.log(this.p1);
}
}
var o1 = new c1;
o1.p2();
function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
console.log(this.p1);
}
var o2 = new c2;
o2.p2();
- 修改构造器中的prototype 属性指向的对象,他是从这个构造器构造出来的所有对象的原型:
箭头函数的this
首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window。另外对箭头函数使用 bind 这类函数是无效的
如果对一个函数进行多次 bind,那么上下文会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
可以转换为如下形式:
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window。
let a = { name: 'yck' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'yck'
发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
- new
- bind
- obj.foo()
- foo 的调用方式
同时箭头函数的this 一旦被绑定,就不会被任何方式再改变
6. == 和===有什么区别?
- 对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换
- 对于 === 来说,首先会进行类型判定,如果相同则再进行判定值
假如我们需要对x和y判断是否相同,需要以下流程
- 首先会判断两者类型是否相同。相同的话就是比大小了
- 类型不相同的话,那么就会进行类型转换
- 会先判断是否在对比 null 和 undefined,是的话就会返回 true
- 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
- 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
- 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
对于 [] == ![]
console.log(typeof([]==![]))//boolean
7. 什么是闭包?
有权访问函数作用域变量的函数
function out() {
var num = 1;
return function (n){
return (n + num)
}
}
var a = out()
console.log(a(1)) //2
console.log(out()(2)) //3
function outer() {
var num = 1;
function inner () {
var n = 2;
alert(n + num)
}
return inner;
}
outer()();//3
使用闭包解决 var
定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i) //结果输出5个6
}, i * 1000)
}
因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
- 使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j) // 12345
}, j * 1000)
})(i)
}
- 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j) //逐步输出12345
},
i * 1000,
i
)
}
- 使用let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i) //12345
}, i * 1000)
}
8. 什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
浅拷贝解决引用数据类型的问题(只有一层),深拷贝解决值中还存在对象的问题。
浅拷贝 Object.assign
对于引用数据类型会存在下面的问题(解决办法:浅拷贝)
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
- Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
- 通过展开运算符
...
来实现浅拷贝
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。
深拷贝JSON.parse(JSON.stringify(object))
拷贝值中存在对象时,需要进行深拷贝
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = a
a.jobs.first = 'native'
console.log(b.jobs.first) // native
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
局限性:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
9. 如何理解原型?如何理解原型链?
每个 JS 对象都有 __proto__
属性,这个属性指向了原型,浏览器在早期为了让我们访问到内部属性 [[prototype]]
来实现的一个东西。
原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 obj 来说,可以通过 __proto__
找到一个原型对象,在该对象中定义了很多函数让我们来使用。
在上面的图中我们还可以发现一个 constructor 属性,也就是构造函数原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型
Function.prototype.bind() 就没有这个属性。
原型链就是多个对象通过 __proto__
的方式连接了起来
为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。
总结:
- object 是所有对象的爸爸,所有对象都可以通过
__proto__
找到它 - Function 是所有函数的爸爸,所有函数都可以通过
__proto__
找到它 - 函数的 prototype 是一个对象
- 对象的
__proto__
属性指向原型,__proto__
将对象和原型连接起来组成了原型链 - 原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型