Tiny'Wo | 小窝

网络中的一小块自留地

面试官:谈谈this对象的理�?

一、定�?

函数�?this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差�?
在绝大多数情况下,函数的调用方式决定�?this 的值(运行时绑定)

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对�?
举个例子�?

1
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
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用�?
console.log( "baz" );
bar(); // <-- bar的调用位�?}

function bar() {
// 当前调用栈是:baz --> bar
// 因此,当前调用位置在baz�?
console.log( "bar" );
foo(); // <-- foo的调用位�?}

function foo() {
// 当前调用栈是:baz --> bar --> foo
// 因此,当前调用位置在bar�?
console.log( "foo" );
}

baz(); // <-- baz的调用位�?```

同时,`this`在函数执行过程中,`this`一旦被确定了,就不可以再更�?
```js
var a = 10;
var obj = {
a: 20
}

function fn() {
this = obj; // 修改this,运行后会报�? console.log(this.a);
}

fn();

二、绑定规�?

根据不同的使用场合,this有不同的值,主要分为下面几种情况�?

  • 默认绑定

  • 隐式绑定

  • new绑定

  • 显示绑定

默认绑定

全局环境中定义person函数,内部使用this关键�?

1
2
3
4
5
var name = 'Jenny';
function person() {
return this.name;
}
console.log(person()); //Jenny

上述代码输出Jenny,原因是调用函数的对象在游览器中位window,因此this指向window,所以输出Jenny

注意�?
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

隐式绑定

函数还可以作为某个对象的方法调用,这时this就指这个上级对象

1
2
3
4
5
6
7
8
9
function test() {
console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;

obj.m(); // 1

这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

1
2
3
4
5
6
7
8
9
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();

上述代码中,this的上一级对象为bb内部并没有a变量的定义,所以输出undefined

这里再举一种特殊情�?

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();

此时this指向的是window,这里的大家需要记住,this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window

new绑定

通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

1
2
3
4
5
6
function test() {
 this.x = 1;
}

var obj = new test();
obj.x // 1

上述代码之所以能过输�?,是因为new关键字改变了this的指�?
这里再列举一些特殊情况:

new过程遇到return一个对象,此时this指向为返回的对象

1
2
3
4
5
6
7
function fn()  
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined

如果返回一个简单类型的时候,则this指向实例对象

1
2
3
4
5
6
7
function fn()  
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx

注意的是null虽然也是对象,但是此时new仍然指向实例对象

1
2
3
4
5
6
7
function fn()  
{
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx

显示修改

apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参�?

1
2
3
4
5
6
7
8
9
var x = 0;
function test() {
 console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1

关于apply、call、bind三者的区别,我们后面再详细�?

三、箭头函�?

�?ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确�?this 的指向(编译时绑定)

举个例子�?

1
2
3
4
5
6
7
8
const obj = {
sayThis: () => {
console.log(this);
}
};

obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面�?this 就绑�?window 上去�?const globalSay = obj.sayThis;
globalSay(); // window 浏览器中�?global 对象

虽然箭头函数的this能够在编译的时候就确定了this的指向,但也需要注意一些潜在的�?
下面举个例子�?
绑定事件监听

1
2
3
4
5
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
console.log(this === window) // true
this.innerHTML = 'clicked button'
})

上述可以看到,我们其实是想要this为点击的button,但此时this指向了window

包括在原型上添加方法时候,此时this指向window

1
2
3
4
5
6
Cat.prototype.sayName = () => {
console.log(this === window) //true
return this.name
}
const cat = new Cat('mm');
cat.sayName()

同样的,箭头函数不能作为构建函数

四、优先级

隐式绑定 VS 显式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
console.log( this.a );
}

var obj1 = {
a: 2,
foo: foo
};

var obj2 = {
a: 3,
foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

显然,显示绑定的优先级更�?

new绑定 VS 隐式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo(something) {
this.a = something;
}

var obj1 = {
foo: foo
};

var obj2 = {};

obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

可以看到,new绑定的优先级>隐式绑定

new绑定 VS 显式绑定

因为newapply、call无法一起使用,但硬绑定也是显式绑定的一种,可以替换测试

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(something) {
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar被绑定到obj1上,但是new bar(3) 并没有像我们预计的那样把obj1.a修改�?。但是,new修改了绑定调用bar()中的this

我们可认为new绑定优先级>显式绑定

综上,new绑定优先�?> 显示绑定优先�?> 隐式绑定优先�?> 默认绑定优先�?

相关链接

面试官:谈谈 JavaScript 中的类型转换机制

一、概�?

前面我们讲到,JS 中有六种简单数据类型:undefinednullbooleanstringnumbersymbol,以及引用类型:object

但是我们在声明的时候只有一种数据类型,只有到运行期间才会确定当前类�?

1
let x = y ? 1 : a;

上面代码中,x的值在编译阶段是无法获取的,只有等到程序运行时才能知道

虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型与预期不符合,就会触发类型转换机制

常见的类型转换有�?

  • 强制转换(显示转换)
  • 自动转换(隐式转换)

二、显示转�?

显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有�?

  • Number()
  • parseInt()
  • String()
  • Boolean()

Number()

将任意类型的值转化为数�?
先给出类型转换规则:

实践一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数�?Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转�?NaN
Number(undefined) // NaN

// null:转�?
Number(null) // 0

// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

从上面可以看到,Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

parseInt()

parseInt相比Number,就没那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来

1
parseInt('32a3') //32

String()

可以将任意类型的值转化成字符�?
给出转换规则图:

实践一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 数值:转为相应的字符串
String(1) // "1"

//字符串:转换后还是原来的�?String("a") // "a"

//布尔值:true转为字符�?true",false转为字符�?false"
String(true) // "true"

//undefined:转为字符串"undefined"
String(undefined) // "undefined"

//null:转为字符串"null"
String(null) // "null"

//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

Boolean()

可以将任意类型的值转为布尔值,转换规则如下�?

实践一下:

1
2
3
4
5
6
7
8
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

三、隐式转�?

在隐式转换中,我们可能最大的疑惑�?:何时发生隐式转换?

我们这里可以归纳为两种情况发生隐式转换的场景�?

  • 比较运算(==!=><)、ifwhile需要布尔值地�?- 算术运算(+-*/%�?
    除了上面的场景,还要求运算符两边的操作数不是同一类型

自动转换为布尔�?

在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数

可以得出个小结:

  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • “”

除了上面几种会被转化成false,其他都换被转化成true

自动转换成字符串

遇到预期为字符串的地方,就会将非字符串的值自动转为字符串

具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串

常发生在+运算中,一旦存在字符串,则会进行字符串拼接操作

1
2
3
4
5
6
7
8
'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

自动转换成数�?

除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数�?

1
2
3
4
5
6
7
8
9
10
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN

null转为数值时,值为0undefined转为数值时,值为NaN

面试官:typeof �?instanceof 区别

一、typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型

使用方法如下�?

1
2
typeof operand
typeof(operand)

operand表示对象或原始值的表达式,其类型将被返�?
举个例子

1
2
3
4
5
6
7
8
9
10
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

从上面例子,�?个都是基础数据类型。虽然typeof nullobject,但这只是 JavaScript 存在的一个悠�?Bug,不代表null 就是引用数据类型,并且null 本身也不是对�?
所以,null �?typeof 之后返回的是有问题的结果,不能作为判断null的方法。如果你需要在 if 语句中判断是否为 null,直接通过===null来判断就�?
同时,可以发现引用类型数据,用typeof来判断的话,除了function会被识别出来之外,其余的都输出object

如果我们想要判断一个变量是否存在,可以使用typeof�?不能使用if(a)�?若a未声明,则报�?

1
2
3
if(typeof a != 'undefined'){
//变量存在
}

二、instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链�?
使用如下�?

1
object instanceof constructor

object为实例对象,constructor为构造函�?
构造函数通过new可以实例对象,instanceof 能判断这个对象是否是之前那个构造函数生成的对象

1
2
3
4
5
6
7
8
// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false

关于instanceof的实现原理,可以参考下面:

1
2
3
4
5
6
7
8
9
10
11
function myInstanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}

也就是顺着原型链去找,直到找到相同的原型对象,返回true,否则为false

三、区�?

typeofinstanceof都是判断数据类型的方法,区别如下�?

  • typeof会返回一个变量的基本类型,instanceof返回的是一个布尔�?
  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  • typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判�?
    可以看到,上述两种方法都有弊端,并不能满足所有场景的需�?
    如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]�?的字符串

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"

了解了toString的基本用法,下面就实现一个全局通用的数据类型判断方�?

1
2
3
4
5
6
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返�? return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结�? return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}

使用如下

1
2
3
4
5
6
getType([])     // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判�?getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小�?getType(/123/g) //"RegExp" toString返回

面试官:如何判断一个元素是否在可视区域中?

一、用�?可视区域即我们浏览网页的设备肉眼可见的区域,如下�?

在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:

  • 图片的懒加载
  • 列表的无限滚�?- 计算广告元素的曝光情�?- 可点击链接的预加�?

二、实现方�?

判断一个元素是否在可视区域,我们常用的有三种办法:

  • offsetTop、scrollTop

  • getBoundingClientRect

  • Intersection Observer

offsetTop、scrollTop

offsetTop,元素的上外边框至包含元素的上内边框之间的像素距离,其他offset属性如下图所示:

下面再来了解下clientWidthclientHeight�?

  • clientWidth:元素内容区宽度加上左右内边距宽度,即clientWidth = content + padding
  • clientHeight:元素内容区高度加上上下内边距高度,即clientHeight = content + padding

这里可以看到client元素都不包括外边�?
最后,关于scroll系列的属性如下:

  • scrollWidth �?scrollHeight 主要用于确定元素内容的实际大�?

  • scrollLeft �?scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置

    • 垂直滚动 scrollTop > 0
    • 水平滚动 scrollLeft > 0
  • 将元素的 scrollLeft �?scrollTop 设置�?0,可以重置元素的滚动位置

注意

  • 上述属性都是只读的,每次访问都要重新开�?

下面再看看如何实现判断:

公式如下�?```js
el.offsetTop - document.documentElement.scrollTop <= viewPortHeight

1
2
3
4
5
6
7
8
9
代码实现�?```js
function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
return top <= viewPortHeight
}

getBoundingClientRect

返回值是一�?DOMRect对象,拥有left, top, right, bottom, x, y, width, �?height属�?

1
2
3
4
5
6
7
8
9
10
11
12
const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();
console.log(clientRect);

// {
// bottom: 556.21875,
// height: 393.59375,
// left: 333,
// right: 1017,
// top: 162.625,
// width: 684
// }

属性对应的关系图如下所示:

当页面发生滚动的时候,topleft属性值都会随之改�?
如果一个元素在视窗之内的话,那么它一定满足下面四个条件:

  • top 大于等于 0
  • left 大于等于 0
  • bottom 小于等于视窗高度
  • right 小于等于视窗宽度

实现代码如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();

return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
);
}

Intersection Observer

Intersection Observer 即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect 会好很多

使用步骤主要分为两步:创建观察者和传入被观察�?

创建观察�?

1
2
3
4
5
6
7
8
9
const options = {
// 表示重叠面积占被观察者的比例,从 0 - 1 取值,
// 1 表示完全被包�? threshold: 1.0,
root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};

const callback = (entries, observer) => { ....}

const observer = new IntersectionObserver(callback, options);

通过new IntersectionObserver创建了观察�?observer,传入的参数 callback 在重叠比例超�?threshold 时会被执行`

关于callback回调函数常用属性如下:

1
2
3
4
5
6
7
// 上段代码中被省略�?callback
const callback = function(entries, observer) {
entries.forEach(entry => {
entry.time; // 触发的时�? entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位�? entry.boundingClientRect; // 被观察者的位置举行
entry.intersectionRect; // 重叠区域的位置矩�? entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.target; // 被观察�? });
};

传入被观察�?

通过 observer.observe(target) 这一行代码即可简单的注册被观察�?

1
2
const target = document.querySelector('.target');
observer.observe(target);

三、案例分�?

实现:创建了一个十万个节点的长列表,当节点滚入到视窗中时,背景就会从红色变为黄�?
Html结构如下�?

1
<div class="container"></div>

css样式如下�?

1
2
3
4
5
6
7
8
9
10
.container {
display: flex;
flex-wrap: wrap;
}
.target {
margin: 5px;
width: 20px;
height: 20px;
background: red;
}

container插入1000个元�?

1
2
3
4
5
6
7
8
9
const $container = $(".container");

// 插入 100000 �?<div class="target"></div>
function createTargets() {
const htmlString = new Array(100000)
.fill('<div class="target"></div>')
.join("");
$container.html(htmlString);
}

这里,首先使用getBoundingClientRect 方法进行判断元素是否在可视区�?

1
2
3
4
5
6
7
8
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight =
window.innerHeight || document.documentElement.clientHeight;
const { top, right, bottom, left } = element.getBoundingClientRect();

return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}

然后开始监听scroll事件,判断页面上哪些元素在可视区域中,如果在可视区域中则将背景颜色设置为yellow

1
2
3
4
5
6
7
8
$(window).on("scroll", () => {
console.log("scroll !");
$targets.each((index, element) => {
if (isInViewPort(element)) {
$(element).css("background-color", "yellow");
}
});
});

通过上述方式,可以看到可视区域颜色会变成黄色了,但是可以明显看到有卡顿的现象,原因在于我们绑定了scroll事件,scroll事件伴随了大量的计算,会造成资源方面的浪�?
下面通过Intersection Observer的形式同样实现相同的功能

首先创建一个观察�?

1
const observer = new IntersectionObserver(getYellow, { threshold: 1.0 });

getYellow回调函数实现对背景颜色改变,如下�?

1
2
3
4
5
function getYellow(entries, observer) {
entries.forEach(entry => {
$(entry.target).css("background-color", "yellow");
});
}

最后传入观察者,即.target元素

1
2
3
$targets.each((index, element) => {
observer.observe(element);
});

可以看到功能同样完成,并且页面不会出现卡顿的情况

参考文�?

面试官:说说�?Node 中的 Buffer 的理解?应用场景�?

一、是什�?

Node应用中,需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,要处理大量二进制数据,而Buffer就是在内存中开辟一片区域(初次初始化为8KB),用来存放二进制数�?
在上述操作中都会存在数据流动,每个数据流动的过程中,都会有一个最小或最大数据量

如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理

这里的等待区就指的缓冲区(Buffer),它是计算机中的一个小物理单位,通常位于计算机的 RAM �?
简单来讲,Nodejs不能控制数据传输的速度和到达时间,只能决定何时发送数据,如果还没到发送时间,则将数据放在Buffer中,即在RAM中,直至将它们发送完�?
上面讲到了Buffer是用来存储二进制数据,其的形式可以理解成一个数组,数组中的每一项,都可以保�?位二进制:00000000,也就是一个字�?
例如�?

1
const buffer = Buffer.from("why")

其存储过程如下图所示:

二、使用方�?

Buffer 类在全局作用域中,无须require导入

创建Buffer的方法有很多种,我们讲讲下面的两种常见的形式�?

  • Buffer.from()

  • Buffer.alloc()

Buffer.from()

1
2
3
4
5
6
const b1 = Buffer.from('10');
const b2 = Buffer.from('10', 'utf8');
const b3 = Buffer.from([10]);
const b4 = Buffer.from(b3);

console.log(b1, b2, b3, b4); // <Buffer 31 30> <Buffer 31 30> <Buffer 0a> <Buffer 0a>

Buffer.alloc()

1
2
const bAlloc1 = Buffer.alloc(10); // 创建一个大小为 10 个字节的缓冲�?const bAlloc2 = Buffer.alloc(10, 1); // 建一个长度为 10 �?Buffer,其中全部填充了值为 `1` 的字�?console.log(bAlloc1); // <Buffer 00 00 00 00 00 00 00 00 00 00>
console.log(bAlloc2); // <Buffer 01 01 01 01 01 01 01 01 01 01>

在上面创建buffer后,则能够toString的形式进行交互,默认情况下采取utf8字符编码形式,如�?

1
2
3
4
5
6
const buffer = Buffer.from("你好");
console.log(buffer);
// <Buffer e4 bd a0 e5 a5 bd>
const str = buffer.toString();
console.log(str);
// 你好

如果编码与解码不是相同的格式则会出现乱码的情况,如下�?

1
2
3
4
5
6
const buffer = Buffer.from("你好","utf-8 ");
console.log(buffer);
// <Buffer e4 bd a0 e5 a5 bd>
const str = buffer.toString("ascii");
console.log(str);
// d= e%=

当设定的范围导致字符串被截断的时候,也会存在乱码情况,如下:

1
2
3
4
5
6
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');

console.log(buf) // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length) // 17

console.log(buf.toString('UTF-8', 0, 9)) // Node.js �?console.log(buf.toString('UTF-8', 0, 11)) // Node.js 技

所支持的字符集有如下:

  • ascii:仅支持 7 �?ASCII 数据,如果设置去掉高位的话,这种编码是非常快�?- utf8:多字节编码�?Unicode 字符,许多网页和其他文档格式都使�?UTF-8
  • utf16le�? �?4 个字节,小字节序编码�?Unicode 字符,支持代理对(U+10000�?U+10FFFF�?- ucs2,utf16le 的别�?- base64:Base64 编码
  • latin:一种把 Buffer 编码成一字节编码的字符串的方�?- binary:latin1 的别名,
  • hex:将每个字节编码为两个十六进制字�?

三、应用场�?

Buffer的应用场景常常与流的概念联系在一起,例如有如下:

  • I/O操作
  • 加密解密
  • zlib.js

I/O操作

通过流的形式,将一个文件的内容读取到另外一个文�?

1
2
3
4
const fs = require('fs');

const inputStream = fs.createReadStream('input.txt'); // 创建可读�?const outputStream = fs.createWriteStream('output.txt'); // 创建可写�?
inputStream.pipe(outputStream); // 管道读写

加解�?

在一些加解密算法中会遇到使用 Buffer,例�?crypto.createCipheriv 的第二个参数 key �?string �?Buffer 类型

zlib.js

zlib.js �?Node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操作二进制数据流,提供了压缩或解压功能

参考文�?- http://nodejs.cn/api/buffer.html

面试官:说说Node中的EventEmitter? 如何实现一个EventEmitter?

一、是什�?

我们了解到,Node 采用了事件驱动机制,而EventEmitter 就是Node实现事件驱动的基础

EventEmitter的基础上,Node 几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定/触发监听器,实现了异步操�?
Node.js 里面的许多对象都会分发事件,比如 fs.readStream 对象会在文件被打开的时候触发一个事�?
这些产生事件的对象都�?events.EventEmitter 的实例,这些对象有一�?eventEmitter.on() 函数,用于将一个或多个函数绑定到命名事件上

二、使用方�?

Node events模块只提供了一个EventEmitter类,这个类实现了Node异步事件驱动架构的基本模式——观察者模�?
在这种模式中,被观察�?主体)维护着一组其他对象派�?注册)的观察者,有新的对象对主体感兴趣就注册观察者,不感兴趣就取消订阅,主体有更新的话就依次通知观察者们

基本代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()

function callback() {
console.log('触发了event事件�?)
}
myEmitter.on('event', callback)
myEmitter.emit('event')
myEmitter.removeListener('event', callback);

通过实例对象的on方法注册一个名为event的事件,通过emit方法触发该事件,而removeListener用于取消事件的监�?
关于其常见的方法如下�?

  • emitter.addListener/on(eventName, listener) :添加类型为 eventName 的监听事件到事件数组尾部
  • emitter.prependListener(eventName, listener):添加类型为 eventName 的监听事件到事件数组头部
  • emitter.emit(eventName[, …args]):触发类型为 eventName 的监听事�?
  • emitter.removeListener/off(eventName, listener):移除类型为 eventName 的监听事�?
  • emitter.once(eventName, listener):添加类型为 eventName 的监听事件,以后只能执行一次并删除
  • emitter.removeAllListeners([eventName])�?移除全部类型�?eventName 的监听事�?

三、实现过�?

通过上面的方法了解,EventEmitter是一个构造函数,内部存在一个包含所有事件的对象

1
2
3
4
5
class EventEmitter {
constructor() {
this.events = {};
}
}

其中events存放的监听事件的函数的结构如下:

1
2
3
{
"event1": [f1,f2,f3]�? "event2": [f4,f5]�? ...
}

然后开始一步步实现实例方法,首先是emit,第一个参数为事件的类型,第二个参数开始为触发事件函数的参数,实现如下�?

1
2
3
4
5
emit(type, ...args) {
this.events[type].forEach((item) => {
Reflect.apply(item, this, args);
});
}

当实现了emit方法之后,然后实现onaddListenerprependListener这三个实例方法,都是添加事件监听触发函数,实现也是大同小�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
on(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].push(handler);
}

addListener(type,handler){
this.on(type,handler)
}

prependListener(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].unshift(handler);
}

