【面试】易混淆知识点记录
JS 检测数据类型的 4 种方式
- typeof
- instanceof
- constructor
- Object.prototype.toString.call()
混淆点
-
typeof
1
2
3typeof null; //'object'
typeof function () {}; //'function'
typeof Symbol(); //'symbol'- typeof 的返回值类型为字符串类型
- typeof 判断基本数据类型时,除了 null 的输出结果为’object’ 其它类型都能正确判断
- typeof 判断引用数据类型时,除了判断函数会输出’function’ 其它都输出’object’
注意点:
null 的数据类型是 object (null 是对一个空对象的引用,是一个占位符)
- typeof 并不能区分引用数据类型(Array 数组、Date 时间)等
- 所以我们可以使用 instanceof 来判断引用数据类型
symbol()的注意点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');
console.log(typeof symbol1);
// expected output: "symbol"
console.log(symbol2 === 42);
// expected output: false
console.log(symbol3.toString());
// expected output: "Symbol(foo)"
console.log(Symbol('foo'));
// expected output: false
const a = Symbol('foo')
console.log(a === a);
// expected output: true
console.log(Symbol('foo') === Symbol('foo'));
// expected output: false -
instanceof
基本语法:
1
obj1 instanceof obj2; // 判断obj1是否为obj2的实例
- instanceof 用来判断两个对象是否属于实例关系,通过这种关系来判断对象是否属于某一类型。(但不能判断对象具体属于哪种类型)。
- instanceof 可以准确判断引用数据类型,原理:检测构造函数的 prototype 属性是否在某个实例对象的原型链上。
- instanceof 返回值为布尔值
-
constructor
基本语法:
1
2
3
4
5
6
7"".constructor === String; // true
var num = 1;
num.constructor === Number; // true
true.constructor === Boolean; // true
[].constructor === Array; // true
var obj = {};
obj.constructor === Object; // true当一个函数 F 被定义时,JS 引擎会为 F 添加 prototype 原型,然后在 prototype 上添加了一个 constructor 属性,并让其指向 F 的引用
1
2
3
4
5
6
7<script>
function F() {}
const f = new F(); // 实例
console.log(F.prototype.constructor); // function F(){}
console.log(F.prototype.constructor === F); // 构造器指向F
console.log(f.constructor === F);
</script>当执行 const f = new F() 时,F 被当成了构造函数,f 是 F 的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此
f.__proto__.constructor===F
简写成f.constructor === F
从上面的整个过程来看,构造函数 F 就是新对象 f(实例)的类型。所以如果某个实例的 constructor 指向某个构造函数,那这个构造函数就是这个实例的类型。
注意:
- constructor 是不稳定的,因为开发者可以重写 prototype,重写后,原有的 constructor 引用会丢失,需要我们重新指定 constructor 的引用
- 在没有重新指定时,constructor 会默认为 Object
为什么重写 prototype 后,constructor 的默认值会为 Object 呢?
1
2
3
4
5
6<script>
function F() {}
F.prototype = {};
console.log(F.prototype.constructor);
// 结果为 Object() { [native code] }
</script>当
F.prototype={ }
时,{ }
是new Object
的字面量(Object 的实例),所以 Object 原型上的 constructor 会被传递给{ },Object 的 constructor 指向的就是 Object 本身。 -
Object.prototype.toString.call()
详细解读
toString()
是 Object 的原型方法,调用该方法,默认返回当前对象的[object type]
。其中 type 就是对象的类型。Object
对象,直接调用toString()
就能返回 [object Object]- 其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息
1
2
3
4
5
6
7
8
9
10
11Object.prototype.toString.call(""); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(Symbol()); // [object Symbol]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call({}); // [object Object]
Object.prototype.toString.call(document); // [object HTMLDocument]
Object.prototype.toString.call(window); // [object Window]
null 和 undefined 的区别 ?
-
undefined(未定义):当一个变量被定义(声明)但并没有赋值时,他的初始值就是 undefined。
-
null(空)
:表示对一个空对象的引用。
- 当一个变量定好之后,未来是用来保存对象的引用时,我们可以给他赋初始值为 null。
- 当一个对象使用完,需要对其进行释放内存时,可以将其值设置 null (js 会自动垃圾回收)
相同点:
- undefined 和 null 都是基本数据类型,保存栈中。
- undefined 和 null 转换为 boolean 布尔值都为 false
不同点:
两者转换为数字类型时,其值不一样
1 | Number(undefined); //NaN |
特殊点:
1 | undefined == null; //true |
基本数据类型和引用数据类型的区别?
比较 | 基本数据类型 | 引用数据类型 |
---|---|---|
数据存放位置 | 基本数据类型存放在栈中,数据大小确定,内存空间大小可以分配 | 引用数据类型存放在堆中,每个空间大小不一样,要根据情况进行特定的配置 |
变量存储内容 | 变量中存储的是值本身 | 变量存储的是地址 |
变量用来赋值时 | 把变量的值复制一份去赋值 | 把变量的内存地址复制一份去赋值 |
存储内容大小 | 存储值较小 | 存储值较大 |
typeof(NaN)返回结果?
- NaN 不是数字的数字类型,所以
typeof(NaN)
返回结果就是 number - NaN === NaN 结果为 false,他自己和他自己都不相等
- 如何判断一个变量的值是 NaN ?
1 | var a = "我" - "你"; |
以下两种方式的区别?及 typeof 得到的结果
1 | const str1 = "abc"; |
str1 是基本数据类型
- 存储在在栈内存中,用 typeof 检测的结果为 string。
- 当我们把 str1 赋值给别一个变量时,是把 str1 中的值复制一份来赋值。
str2 是引用数据类型
- 存储在堆内存中,不过变量 str2 中存的是指向堆内存中的地址,用 typeof 检测 str2 结果为 Object。
- 当我们把 str2 赋值给另一个变量时,是把 str2 中存入的地址复制一分给到了变量。
typeof 能判断那些类型 ?
typeof 能判断的类型有:string、number、boolean、undefined、symbol、function
- typeof 判断基本数据类型时,除了 null 的输出结果为’object’ 其它类型都能正确判断
- typeof 判断引用数据类型时,除了判断函数会输出’function’ 其它都输出’object’
typeof(null) 为什么返回的是’object’ ?
typeof(null) = object
是 JS 在诞生设计之初留下的历史遗留 BUG 问题- 在 JS 中进行数据底层存储的时候是用二进制存储的,这是一定的,而且它的前三位是代表存储的数据类型,而 000 是代表 object 类型也就是引用类型的数据。
- 而 null 正好全是 0,所以它巧妙的符合 object 类型的存储格式,所以在 typeof 检测的时候,它才会输出 object。
== 和 === 的区别 ?
== 在比较类型不同的变量时,如果两侧的数据类型不同,则会按以下规则进行相应的隐式类型类型转换
- 对象 --> 字符串 --> 数值
- 布尔值 --> 数值
转换后,再比较两边的值是否相等,值相等返回 true,不等返回 false;
=== 在比较时,会比较值和类型两个。只要两边值的类型不相等就返回 false
1 | var x = 2; |
不过要注意以下几个特殊情况的比较
1 | NaN === NaN; // false NaN和任何数据都不相等,包括自身 |
对象转字符串,得到的是’[object Object]’
考题 1:以下输出结果
1 | console.log([] == false); // true |
- [] 转换成字符串是’’ ,然后’’ 转换成数值是 0
- false 转换成数值是 0 所以最后比较的值是 0==0 ,结果为 true
考题 2:以下输出结果
1 | if ([]) { |
- if 语句中的表达式或值都会被转成 boolean
- [] 转成布尔值是 true,所以可以弹出。
什么是变量提升 ?
是指使用 var 关键字声明的变量会自动提升到当前作用域的最前面。不过只会提升声明,不会提升其初始化。
1 | <script> |
变量只有被声明后,才能使用
- 我们在 var a=10; 之前 console.log(a); 之所以不会报错,就是因为变 a 的声明被提前到了当前作用域的最顶部
- 不过只提升了声明,并没会提升初始化,所以打印结果为 undefined; (变量声明初始化,其默认值是 undefined)
上面代码可以看成如下:
1 | <script> |
补充:
- 函数声明也会被提升,函数和变量相比,会被优先提升。
- 这意味着函数会被提升到更靠前的位置,如果出现了重名的变量和函数,声明提升时会以函数为主。
1 | <script> |
const、let、var 区别
const、let、var 三者的区别,我们可以从以下 5 个点来展开对比
- 变量提升和暂时性死区: var 存在变量提升,let 和 const 不存在变量提升,所以 let 和 const 会存在暂时性死区
- 块级作用域: var 不存在块级作用域,let 和 const 存在块级作用域
- 重复声明: var 允许重复声明变量,let 和 const 在同一作用域下不允许重复声明变量
- 修改变量: var 和 let 声明的变量可以修改,const 是不可以的。
- 使用:const 用来声明常量,引用类型值。其它情况推荐用 let ,避免用 var
① 变量提升
详细解读
- var 声明的变量存在变量提升,即变量可以在声明之前被调用。
- let 和 const 不存在变量提升,即它们所声明的变量一定要在声明后使用,否则会报错
1 | <script> |
② 块级作用域
详细解析
var 不存在块级作用域,let 和 const 存在块级作用域
1 | { |
③ 重复声明
详细解读
var 允许重复声明变量,let 和 const 在同一作用域下不允许重复声明变量
1 | <script> |
④ 修改变量
详细解读
- var 和 let 声明的变量,可以修改
- const 声明的是一个只读的常量。一旦声明,常量的值就不能改变
1 | <script> |
⑤ 使用
详细解读
- 能用 const 的情况尽量使用 const,比如声明的变量是用来保存 对象、数组等引用类型时,用 const
- 其他情况下大多数使用 let,比如 for 循环,避免使用 var
提示:var 在全局作用域中声明的变量,相当于 window 对象的属性。
const 定义的值一定是不能改变的吗?
- const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个栈内存地址所保存的数据不得改动。
- 对于简单类型的数据(数值、字符串、布尔值)值就保存在变量指向的那个栈内存地址,因此等同于常量。
- 引用类型的数据(主要是对象和数组)变量指向的栈内存地址,保存的只是一个指向实际数据的指针
- const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
所以如果是 const 声明的是一个引用类型的变量,其引用类型的结构是可以发生改变的。
const 声明了数组,还能 push 元素吗,为什么 ?
可以
- 因为 const 声明的变量保存的只是栈内存中的地址,只是一个指向实际数据的指针。指针指向堆内存中保存的数据。
- const 只能保证栈内存中的地址不变,但是堆内存中的数据如何改变是没有办法控制的。
- push 方法相当于是改变了堆内存中的数据结构。
闭包 、作用域面试题
闭包里面的变量为什么不会被垃圾回收
① 首先我们来了解下什么是垃圾回收 ?
详细解读
在 js 中所谓的垃圾就是指不会再被使用的值,就会被当成垃圾回收掉。
-
javaScript 会自动回收不再使用的变量,释放其所占的内存,开发人员不需要手动的做垃圾回收的处理。
-
垃圾回收机制只会回收局部变量,全局变量不会被回收,因为全局变量随时有可能被使用。(全局变量在浏览器关闭之后会回收)所以当我们定义了一个全局对象时,在使用完毕之后,最好给它重新复值为 null,以便释放其所占用的内存。
-
目前浏览器基本使用标记清除和引用计数两种垃圾回收策略
- 标记清理
- 当函数被调用,变量进入上下文时,会被加上存在上下文标记,是不会被清理的。
- 当函数执行完成后,就会去掉存在上下文中的标记,随后垃圾回收程序会做一次内存清理,销毁这些变量。
1
2
3
4function fn() {
var a = 1; // 函数调用时 被标记 进入上下文
}
test(); // 函数执行完毕,a的标记去掉,被回收- 引用计数
- 引用计数就是追踪值被引用的次数。声明变量并给它赋一个引用类型值时,这个值的引用数 为 1。
- 如果同一个值又被赋给另一个变量,那引用数+1 。如果保存该值引用的变量被其它值覆 盖了,则引用数减 1。
- 当引用计数为 0 时,表示这个值不再用到,垃圾收集器就会回收他所占 用的内存。
- 标记清理
1 | <script> |
引用计数有一个很大的坑,就是循环引用时,会造成内存永远无法释放。
②为什么闭包中的变量不会被垃圾回收 ?
答案解析
这里我们要明确一个点,如果闭包函数的引用计数为 0 时,函数就会释放,它引用的变量也会被释放。
- 只有当闭包函数的引用计数不为 0 时,说明闭包函数随时有可能被调用,他被调用后,就会引用他在定义时所处的环境的变量。
- 闭包中的变量就得一直需要在内存中,则就不会被垃圾回收掉。
说说 JS 作用域及作用域链
-
作用域是一套规则,规定了代码的作用范围。
-
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
-
局部作用域(函数作用域)
- 函数被调用时创建函数作用域,函数执行完毕后,函数作用域被销毁,保存在其中的变量和函数定义了随之被销毁(闭包除外,只有当闭包函数的引用次数为 0 时,闭包函数和闭包中的变量被销毁)
特殊的闭包
1
2
3
4
5
6
7
8
9<script>
function checkWeight(weight) {
return function (_weight) {
weight > _weight ? alert("过胖") : alert("ok达标");
};
}
var P1 = checkWeight(100); // 调用完毕,作用域和变量weight不会被销毁
P1(110); // 调用完毕,作用域和变量_weight会被销毁
</script>- 如果我们在最后加上
P1 = null
,则垃圾回收器回在下一次清理内存时 - 销毁掉 checkWeight 调用形成的作用域和作用域中的变量 weight。
-
使用 let 或 const 关键字声明的变量,会形成块级作用域。
- 在 {}、if 、for 里用 let 来声明变量,会形成块级作用域。{} 之外是不能访问 {} 里面的内容
- 块级作用域中定义的变量,在 if 或 for 等语句执行完后,变量就会被销,不占用内存
- 对象的 { } 不会形成块级作用域
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)
怎么理解 JS 静态作用域和动态作用域
- 静态作用域:又称词法作用域,是指作用域在词法阶段就被确定了(函数定义的位置就决定了函数的作用域)不会改变,javascript 采用的是词法作用域。
- 动态作用域:函数的作用域在函数调用时才决定的。
1 | <script> |
最终输出的结果为 1
说明 fn 中打印的是全局下的 a ,这也印证了 JavaScript 使用了静态作用域。
静态作用域执行过程
当执行 fn 函数时,先从内部查找是否有a
变量,如果没有,沿着作用域链往上查找(由于JavaScript
是词法作用域),上层为全局a
,所以结果打印1
以下代码输出的结果是 ?
1 | Object.prototype.a = 10; |
以上输出的结果为:b,c,a
for...in
语句以任意顺序遍历一个对象的可枚举属性(除 Symbol 类型的属性)
原型、原型链面试题
如何理解原型和原型链
问题 | 相关知识点 |
---|---|
什么是原型和原型特点 | 构造函数、对象实例、prototype、__proto__ 、constructor |
什么是原型链 | 绘制实例对象原型链、数组原型链、原型链的终点 |
原型链查找 | 原型链查找、属性遮蔽 |
重写原型带来的问题 | 切断现有实例与新原型之间的联系、 会导致原型对象的 constructor 属性指向 Object |
Object.create() 方法 | 手写 create 方法、用 create 方法实现继承 |
instanceof 操作符 | 用法和实现原理 |
in 和 hasOwnProperty 操作符 | in 与 hasOwnProperty 的对比、如何判断一个属性是原型上属性 |
for …in | for…in 的用法、可枚举对象的哪些属性? |
扩展知识点:
方法 | 说明 |
---|---|
Object.keys() | Object.keys()方法可以将一个对象作为参数,然后把这个对象[key,value] 对中的 key 值以数组的形式遍历出来。 |
Object.values() | Object.values()方法可以将一个对象作为参数,然后把这个对象[key,value] 对中的 value 值以数组的形式遍历出来。 |
Object.entries() | Object.entries()方法可以将对象作为参数,返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性) |
getOwnPropertyNames() | 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。 |
原型和原型特点
- 我们创建的每一个函数都有一个prototype(原型)属性,被称为显示原型,这个属性是一个指针,指向一个对象(原型对象)。
- 这个对象的好处是,在它上面定义的属性和方法可以由特定类型的所有实例共享。
- 原型对象默认拥有一个 constructor 属性,指向它的构造函数
原型结构图
- 原型对象默认拥有一个 constructor 属性,指向它的构造函数
1 | console.log(Person.prototype.constructor === Person); // true |
- 每个
对象实例
都有一个隐藏的属性__proto__
,被称为隐式原型
,指向它的构造函数的原型
1 | console.log(p1.__proto__ === Person.prototype); // true |
- 对象实例可以共享原型上面的所有属性和方法
1 | p1.sayHello(); // 大家好,我是小明今年23岁了 |
- 实例自身的属性会
屏蔽(遮蔽)
原型上同名的属性,实例上没有的属性就会去原型上去找
1 | p1.study(); // 我小明正在学习web前端开发课程 |
原型链
- JavaScript 中所有的对象都是由它的原型对象继承而来。
- 而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链
原型链结构图
- 所有原型链的终点都是 Object.prototype
Objec.prototype
指向的原型对象同样拥有原型Object.prototype.__proto__,不过它的原型是 null
,而null
则没有原型