进阶面试题
# 纯函数(pure function)
函数给予相同的输入总是返回相同的输出
# immutable
减少更改变量的机会,减少负作用
const、slice(immutable function)
# mutable
let、splice(mutable function)
# js继承
# 属性继承
# 继承方法
Son.prototype = new Father()
Son.prototype.constructor = Son
# 寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这个例子的高效率体现在它只调用了一次SuperType
构造函数,并且因此避免了在SubType.prototype
上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
# 混入方式继承多个对象
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do something
};
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object.assign
会把 OtherSuperClass
原型上的函数拷贝到 MyClass
原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
# ES6类继承extends
extends
关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor
表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError
错误,如果没有显式指定构造方法,则会添加默认的 constructor
方法,使用例子如下。
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width;
}
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200
-----------------------------------------------------------------
// 继承
class Square extends Rectangle {
constructor(length) {
super(length, length);
// 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'Square';
}
get area() {
return this.height * this.width;
}
}
const square = new Square(10);
console.log(square.area);
// 输出 100
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
extends
继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 总结
1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。
let p = new Rectangle();
// ReferenceError
class Rectangle {}
复制代码
2
3
4
5
2、ES5继承和ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
# 函数提升高于变量提升
首先, js有变量提升和函数提升,指的是用 var声明变量 或用 function 函数名(){ } 声明的,会在 js预解析 阶段提升到顶端;(es6的let 和 const 不会提升)
● 其次,函数提升优先级 高于 变量提升
console.log(foo);
var foo = 1 //变量提升
console.log(foo)
foo()
function foo(){
//函数提升
console.log('函数')
}
2
3
4
5
6
7
8
等价于
function foo(){ //提到顶端
console.log('函数')
}
var foo
console.log(foo)
//输出foo这个函数,因为上面foo没有被赋值,foo还是原来的值
foo = 1;
//赋值不会提升,赋值后 foo就不再是函数类型了,而是number类型
console.log(foo) //输出1
foo() //这里会报错,因为foo不是函数了
2
3
4
5
6
7
8
9
10
# 全局函数
js的全局属性:Infinity、NAN、undefined
JavaScript 中包含以下 7 个全局函数,用于完成一些常用的功能(以后的章节中可能会用到):
escape( )、eval_r( )、isFinite( )、isNaN( )、parseFloat( )、parseInt( )、unescape( )。
# 立即执行函数的this
指向window
# 进程和线程的关系,以及死锁是什么
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
- 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
- 页面渲染,脚本执行,事件处理等
强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)
当然,浏览器有时会将多个进程合并(譬如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程)
# WebWorker
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,perfect!
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
# 死锁
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
环路等待条件指都是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图
个人整理了一些资料,有需要的朋友可以直接点击领取。
# 避免死锁问题的发生
资源有序分配法
我们先要清楚线程 A 获取资源的顺序,它是先获取互斥锁 A,然后获取互斥锁 B。
所以我们只需将线程 B 改成以相同顺序地获取资源,就可以打破死锁了。
# 总结
简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。
所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。
# for循环
逗号表达式只有最后一项是有效的,即对于i<10,j<6; 来说,判断循环是否结束的是 j < 6;而对于 j<6,i<10; 来说,判断循环是否结束的是 i < 10。
var k = ``0``;``for``(var i=``0``,j=``0``;i<``10``,j<``6``;i++,j++){`` ``k += i + j;``}``console.log(k) ``// 打印结果为 30
var k = 0;
for(var i=0,j=0;i<10,j<6;i++,j++){
k += i + j;
}
console.log(k) // 打印结果为 30
var k = 0;
for(var i=0,j=0;j<6,i<10;i++,j++){
k += i + j;
}
console.log(k) //打印结果为 90
2
3
4
5
6
7
8
9
10
# 如何设置0.5px的边
.hr.scale-half {
height: 1px;
transform: scaleY(0.5);
transform-origin: 50% 100%;
}
2
3
4
5
.hr.scale-half {
box-shadow:0 0.5px 0 #000;
height: 1px;
}
2
3
4
# 画体型
<style>
.trapezoid {
width: 50px;
height: 50px;
background: #ff0;
border-top: 50px solid #f00;
border-bottom: 50px solid #00f;
border-left: 50px solid #0f0;
border-right: 50px solid #0ff;
}
<div class="trapezoid"></div>
</style>
2
3
4
5
6
7
8
9
10
11
12
# 画椭圆
高度设置为宽度的一半
.container{
width: 100px;
height: 50px;
border-radius: 50px/25px;
background-color: #f00;
}
2
3
4
5
6
# 画1/4圆
.sector1 {
border-radius:100px 0 0;
width: 100px;
height: 100px;
background: #00f;
}
<div class="sector1"></div>
2
3
4
5
6
7
# 画扇形
<style>
.sector2 {
border: 100px solid transparent;
width: 0;
border-radius: 100px;
border-top-color: #f00;
}
<div class="sector2"></div>
</style>
2
3
4
5
6
7
8
9
# 画箭头
<style>
.arrow{
width: 0;
height: 0;
border: 50px solid;
border-color: transparent #0f0 transparent transparent;
position: relative;
}
.arrow::after{
content: '';
position: absolute;
/* right: -55px;
top: -50px;*/
transform:translate(-45%,-50%);
border: 50px solid;
border-color: transparent #fff transparent transparent;
}
<div class="arrow"></div>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# flex属性
- 弹性盒布局,CSS3 的新属性,用于方便布局,比如垂直居中
- flex属性是
flex-grow
、flex-shrink
和flex-basis
的简写,默认为0 1 auto,分别表示放大、缩小以及自身内容决定 - flex:1改变的是
flex-grow
为1,并且auto为任意值即,flex:1===flex:1 1 除了auto任意值
# div 垂直水平居中
并完成 div 高度永远是宽度的一半(宽度可以不指定)
实现方式:padding-bottom: 50%;继承父元素宽度的一半,不要设置height
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}
.outer {
width: 400px;
height: 100%;
background: blue;
margin: 0 auto;
display: flex;
align-items: center;
}
.inner {
position: relative;
width: 100%;
height: 0;
padding-bottom: 50%;
background: red;
}
.box {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner">
<div class="box">hello</div>
</div>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# IFC
行内格式化上下文,vertical:middle;text-align:center;
# IFC一般有什么用呢?
水平居中:当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
# TDZ(暂时性死区)
如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。 总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
# typeof的“死区”陷阱
我们都知道使用typeof 可以用来检测给定变量的数据类型,也可以使用它来判断值是否被定义。当返回undefined时表示值未定义; 但是在const/let定义的变量在变量声明之前如果使用了typeof就会报错
typeof x; // ReferenceError
let x;
复制代码
2
3
因为x是使用let声明的,那么在x未声明之前都属于暂时性死区,在使用typeof时就会报错。所以在使用let/const进行声明的变量在使用typeof时不一定安全喔。
typeof y; // 'undefined'
复制代码
2
但是可以看出,如果我们只是用了typeof而没有去定义,也是不会报错的,从这粒可以看出只要没有明确规定使用const/let定义之前就是不会出错。
# 传参的“死区”陷阱
例如下面一段代码,我们在使用
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
2
3
4
5
上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
2
3
4
使用var和let声明的另外一种区别。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
2
3
4
5
6
受“死区”的影响,使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。
# 总结
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
# new 命令的原理
使用new
命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的实例对象。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部的
this
关键字。 - 开始执行构造函数内部的代码。(代码中this指向空对象(实例对象))
- 返回实例对象(或自定义对象)
# 为什么不在浏览器也是用CommonJS
回答这个问题之前,我们首先要清楚一个事实,CommonJS的 require
语法是同步的,当我们使用require
加载一个模块的时候,必须要等这个模块加载完后,才会执行后面的代码。如果知道这个事实,那我们的问题也就很容易回答了。NodeJS 是服务端,使用 require
语法加载模块,一般是一个文件,只需要从本地硬盘中读取文件,它的速度是比较快的。但是在浏览器端就不一样了,文件一般存放在服务器或者CDN上,如果使用同步的方式加载一个模块还需要由网络来决定快慢,可能时间会很长,这样浏览器很容易进入“假死状态”。所以才有了后面的AMD和CMD模块化方案,它们都是异步加载的,比较适合在浏览器端使用。
好了,解决了第一个疑问后,我们开始进入正题。
它们最大的两个差异就是:
- CommonJS模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
我们先来看第一个差异。
CommonJS输出的是值的拷贝,换句话说就是,一旦输出了某个值,如果模块内部后续的变化,影响不了外部对这个值的使用。具体例子:
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
2
3
4
5
6
7
8
9
然后我们在其它文件中使用这个模块:
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
2
3
4
上面的例子充分说明了如果我们对外输出了counter
变量,就算后续调用模块内部的incCounter
方法去修改它的值,它的值依旧没有变化。
ES6模块运行机制完全不一样,JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里去取值。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
2
3
4
5
6
7
8
9
10
11
上面代码说明,ES6 模块import
的变量counter
是可变的,完全反应其所在模块lib.js
内部的变化。
# 总结
写到这里,本文也就基本结束了。我们总结一下文中涉及到的内容:
- 因为CommonJS的
require
语法是同步的,所以就导致了CommonJS模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用 ; - CommonJS 模块输出的是一个值的拷贝,而ES6 模块输出的是值的引用;
- CommonJS 模块是运行时加载,而ES6 模块是编译时输出接口,使得对JS的模块进行静态分析成为了可能;
- 因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。CommonJS遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用
import
加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值; - 关于模块顶层的
this
指向问题,在CommonJS顶层,this
指向当前模块;而在ES6模块中,this
指向undefined
; - 关于两个模块互相引用的问题,在ES6模块当中,是支持加载CommonJS模块的。但是反过来,CommonJS并不能
require
ES6模块,在NodeJS中,两种模块方案是分开处理的。
# 解决0.1+0.2等于0.3
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...
(1100循环),0.2的二进制是:0.00110011001100...
(1100循环),这两个数的二进制都是无限循环的数。
# 解决办法
属性:Number.EPSILON
,而这个值正等于2^-52。这个值非常非常小,在底层计算机已经帮我们运算好,并且无限接近0,但不等于0,。这个时候我们只要判断(0.1+0.2)-0.3小于
Number.EPSILON`,在这个误差的范围内就可以判定0.1+0.2===0.3为`true
function numbersequal(a,b){
return Math.abs(a-b)<Number.EPSILON;
}
var a=0.1+0.2, b=0.3;
console.log(numbersequal(a,b)); //true
2
3
4
5
但是这里要考虑兼容性的问题了,在chrome
中支持这个属性,但是IE
并不支持(笔者的版本是IE10
不兼容),所以我们还要解决IE
的不兼容问题。
Number.EPSILON=(function(){ //解决兼容性问题
return Number.EPSILON?Number.EPSILON:Math.pow(2,-52);
})();
2
3
# QUIC协议
QUIC(Quick UDP Internet Connections),直译过来就是“快速的 UDP 互联网连接”,是 Google 基于 UDP 提出的一种改进的通信协议,作为传统 HTTP over TCP 的替代品,开源于 Chromium 项目中。
为了加快 TCP 的传输效率,Google 提出了 BBR 拥塞控制算法,将 TCP 的性能发挥到了极致。由于 TCP 和 UDP 协议是系统内核实现的。
# 特点
QUIC 就诞生了,它汇集了 TCP 和 UDP 的优点,使用 UDP 来传输数据以加快网络速度,降低延迟,由 QUIC 来保证数据的顺序、完整性和正确性,即使发生了丢包,也由 QUIC 来负责数据的 纠错。
# 什么时候适合用QUIC
移动端 由于 QUIC 并不使用 IP + 端口来标识客户身份,而是使用 ID,这使得在网络环境切换后还可以保持连接,非常适合用在移动网站上面,在手机信号不稳定的情况下,TCP + TLS 的开销是非常大的!QUIC 的 0-RTT 可以极大限度地提升访问速度。
# 总结
QUIC 实现的目标,就是利用 UDP 实现一个 TCP,支持 TCP 的所有特性,并且比 TCP 更快更好用。
QUIC 是从 2012 年开始的项目,到目前也还只是草案阶段,并且同样处于草案阶段的 TLS1.3 也同样拥有了 QUIC 中的很多优点(比如 0-RTT)。对于访问速度的优化方式越来越多,适当的选择可以为网站增色许多。
# 手写promise.all
function diPromiseAll(promises){
return new Promise((resolve, reject)=>{
// 参数判断
if(!Array.isArray(promises)){
throw new TypeError("promises must be an array")
}
let count = 0 // 记录有几个resolved
let len = promises.length
let result = Array(len) // 存放结果
for(let i=0;i<len;i++){
Promise.resolve(promises[i]).then((value) => {
count++
result[i] = value
if(count===len){
return resolve(result)
}
},(reason)=>{
return reject(reason)
})
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 验证
let p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
diPromiseAll([p1, p2, p3]).then((res)=>{
console.log(res, 'res')
}, (err)=>{
console.log(err, 'err')
})
// [1, 2, 3]
2
3
4
5
6
7
8
9
10
11
# 手写深克隆
//标准的深拷贝 => 引用数据类型(数组,对象)
function deepClone(source){
const target = source.constructor === Array ? [] : {}
for(let keys in source){
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){
target[keys] = source[keys].constructor === Array ? [] : {}
target[keys] = deepClone(source[keys])
}else{
target[keys] = source[keys]
}
}
}
retrun target
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Undefined和Null区别
# undefined
undefined 的字面意思就是:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:
# 1、声明一个变量,但是没有赋值
var foo;
console.log(foo); // undefined
复制代码
2
3
访问 foo,返回了 undefined,表示这个变量自从声明了以后,就从来没有使用过,也没有定义过任何有效的值。
# 2、访问对象上不存在的属性或者未定义的变量
console.log(Object.foo); // undefined
console.log(typeof demo); // undefined
复制代码
2
3
访问 Object 对象上的 foo 属性,返回 undefined , 表示Object 上不存在或者没有定义名为 foo 的属性;对未声明的变量执行typeof操作符返回了undefined值。
# 3、函数定义了形参,但没有传递实参
//函数定义了形参 a
function fn(a) {
console.log(a); // undefined
}
fn(); //未传递实参
复制代码
2
3
4
5
6
函数 fn 定义了形参 a,但 fn 被调用时没有传递参数,因此,fn 运行时的参数 a 就是一个原始的、未被赋值的变量。
# 4、使用void对表达式求值
void 0 ; // undefined
void false; // undefined
void []; // undefined
void null; // undefined
void function fn(){} ; // undefined
复制代码
2
3
4
5
6
ECMAScript 明确规定 void 操作符 对任何表达式求值都返回 undefined ,这和函数执行操作后没有返回值的作用是一样的,JavaScript 中的函数都有返回值,当没有 return 操作时,就默认返回一个原始的状态值,这个值就是 undefined,表明函数的返回值未被定义。
# null
null 的字面意思是:空值 。这个值的语义是,希望表示一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象。
# 1、一般在以下两种情况下我们会将变量赋值为null
- 如果定义的变量在将来用于保存对象,那么最好将该变量初始化为null,而不是其他值。换句话说,只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存null值,这样有助于进一步区分null和undefined。
- 当一个数据不再需要使用时,我们最好通过将其值设置为null来释放其引用,这个做法叫做解除引用。不过解除一个值的引用并不意味着自动回收改值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器在下次运行时将其回收。解除引用还有助于消除有可能出现的循环引用的情况。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时(函数执行完时)自动被解除引用。
# 2、特殊的typeof null
当我们使用typeof操作符检测null值,我们理所应当地认为应该返"Null"类型呀,但是事实返回的类型却是"object"。
var data = null;
console.log(typeof data); // "object"
复制代码
2
3
是不是很奇怪?其实我们可以从两方面来理解这个结果:
- 一方面从逻辑角度来看,null值表示一个空对象指针,它代表的其实就是一个空对象,所以使用typeof操作符检测时返回"object"也是可以理解的。
- 另一方面,其实在JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的(对象的类型标签是 0)。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"。在ES6中,当时曾经有提案为历史平凡, 将type null的值纠正为null, 但最后提案被拒了,所以还是保持"object"类型。
# 总结
用一句话总结两者的区别就是:undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。