紧接着就是实现事件监听的方法removeListener/on

1
2
3
4
5
6
7
8
9
10
removeListener(type, handler) {
if (!this.events[type]) {
return;
}
this.events[type] = this.events[type].filter(item => item !== handler);
}

off(type,handler){
this.removeListener(type,handler)
}

最后再来实现once方法�?再传入事件监听处理函数的时候进行封装,利用闭包的特性维护当前状态,通过fired属性值判断事件函数是否执行过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
once(type, handler) {
this.on(type, this._onceWrap(type, handler, this));
}

_onceWrap(type, handler, target) {
const state = { fired: false, handler, type , target};
const wrapFn = this._onceWrapper.bind(state);
state.wrapFn = wrapFn;
return wrapFn;
}

_onceWrapper(...args) {
if (!this.fired) {
this.fired = true;
Reflect.apply(this.handler, this.target, args);
this.target.off(this.type, this.wrapFn);
}
}

完整代码如下�?

1
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
56
57
58
59
class EventEmitter {
constructor() {
this.events = {};
}

on(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].push(handler);
}

addListener(type,handler){
this.on(type,handler)
}

prependListener(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].unshift(handler);
}

removeListener(type, handler) {
if (!this.events[type]) {
return;
}
this.events[type] = this.events[type].filter(item => item !== handler);
}

