JS 检测数据类型的 4 种方式

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString.call()

混淆点

  1. typeof

    1
    2
    3
    typeof 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
    22
    const 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
  2. instanceof

    基本语法

    1
    obj1 instanceof obj2; // 判断obj1是否为obj2的实例
    • instanceof 用来判断两个对象是否属于实例关系,通过这种关系来判断对象是否属于某一类型。(但不能判断对象具体属于哪种类型)。
    • instanceof 可以准确判断引用数据类型,原理:检测构造函数的 prototype 属性是否在某个实例对象的原型链上。
    • instanceof 返回值为布尔值
  3. 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 本身。

  4. 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
    11
    Object.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
2
Number(undefined); //NaN
Number(null); //0

特殊点:

1
undefined == null; //true

基本数据类型和引用数据类型的区别?

比较 基本数据类型 引用数据类型
数据存放位置 基本数据类型存放在中,数据大小确定,内存空间大小可以分配 引用数据类型存放在中,每个空间大小不一样,要根据情况进行特定的配置
变量存储内容 变量中存储的是值本身 变量存储的是地址
变量用来赋值时 把变量的复制一份去赋值 把变量的内存地址复制一份去赋值
存储内容大小 存储值较小 存储值较大

typeof(NaN)返回结果?

  • NaN 不是数字的数字类型,所以 typeof(NaN) 返回结果就是 number
  • NaN === NaN 结果为 false,他自己和他自己都不相等
  • 如何判断一个变量的值是 NaN ?
1
2
var a = "我" - "你";
isNaN(a) && typeof a === "number";

以下两种方式的区别?及 typeof 得到的结果

