1. 数据类型

  • 原始(Primitive)类型
           面试题:原始类型有哪几种?null 是对象吗?

           原始类型种类:boolean、number、string、null、undefined、symbol
           原始类型存储的都是值,没有函数可以调用,true.toString()会强制转换成对象 String 类型
           typof null 输出 object,但是是 JS 的一个 bug,最初设计使用 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象, null 表示全为 0,所以误判为 object。

  • 对象(Object)类型
           面试题:对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?

           原始类型存储的是值,对象类型存储的是地址(指针)。当我们将变量赋值给另一个变量时,复制的是原本变量的地址(指针),所以数据修改时,是修改存放在该地址(指针)上的值,导致两个变量的值都发生了变化。
           函数传参是传递对象指针的副本,形参会被重新分配一个新的地址(指针),就和原本的对象没有关系了,两个变量的值就不同。

  • 类型判断
           面试题:typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?

           typeof 除了 null 之外都可以显示正确的类型,typeof null 为 object,typeof 除了函数的对象,都会显示 object,typeof 函数会显示 function,所以 typeof 不能准确判断变量的类型。
           instanceof 是通过原型链来判断对象的正确类型的,但是不能直接通过 instanceof 来判断原始类型,但是可以通过 Symbol.hasInstance 来改造。

1
2
3
4
5
6
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
  • 类型转换
           原始类型之间转换:转换为布尔值、转换为数字、转换为字符串
    类型转换表

    注:在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。

           对象转原始类型:会调用内置的 [[ToPrimitive]] 函数,该函数会优先调用 valueOf 方法,如果转换为基础类型就返回转换后的值,否则调用 toString 方法,如果两种方法都没有返回原始类型,则抛错。(可以重写 Symbol.toPrimitive 方法,该方法在转原始类型时调用的优先级最高)

           toString()与 valueOf()的差别:

    • toString()将数据转换成相应的字符串形式,参考类型转换表
    • valueOf()返回自身的值,比如数组会返回数组,函数会返回函数

2.四则运算符

  • 加法运算符:
           如果其中一方是字符串,就把另一方也转换为字符串,例:

    1
    1 + '1' -> '11'

           如果一方不是字符串或数字,就将它转换成数字或字符串,例:

    1
    2
    true + true -> 2
    4 + [1,2,3] -> '41,2,3'

           一元加法运算,快速获取 number 类型,例:

    1
    2
    +'1' -> 1
    'a' + + 'b' -> 'aNaN'
  • 其他的运算符:
           只要其中一方是数字,另一方就转为数字

注:如果{}被放在运算符最前面,会当做代码块处理,如果{}被放在运算中,那么他首先会调用 toString 方法

1
2
{} + [] => 0
[] + {} => "[object Object]"

3.比较运算符

       如果是对象,通过 toPrimitive 转换对象,具体调用规则看上面的类型转换
       如果是字符串,通过 unicode 字符索引来比较

4.this

       面试题:如何正确判断 this?箭头函数的 this 是什么?

       箭头函数没有 this,箭头函数中的 this 取决于包裹箭头函数的第一个普通函数的 this,就不能用过构造函数。
       谁调用了函数,谁就是 this。多个 this 同时出现的情况下,this 的优先级:new foo() > foo.bind() > obj.foo() > foo()
单个规则的this

5.== 和 ===

       面试题:==和===有什么区别?

  • == 类型不一样就会进行类型转换,相同就比大小。流程:
  1. 比对双方是否为 null 和 undefined,是则返回 true
  2. 比对双方类型是否为 string 和 number,是则将字符串转为 number,例:
1
1 == '1' -> 1 == 1
  1. 比对一方是否为 boolean,是则把 boolean 转为 number,例:
1
'1' == true -> '1' == 1
  1. 判断一方是否为 object,另一方为 string、number、symbol,是则会 object 转为原始类型,例:
1
'1' == { name:'xx' } -> '1' == '[object Object]'

判定流程

       思考题: [] == ![]

1
2
3
![].valueOf() -> false
[].valueOf() -> []
[] == false -> true
  • ===判断类型和值是否相同。

6. 闭包

       面试题:什么是闭包?如何解决循环中 var 定义函数的问题?

       定义:函数 A 的内部有一个函数 B,函数 B 可以访问函数 A 中的变量,函数 B 就是闭包。函数嵌套函数,返回一个函数,这个解释不完整,看下面的例子:

1
2
3
4
5
6
7
8
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1

       经典面试题:

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}

       输出全是 6 的原因:setTimeout 是个异步函数,所以 for 先全部执行完毕,再到 event loop 中执行 setTimeout,此时 i 已经是 6 了。解决方法:

  • 方法 1:使用闭包,用立即执行函数包裹(关键!在 for 循环中定义了 5 个定时器,本来 5 个定时器会放到队列里等待执行,由于外面是立即执行函数,就会立即执行,并且把 i 传进去了,等到函数执行的时候,向上查找 i,正好在立即执行函数的作用域里找到了 i)
1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
setTimeout((function(i) {
return function() {
console.log(i);
}
})(i), i * 1000);
}
  • 方法 2:使用 setTimeout 的第三个参数,当做定时器函数的参数
1
2
3
4
5
6
7
8
9
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i \* 1000,
i
)
}
  • 方法 3:使用 let 来定义 i

7. 深浅拷贝

       面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?

  • 浅拷贝只拷贝对象的第一层,如果接下去的值还有对象,则需要使用深拷贝。
  • 用 Object.assign 和展开运算符…解决浅拷贝的问题,拷贝所有的属性值到新对象
  • 实现深拷贝:使用 JSON.parse(JSON.stringify(object)),但是会忽略 undefined、symbol,不能序列化函数,也不能解决循环引用的对象,所以推荐使用 lodash 的深拷贝函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}

if (!isObject(obj)) {
throw new Error('非对象')
}

let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : { ...obj }
//Reflect.ownKeys返回所有属性key,Object.keys()返回属性key,但不包括方法属性
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})

return newObj
}

7. 原型

       面试题:如何理解原型?如何理解原型链?

       每个 JS 对象都有proto属性,这个属性实际上是个对象,这个属性指向原型,是为了让我们访问到内部属性 [[prototype]] 。原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型。
       prototype 存在的意义是在 function 作为 constructor 用时(new 或 super)能复制到生成对象的proto上,对于一些内部方法(比如 toString、Math.max)不会作为 constructor,所以没有 prototype 是合理的(同时也没有[[Construct]]内部属性)
       原型链就是多个对象通过proto的方式连接了起来,obj 可以访问 valueOf 就是因为 obj 通过原型链找到 Object,从而找到了 valueOf 函数,一个类型的原型对象等于另一个类型的实例,伪代码 Subtype.prototype = new SuperType()。

1
2
3
4
5
6
7
8
9
__proto__ :{
constructor:{ //constructor指向下面的原型
prototype:{ //原型的constructor指向构造函数
constructor:{ //构造函数又通过prototype指向原型
prototype... //形成原型链
}
}
}
}