off(type,handler){
this.removeListener(type,handler)
}

emit(type, ...args) {
this.events[type].forEach((item) => {
Reflect.apply(item, this, args);
});
}

once(type, handler) {
this.on(type, this._onceWrap(type, handler, this));
}

_onceWrap(type, handler, target) {
const state = { fired: false, handler, type , target};
const wrapFn = this._onceWrapper.bind(state);
state.wrapFn = wrapFn;
return wrapFn;
}

_onceWrapper(...args) {
if (!this.fired) {
this.fired = true;
Reflect.apply(this.handler, this.target, args);
this.target.off(this.type, this.wrapFn);
}
}
}

测试代码如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ee = new EventEmitter();

// 注册所有事�?ee.once('wakeUp', (name) => { console.log(`${name} 1`); });
ee.on('eat', (name) => { console.log(`${name} 2`) });
ee.on('eat', (name) => { console.log(`${name} 3`) });
const meetingFn = (name) => { console.log(`${name} 4`) };
ee.on('work', meetingFn);
ee.on('work', (name) => { console.log(`${name} 5`) });

ee.emit('wakeUp', 'xx');
ee.emit('wakeUp', 'xx'); // 第二次没有触�?ee.emit('eat', 'xx');
ee.emit('work', 'xx');
ee.off('work', meetingFn); // 移除事件
ee.emit('work', 'xx'); // 再次工作