1
2
const str1 = "abc";
const str2 = new String("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
2
3
4
5
6
7
var x = 2;
var y = "2";
(x == y)(
// 返回 true,因为 x 和 y 的值相同
x === y
);
// 返回 false,因为 x 的类型是“数字”,y 的类型是“字符串”

不过要注意以下几个特殊情况的比较

1
2
3
4
NaN === NaN; // false NaN和任何数据都不相等,包括自身
[] == []; // false 比较的是地址
{} == {}; // false 比较的是地址
undefined == null; // true; 特殊情况,记下

对象转字符串,得到的是’[object Object]’

考题 1:以下输出结果

1
console.log([] == false); // true
  • [] 转换成字符串是’’ ,然后’’ 转换成数值是 0
  • false 转换成数值是 0 所以最后比较的值是 0==0 ,结果为 true

考题 2:以下输出结果

1
2
3
if ([]) {
alert("能弹出吗?"); // 可以弹出弹窗
}
  • if 语句中的表达式或值都会被转成 boolean
  • [] 转成布尔值是 true,所以可以弹出。

什么是变量提升 ?

是指使用 var 关键字声明的变量会自动提升到当前作用域的最前面。不过只会提升声明,不会提升其初始化

1
2
3
4
5
6
7
8
9
<script>
console.log(a); // undefined
var a = 10;
function fn() {
console.log(b); // undefined
var b = 1;
}
fn();
</script>

变量只有被声明后,才能使用

  • 我们在 var a=10; 之前 console.log(a); 之所以不会报错,就是因为变 a 的声明被提前到了当前作用域的最顶部
  • 不过只提升了声明,并没会提升初始化,所以打印结果为 undefined; (变量声明初始化,其默认值是 undefined)

上面代码可以看成如下:

1
2
3
4
5
6
7
8
9
10
11
<script>
var a;
console.log(a); // undefined
a = 10;
function fn() {
var b;
console.log(b); // undefined
b = 1;
}
fn();
</script>

补充

  • 函数声明也会被提升,函数和变量相比,会被优先提升。
  • 这意味着函数会被提升到更靠前的位置,如果出现了重名的变量和函数,声明提升时会以函数为主。
1
2
3
4
5
6
7
8
9
<script>
console.log(num); // function num(){console.log('函数');} 函数被优先提升
var num = 1;
console.log(num); // 1 在从上往下执行时num变量赋值为 1
function num() {
console.log("函数");
}
console.log(num()); // 报错,因为变量num被重新赋值为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
2
3
4
5
6
7
8
9
10
<script>
console.log(a); // undefined
var a = 1;
</script>
<script>
console.log(b); // Cannot access 'b' before initialization
let b = 2;
console.log(c); // Cannot access 'c' before initialization
const c = 3;
</script>

② 块级作用域

详细解析

var 不存在块级作用域,let 和 const 存在块级作用域

1
2
3
4
5
6
7
8
9
10
11
12
{
var a = 1;
}
console.log(a); // 1
{
let b = 2;
}
console.log(b); // Uncaught ReferenceError: b is not defined
{
const c = 3;
}
console.log(c); // Uncaught ReferenceError: c is not defined

③ 重复声明

详细解读

var 允许重复声明变量,let 和 const 在同一作用域下不允许重复声明变量

1
2
3
4
5
6
7
8
9
10
11
12
<script>
var a = 1;
var a = 2;
</script>
<script>
let b = 1;
let b = 2; // Uncaught SyntaxError: Identifier 'b' has already been declared
</script>
<script>
const c = 1;
const c = 2; // Uncaught SyntaxError: Identifier 'c' has already been declared
</script>

④ 修改变量

详细解读

  • var 和 let 声明的变量,可以修改
  • const 声明的是一个只读的常量。一旦声明,常量的值就不能改变
1
2
3
4
5
6
7
8
9
10
11
12
<script>
var a = 1;
a = 2;
let b = 2;
b = 3;
console.log(a, b); // 2 3
</script>
<script>
const c = 3;
c = 3;
console.log(c); // ncaught TypeError: Assignment to constant variable.
</script>

⑤ 使用

详细解读

  • 能用 const 的情况尽量使用 const,比如声明的变量是用来保存 对象、数组等引用类型时,用 const
  • 其他情况下大多数使用 let,比如 for 循环,避免使用 var

提示:var 在全局作用域中声明的变量,相当于 window 对象的属性。

const 定义的值一定是不能改变的吗?

  • const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个栈内存地址所保存的数据不得改动。
  • 对于简单类型的数据(数值、字符串、布尔值)值就保存在变量指向的那个栈内存地址,因此等同于常量。
  • 引用类型的数据(主要是对象和数组)变量指向的栈内存地址,保存的只是一个指向实际数据的指针
  • const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

所以如果是 const 声明的是一个引用类型的变量,其引用类型的结构是可以发生改变的。

const 声明了数组,还能 push 元素吗,为什么 ?

可以

  • 因为 const 声明的变量保存的只是栈内存中的地址,只是一个指向实际数据的指针。指针指向堆内存中保存的数据。
  • const 只能保证栈内存中的地址不变,但是堆内存中的数据如何改变是没有办法控制的。
  • push 方法相当于是改变了堆内存中的数据结构。

闭包 、作用域面试题

闭包里面的变量为什么不会被垃圾回收

① 首先我们来了解下什么是垃圾回收 ?

详细解读

在 js 中所谓的垃圾就是指不会再被使用的值,就会被当成垃圾回收掉。

  • javaScript 会自动回收不再使用的变量,释放其所占的内存,开发人员不需要手动的做垃圾回收的处理。

  • 垃圾回收机制只会回收局部变量,全局变量不会被回收,因为全局变量随时有可能被使用。(全局变量在浏览器关闭之后会回收)所以当我们定义了一个全局对象时,在使用完毕之后,最好给它重新复值为 null,以便释放其所占用的内存。

  • 目前浏览器基本使用标记清除引用计数两种垃圾回收策略

    • 标记清理
      • 当函数被调用,变量进入上下文时,会被加上存在上下文标记,是不会被清理的。
      • 当函数执行完成后,就会去掉存在上下文中的标记,随后垃圾回收程序会做一次内存清理,销毁这些变量。
    1
    2
    3
    4
    function fn() {
    var a = 1; // 函数调用时 被标记 进入上下文
    }
    test(); // 函数执行完毕,a的标记去掉,被回收
    • 引用计数
      • 引用计数就是追踪被引用的次数。声明变量并给它赋一个引用类型值时,这个值的引用数 为 1。
      • 如果同一个值又被赋给另一个变量,那引用数+1 。如果保存该值引用的变量被其它值覆 盖了,则引用数减 1。
      • 当引用计数为 0 时,表示这个值不再用到,垃圾收集器就会回收他所占 用的内存。
1
2
3
4
5
6
7
8
<script>
var a = [1, 2, 3]; // [1,2,3]的引用计数为1
var b = a; // 变量b也引用了这个数组,所以[1,2,3]的引用数为2
var a = null; // [1,2,3]的引用被切断,引用数-1,所以[1,2,3]的引用数为1
// 如果只是到这里,那[1,2,3]不所占的内存不会被回收
var b = null; // [1,2,3] 的引用被切断,引用数-1,所 [1,2,3]的引用数为0
// 到这里,垃圾收集器在下一次清理内存时,就会把[1,2,3]所占的内存清理掉
</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
2
3
4
5
6
7
8
9
10
11
<script>
var a = 1;
function fn() {
console.log(a);
}
function test() {
var a = 2;
fn();
}
test(); // 1
</script>

最终输出的结果为 1

说明 fn 中打印的是全局下的 a ,这也印证了 JavaScript 使用了静态作用域。

静态作用域执行过程

当执行 fn 函数时,先从内部查找是否有a变量,如果没有,沿着作用域链往上查找(由于JavaScript是词法作用域),上层为全局a,所以结果打印1

以下代码输出的结果是 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.prototype.a = 10;
var s = Symbol();
var obj = {
[s]: 20,
b: 30
}
Object.defineProperty(obj, 'c', {
enumerable: true
value: 40
})
for(let val in obj) {
console.log(val)
}

以上输出的结果为: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 则没有原型