参考文�?- http://nodejs.cn/api/events.html#events_class_eventemitter

面试官:说说�?Node 中的 Stream 的理解?应用场景�?

一、是什�?

流(Stream),是一个数据传输手段,是端到端信息交换的一种方式,而且是有顺序�?是逐块读取数据、处理内容,用于顺序读取输入或写入输�?
Node.js中很多对象都实现了流,总之它是会冒数据(以 Buffer 为单位)

它的独特之处在于,它不像传统的程序那样一次将一个文件读入内存,而是逐块读取数据、处理其内容,而不是将其全部保存在内存�?
流可以分成三部分:sourcedestpipe

sourcedest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest)sourcedest就是通过pipe连接,让数据从source流向了dest,如下图所示:

二、种�?

NodeJS,几乎所有的地方都使用到了流的概念,分成四个种类�?

  • 可写流:可写入数据的流。例�?fs.createWriteStream() 可以使用流将数据写入文件

  • 可读流: 可读取数据的流。例如fs.createReadStream() 可以从文件读取内�?

  • 双工流: 既可读又可写的流。例�?net.Socket

  • 转换流: 可以在数据写入和读取时修改或转换数据的流。例如,在文件压缩操作中,可以向文件写入压缩数据,并从文件中读取解压数据

NodeJSHTTP服务器模块中,request 是可读流,response 是可写流。还有fs 模块,能同时处理可读和可写文件流

可读流和可写流都是单向的,比较容易理解,而另外两个是双向�?

双工�?

之前了解过websocket通信,是一个全双工通信,发送方和接受方都是各自独立的方法,发送和接收都没有任何关�?
如下图所示:

基本代码如下�?

1
2
3
4
5
6
7
8
9
10
const { Duplex } = require('stream');

const myDuplex = new Duplex({
read(size) {
// ...
},
write(chunk, encoding, callback) {
// ...
}
});

双工�?

双工流的演示图如下所示:

除了上述压缩包的例子,还比如一�?babel,把es6转换为,我们在左边写�?es6,从右边读取 es5

基本代码如下所示:

1
2
3
4
5
6
7
const { Transform } = require('stream');

const myTransform = new Transform({
transform(chunk, encoding, callback) {
// ...
}
});

三、应用场�?

stream的应用场景主要就是处理IO操作,而http请求和文件操作都属于IO操作

试想一下,如果一次IO操作过大,硬件的开销就过大,而将此次大的IO操作进行分段操作,让数据像水管一样流动,直到流动完成

常见的场景有�?

  • get请求返回文件给客户端
  • 文件操作
  • 一些打包工具的底层操作

get请求返回文件给客户端

使用stream流返回文件,res也是一个stream对象,通过pipe管道将文件数据返�?

1
2
3
4
5
6
7
8
9
const server = http.createServer(function (req, res) {
const method = req.method; // 获取请求方法
if (method === 'GET') { // get 请求
const fileName = path.resolve(__dirname, 'data.txt');
let stream = fs.createReadStream(fileName);
stream.pipe(res); // �?res 作为 stream �?dest
}
});
server.listen(8000);

文件操作

创建一个可读数据流readStream,一个可写数据流writeStream,通过pipe管道把数据流转过�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs')
const path = require('path')

// 两个文件�?const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 读取文件�?stream 对象
const readStream = fs.createReadStream(fileName1)
// 写入文件�?stream 对象
const writeStream = fs.createWriteStream(fileName2)
// 通过 pipe执行拷贝,数据流�?readStream.pipe(writeStream)
// 数据读取完成监听,即拷贝完成
readStream.on('end', function () {
console.log('拷贝完成')
})

一些打包工具的底层操作

目前一些比较火的前端打包构建工具,都是通过node.js编写的,打包和构建的过程肯定是文件频繁操作的过程,离不来stream,如gulp

参考文�?

面试官:说说对Nodejs中的事件循环机制理解?

一、是什�?

浏览器事件循环中,我们了解到javascript在浏览器中的事件循环机制,其是根据HTML5定义的规范来实现

而在NodeJS中,事件循环是基于libuv实现,libuv是一个多平台的专注于异步IO的库,如下图最右侧所示:

上图EVENT_QUEUE 给人看起来只有一个队列,但EventLoop存在6个阶段,每个阶段都有对应的一个先进先出的回调队列

二、流�?

上节讲到事件循环分成了六个阶段,对应如下�?

  • timers阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • 定时器检测阶�?timers):本阶段执行 timer 的回调,�?setTimeout、setInterval 里面的回调函�?- I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调
  • 闲置阶段(idle, prepare):仅系统内部使用
  • 轮询阶段(poll):检索新�?I/O 事件;执行�?I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情�?node 将在适当的时候在此阻�?- 检查阶�?check):setImmediate() 回调函数在这里执�?- 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on(‘close’, …)

每个阶段对应一个队列,当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶�?
除了上述6个阶段,还存在process.nextTick,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过�? 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调,类似插�?
流程图如下所示:

Node中,同样存在宏任务和微任务,与浏览器中的事件循环相似

微任务对应有�?

  • next tick queue:process.nextTick
  • other queue:Promise的then回调、queueMicrotask

宏任务对应有�?

  • timer queue:setTimeout、setInterval
  • poll queue:IO事件
  • check queue:setImmediate
  • close queue:close事件

其执行顺序为�?

  • next tick microtask queue
  • other microtask queue
  • timer queue
  • poll queue
  • check queue
  • close queue

三、题�?

通过上面的学习,下面开始看看题�?

1
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
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}

async function async2() {
console.log('async2')
}

console.log('script start')

setTimeout(function () {
console.log('setTimeout0')
}, 0)

setTimeout(function () {
console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})

console.log('script end')

分析过程�?

  • 先找到同步任务,输出script start
  • 遇到第一�?setTimeout,将里面的回调函数放�?timer 队列�?- 遇到第二�?setTimeout�?00ms后将里面的回调函数放�?timer 队列�?- 遇到第一个setImmediate,将里面的回调函数放�?check 队列�?- 遇到第一�?nextTick,将其里面的回调函数放到本轮同步任务执行完毕后执�?
  • 执行 async1函数,输�?async1 start
  • 执行 async2 函数,输�?async2,async2 后面的输�?async1 end进入微任务,等待下一轮的事件循环
  • 遇到第二个,将其里面的回调函数放到本轮同步任务执行完毕后执行
  • 遇到 new Promise,执行里面的立即执行函数,输�?promise1、promise2
  • then里面的回调函数进入微任务队列
  • 遇到同步任务,输�?script end
  • 执行下一轮回到函数,先依次输�?nextTick 的函数,分别�?nextTick1、nextTick2
  • 然后执行微任务队列,依次输出 async1 end、promise3
  • 执行timer 队列,依次输�?setTimeout0
  • 接着执行 check 队列,依次输�?setImmediate
  • 300ms后,timer 队列存在任务,执行输�?setTimeout2

执行结果如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2

最后有一道是关于setTimeoutsetImmediate的输出顺�?

1
2
3
4
5
6
7
setTimeout(() => {
console.log("setTimeout");
}, 0);

setImmediate(() => {
console.log("setImmediate");
});

输出情况如下�?

1
2
3
4
5
6
情况一�?setTimeout
setImmediate

情况二:
setImmediate
setTimeout

分析下流程:

  • 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
  • 遇到setTimeout,虽然设置的�?毫秒触发,但实际上会被强制改�?ms,时间到了然后塞入times阶段
  • 遇到setImmediate塞入check阶段
  • 同步代码执行完毕,进入Event Loop
  • 先进入times阶段,检查当前时间过去了1毫秒没有,如果过�?毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳�?- 跳过空的阶段,进入check阶段,执行setImmediate回调

这里的关键在于这1ms,如果同步代码执行时间较长,进入Event Loop的时�?毫秒已经过了,setTimeout先执行,如果1毫秒还没到,就先执行了setImmediate

参考文�?

面试官:如何实现文件上传?说说你的思路

一、是什�?

文件上传在日常开发中应用很广泛,我们发微博、发微信朋友圈都会用到了图片上传功能

因为浏览器限制,浏览器不能直接操作文件系统的,需要通过浏览器所暴露出来的统一接口,由用户主动授权发起来访问文件动作,然后读取文件内容进指定内存里,最后执行提交请求操作,将内存里的文件内容数据上传到服务端,服务端解析前端传来的数据信息后存入文件里

对于文件上传,我们需要设置请求头为content-type:multipart/form-data

multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms �?POST 方法上传文件

结构如下�?

1
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
POST /t2/upload.do HTTP/1.1
User-Agent: SOHUWapRebot
Accept-Language: zh-cn,zh;q=0.5
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Content-Length: 60408
Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Host: w.sohu.com

--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data; name="city"

Santa colo
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="desc"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

...
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="pic"; filename="photo.jpg"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

... binary data of the jpg ...
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--

boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾

xxx是即时生成的字符串,用以确保整个分隔符不会在文件或表单项的内容中出现

每个表单项必须包含一�?Content-Disposition 头,其他的头信息则为可选项�?比如 Content-Type

Content-Disposition 包含�?type �?一个名字为name�?parametertype �?form-dataname 参数的值则为表单控件(也即 field)的名字,如果是文件,那么还有一�?filename 参数,值就是文件名

1
Content-Disposition: form-data; name="user"; filename="logo.png"

至于使用multipart/form-data,是因为文件是以二进制的形式存在,其作用是专门用于传输大型二进制数据,效率高

二、如何实�?

关于文件的上传的上传,我们可以分成两步骤�?

  • 文件的上�?- 文件的解�?

文件上传

传统前端文件上传的表单结构如下:

1
2
3
4
<form action="http://localhost:8080/api/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" value="" multiple="multiple" />
<input type="submit" value="提交"/>
</form>

action 就是我们的提交到的接口,enctype="multipart/form-data" 就是指定上传文件格式,input �?name 属性一定要等于file

文件解析

在服务器中,这里采用koa2中间件的形式解析上传的文件数据,分别有下面两种形式:

  • koa-body
  • koa-multer

koa-body

安装依赖

1
npm install koa-body

引入koa-body中间�?

1
2
3
4
5
6
7
const koaBody = require('koa-body');
app.use(koaBody({
multipart: true,
formidable: {
maxFileSize: 200*1024*1024 // 设置上传文件大小最大限制,默认2M
}
}));

获取上传的文�?

1
const file = ctx.request.files.file; // 获取上传文件

获取文件数据后,可以通过fs模块将文件保存到指定目录

1
2
3
4
5
6
7
8
9
router.post('/uploadfile', async (ctx, next) => {
// 上传单个文件
const file = ctx.request.files.file; // 获取上传文件
// 创建可读�? const reader = fs.createReadStream(file.path);
let filePath = path.join(__dirname, 'public/upload/') + `/${file.name}`;
// 创建可写�? const upStream = fs.createWriteStream(filePath);
// 可读流通过管道写入可写�? reader.pipe(upStream);
return ctx.body = "上传成功�?;
});

koa-multer

安装依赖�?

1
npm install koa-multer

使用 multer 中间件实现文件上�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "./upload/")
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname))
}
})

const upload = multer({
storage
});

const fileRouter = new Router();

fileRouter.post("/upload", upload.single('file'), (ctx, next) => {
console.log(ctx.req.file); // 获取文件
})

app.use(fileRouter.routes());

参考文�?

面试官:说说�?Node 中的 fs模块的理�? 有哪些常用方�?

一、是什�?

fs(filesystem),该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包�?
可以说,所有与文件的操作都是通过fs核心模块实现

导入模块如下�?

1
const fs = require('fs');

这个模块对所有文件系统操作提供异步(不具有sync 后缀)和同步(具�?sync 后缀)两种操作方式,而供开发者选择

二、文件知�?

在计算机中有关于文件的知识:

  • 权限�?mode
  • 标识�?flag
  • 文件描述�?fd

权限�?mode

针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限�?�?�?,不具备权限�?

如在linux查看文件权限位:

1
2
drwxr-xr-x�? PandaShen�?97121�? Jun 28 14:41 core
-rw-r--r--�? PandaShen�?97121�?93Jun 23 17:44 index.md

在开头前十位中,d为文件夹,-为文件,后九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),- 代表没有当前位对应的权限

标识�?

标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示:

符号 含义
r 读取文件,如果文件不存在则抛出异常�?
r+ 读取并写入文件,如果文件不存在则抛出异常�?
rs 读取并写入文件,指示操作系统绕开本地文件系统缓存�?
w 写入文件,文件不存在会被创建,存在则清空后写入�?
wx 写入文件,排它方式打开�?
w+ 读取并写入文件,文件不存在则创建文件,存在则清空后写入�?
wx+ �?w+ 类似,排他方式打开�?
a 追加写入,文件不存在则创建文件�?
ax �?a 类似,排他方式打开�?
a+ 读取并追加写入,不存在则创建�?
ax+ �?a+ 类似,排他方式打开�?

文件描述�?fd

操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件

Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述�?
�?NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面�?012三个比较特殊的描述符,分别代�?process.stdin(标准输入)、process.stdout(标准输出)�?process.stderr(错误输出)

三、方�?

下面针对fs模块常用的方法进行展开�?

  • 文件读取
  • 文件写入
  • 文件追加写入
  • 文件拷贝
  • 创建目录

文件读取

fs.readFileSync

同步读取,参数如下:

  • 第一个参数为读取文件的路径或文件描述�?- 第二个参数为 options,默认值为 null,其中有 encoding(编码,默认�?null)和 flag(标识位,默认为 r),也可直接传入 encoding

结果为返回文件的内容

1
2
3
4
5
6
7
const fs = require("fs");

let buf = fs.readFileSync("1.txt");
let data = fs.readFileSync("1.txt", "utf8");

console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(data); // Hello

fs.readFile

异步读取方法 readFile �?readFileSync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参�?err(错误)�?data(数据),该方法没有返回值,回调函数在读取文件成功后执行

1
2
3
4
5
6
7
const fs = require("fs");

fs.readFile("1.txt", "utf8", (err, data) => {
if(!err){
console.log(data); // Hello
}
});

文件写入

writeFileSync

同步写入,有三个参数�?

  • 第一个参数为写入文件的路径或文件描述�?

  • 第二个参数为写入的数据,类型�?String �?Buffer

  • 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认�?utf8)�?flag(标识位,默认为 w)和 mode(权限位,默认为 0o666),也可直接传入 encoding

1
2
3
4
5
6
const fs = require("fs");

fs.writeFileSync("2.txt", "Hello world");
let data = fs.readFileSync("2.txt", "utf8");

console.log(data); // Hello world

writeFile

异步写入,writeFile �?writeFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参�?err(错误),回调函数在文件写入数据成功后执�?

1
2
3
4
5
6
7
8
9
const fs = require("fs");

fs.writeFile("2.txt", "Hello world", err => {
if (!err) {
fs.readFile("2.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
}
});

文件追加写入

appendFileSync

参数如下�?

  • 第一个参数为写入文件的路径或文件描述�?- 第二个参数为写入的数据,类型�?String �?Buffer
  • 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认�?utf8)�?flag(标识位,默认为 a)和 mode(权限位,默认为 0o666),也可直接传入 encoding
1
2
3
4
const fs = require("fs");

fs.appendFileSync("3.txt", " world");
let data = fs.readFileSync("3.txt", "utf8");

appendFile

异步追加写入方法 appendFile �?appendFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参�?err(错误),回调函数在文件追加写入数据成功后执�?

1
2
3
4
5
6
7
8
9
const fs = require("fs");

fs.appendFile("3.txt", " world", err => {
if (!err) {
fs.readFile("3.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
}
});

文件拷贝

copyFileSync

同步拷贝

1
2
3
4
5
6
const fs = require("fs");

fs.copyFileSync("3.txt", "4.txt");
let data = fs.readFileSync("4.txt", "utf8");

console.log(data); // Hello world

copyFile

异步拷贝

1
2
3
4
5
6
7
const fs = require("fs");

fs.copyFile("3.txt", "4.txt", () => {
fs.readFile("4.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
});

创建目录

mkdirSync

同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异�?

1
// 假设已经有了 a 文件夹和 a 下的 b 文件�?fs.mkdirSync("a/b/c")

mkdir

异步创建,第二个参数为回调函�?

1
2
3
fs.mkdir("a/b/c", err => {
if (!err) console.log("创建成功");
});

参考文�?

0%