global
node再任意模块都可以访问global,但是默认声明的属性不是挂载在global下面,例如:let a=1;=>global.a是underfined
console.log(this===global); //false
console.log(this===module.exports); //true
- global出现时机
(function() {
console.log(this);
})();
这样自执行函数,默认是global点的形式调用的,自然内部的this就是global
- global内部属性
console.log(Object.keys(global));
[ 'DTRACE_NET_SERVER_CONNECTION',
'DTRACE_NET_STREAM_END',
'DTRACE_HTTP_SERVER_REQUEST',
'DTRACE_HTTP_SERVER_RESPONSE',
'DTRACE_HTTP_CLIENT_REQUEST',
'DTRACE_HTTP_CLIENT_RESPONSE',
'COUNTER_NET_SERVER_CONNECTION',
'COUNTER_NET_SERVER_CONNECTION_CLOSE',
'COUNTER_HTTP_SERVER_REQUEST',
'COUNTER_HTTP_SERVER_RESPONSE',
'COUNTER_HTTP_CLIENT_REQUEST',
'COUNTER_HTTP_CLIENT_RESPONSE',
'global',
'process', 进程
'Buffer', 缓存区 举例:node读取文件,放到内存中的数据,都是二进制,但是二进制都很长,然后buffer就是存储16进制
以下都是宏任务
'clearImmediate',
'clearInterval',
'clearTimeout',
'setImmediate',
'setInterval',
'setTimeout'
]
process(进程)
- process.platform: window=>win32 平台
- argv: 代表用户传递的参数,默认前两个参数 没有实际意义
参数一node路径 参数二被执行文件路径
参数只针对命令行时候携带的参数
例如:node a.js --port 3000
process.argv.slice(2),//因为前两个参数无实际意义
=》['--port','3000']
简单逻辑实现获取命令行参数
//memo:初始值,后面的{}其实是可选参数代表初始值 current:当前元素 //index:可选,当前元素索引 array:可选,当前元素所属数组对象
let config = process.argv.slice(2).reduce((memo, current, index, array) => {
if (current.includes('--')) {
// console.log(current); //--port --yml
memo[current.slice(2)] = array[index + 1];
}
return memo;
}, {});
node .\1.js --port 3000 --yml haha
console.log(config); //{ port: '3000', yml: 'haha' }
//也有第三方包:commander
- process.cwd() 在哪里执行文件,则输出所在目录
path.resolve() 效果同上
补充:该方法还可以把相对路径转绝对路径
path.resolve(__dirname,filename)
env(环境变量)
可以根据环境变量的不同,执行不同的结果
console.log(process.env); //例如windows就是把配置的环境变量都打印出来了,还有一些其他信息
在命令行模式下,首先设置环境变量(记得都是临时的,终端关闭就没了)
mac下通过export windows下通过set
export NODE_ENV=development && node 1.js mac系统
set NODE_ENV=development
node 1.js windows系统
示例代码
let url = '';
if (process.env.NODE_ENV === 'development') {
url = 'localhost'
} else {
url = 'www'
}
console.log(url);
第三方包 cross-env 这样可以不区分系统设置
事件轮询
上图全是宏任务,poll阶段:例如读文件写文件
注意:此处只讨论node环境下的事件循环,新版本node和浏览器基本一样,但是和浏览器没什么关系,是自己实现的
事件循环的步骤:
- 主线程先执行同步代码
- 执行完毕去执行微任务
- 微任务执行完毕去从图1从上到下依次执行
- 每个矩形都是一个阶段,每个阶段都是一个队列
- 宏任务中每个阶段的队列中任意一个任务执行完毕都会去再次执行微任务
- 然后才继续去取该队列下一个任务,然后再次执行完毕之后,再次清空微任务队列
- 该宏任务队列没有任务之后才开始执行下一个阶段的队列,如此反复
微任务
- nextTick (process) 微任务中优先级最高(node专属)
- Promise.resolve().then 注意then里面是微任务,resolve是同步代码
- MutationObserver(浏览器专属)
补充
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,
并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,
该回调函数会在浏览器下一次重绘之前执行
宏任务:script ajax 事件 requestFrameAnimation
setTimeout setInterval setImmediate
MessageChannel I/O UI rendering
process.nextTick和setImmediate区别
- setImmediate是宏任务
- 多个process.nextTick语句总是在当前"执行栈"一次执行完
- 多个setImmediate可能则需要多次loop才能执行完
这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!
process.nextTick(function foo() {
process.nextTick(foo);
});
小案例
Promise.resolve().then(data => {
//这个也是微任务
console.log('then');
})
process.nextTick(() => {
console.log('');
})
//nextTick then
poll阶段详解
poll区分不同情况详解
- 当timers清空之后,(此处不考虑系统级的,例如I/O callbacks),进入poll,poll执行完之后,此时check阶段不是空的,则先执行check,执行完毕之后,进入下次循环
- 如果check为空,事件循环会停在poll阶段,判断timer有没有到期定时器,有的话去执行,没有则继续等待
setTimeout(()=>{
console.log('timeout');
},0)
setImmediate(()=>{
console.log('setImmediate');
})
所以上面案例结果不能确定,虽然setTimeout理论上先执行,但是实际上可能node启动很快,都没到setTimeout延迟时间(新版本node,小于4毫秒都按照4毫秒算),然后setImmediate可能先执行,下次事件轮询才执行setTimeout的回调(注意是回调,回调其实是poll阶段执行的)。如果node启动慢,大于4毫秒,则第一次进poll就有到期的timer,则timeout会先输出
- 演变
上面情况不是绝对的,执行时机和上下文关系很大
let fs=require('fs');
fs.readFile('./a.txt',function() {
setTimeout(()=>{
console.log('timeout');
},0)
//此时肯定setImmediate先执行
setImmediate(()=>{
console.log('setImmediate');
})
})
百分百先输出setImmediate,因为readFile回调已经是poll阶段,然后check里面setImmediate又不是空,会被执行,下次循环才输出timeout
- 事件循环案例一
setTimeout(()=>{
console.log('timer'+1);
Promise.resolve().then(()=>{
console.log('then1');
})
},0)
Promise.resolve().then(()=>{
console.log('then2');
setTimeout(()=>{
console.log('timer'+2);
},0)
})
先走主栈,例如加载代码,然后then2 timer1 then1 timer2
- 事件循环案例二
new Promise(function(resolve) {
resolve();
}).then(function() {
console.log('1');
}).then(function() {
console.log('2');
})
new Promise(function(resolve) {
resolve();
}).then(function() {
console.log('3');
}).then(function() {
console.log('4');
})
// 1 3 2 4
多个then相连接,则第一个then结束之后返回其实还是一个promise.then,但是某个执行完毕之后又生成一个promise又被注册到微任务队列(追加最后),然后第二个Promise执行完也返回promise追加最后,所以才形成这样的输出结果。
- 事件循环案例三
async function async1() {
console.log('async1 start');
// await async2();
// console.log('async1 end');
//上面两行等价于这个-浏览器
// Promise.resolve(async2()).then(()=>{
// console.log('async1 end');
// })
//而在node环境,不同版本有不同区别,总之就是弄出来俩then,也就是是最好面试题用浏览器结果为准
async2().then(()=>{
console.log('hi');
return 6 //此处返回值,下一个then参数接收
}).then(num=>{
console.log('async1 end'+6);
})
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
})
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
})
console.log('script end');
结果
node版本
script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
浏览器版本
script start
async1 start
async2
promise1
script end
async1 end //位置和上面浏览器版本不同
promise2
setTimeout
node模块(ps:后期有时间可以补充珠峰视频代码,自己实现这个模式)
node特点,每个文件都是一个模块,模块外面包了匿名函数,并且传递了一些参数;而这些参数是,例如:module exports require __dirname __filename;所以在任意js文件才可以使用这些变量
fs
读写大文件
64k以下直接readfile writefile即可,但是过大文件不建议,因为内存可能爆炸,解决方案,读一点写一点
- 基本案例
/*
fs.readFile
但是读文件如果不存在可能报错,所以需要先判断文件是否存在
fs.existSync 同步
fs.access 异步
fs.writeFile 写文件
fs.copyFile 新版本新增,复制文件,也是先读文件再写,大文件也可能导致淹没可用内存
fs.rename 重命令
fs.unlink 删除文件
*/
/**
* r:读
* w:写
* r+:在读的基础上写,如果文件不存在,报错
* w+:在写的基础上读,如果文件不存在,创建
*/
let buffer=Buffer.alloc(3);
const SIZE=5;
fs.open('a.txt','r',(err,rfd)=>{
//读取文件 fd代表文件 buffer要把读取的内容写到buffer中
//0,3 从buffer的第0个位置写入,写入3个
//0 从文件的哪个位置开始读取
fs.open('b.txt','w',(err,wfd)=>{
let readOffset=0;
let writeOffset=0;
function next(){
fs.read(rfd,buffer,0,SIZE,readOffset,function(err,bytesRead) //真正读取到的个数{
if(bytesRead==0){
fs.close(wfd,()=>{});
fs.close(rfd,()=>{});
return
}
fs.write(wfd,buffer,0,3,0,function(err) {
readOffset+=bytesRead;
writeOffset+=bytesRead;
next();
})
});
}
next();
})
});
缺点:很难使用,耦合度高
怎么拆分=》发布订阅=》流
- 文件夹相关
/**
* 文件夹相关:
* fs.mkdir 创建文件夹
* fs.rmdir 删除文件夹
*/
//案例一:递归创建文件夹
//因为直接使用api创建文件夹,父级文件夹不能为空
const fs=require('fs');
function mkdir(paths,callback) {
paths=paths.split('/');
let index=1;
function next() {
if (index===paths.length+1) return callback();//整个层级目录创建完毕,调用最开始传入的回调函数
let dirPath=paths.slice(0,index++).join('/');
fs.access(dirPath,function(err) {
if (err) {
//不存在-则创建
fs.mkdir(dirPath,next);//此处是重点,相当于通过fs.mkdir回调的自动调用,但是传递next函数,实现递归
}else{
next();//存在进入下次递归然后创建下一层
}
})
}
next();
}
mkdir("a/b/c/d/e",function() {
console.log('创建完成');
})
//删除文件夹
//串联
//先序 深度优先删除文件夹-就是进入一个分支,有文件夹继续进入,一直到底,然后逆着删回来
//因为删除文件夹api不能删除非空文件夹
//案例二:串联删除
const fs = require('fs');
const path = require('path');
function preDeep(dir, callback) {
fs.stat(dir, function (err, statObj) {
if (statObj.isFile()) {
//删除文件即可
fs.unlink(dir, callback);
} else {
fs.readdir(dir, function (err, dirs) {
//dir是读取到的儿子 【b,e,1.js】
dirs = dirs.map(item => path.join(dir, item));
let index = 0;
function next() {//取出第一个删除掉
//当儿子都删除完了,则删除自己
if (index === dirs.length) return fs.rmdir(dir, callback);
let currentPath = dirs[index++];
preDeep(currentPath, next);
}
next();
})
}
})
}
//案例三:并发删除,性能更高
const fs=require('fs');
const path=require('path');
function preDeep(dir,callback) {
fs.stat(dir,function(err,statObj) {
if (statObj.isFile()) {
fs.unlink(dir,callback);
}else{
fs.readdir(dir,function(err,dirs) {
dirs=dirs.map(item=>path.join(dir,item));
if (dirs.length===0) {
//针对空文件夹
return fs.rmdir(dir,callback);
}
let index=0;
function done() {
//不要去想很多层,只想一层,一层的子删除完毕,调用return 删除父级文件夹
if (++index===dirs.length) return fs.rmdir(dir,callback);
}
dirs.forEach(dir => {
preDeep(dir,done);
});
})
}
})
}
preDeep("a", function () {
console.log('删除成功');
})
//案例四:并发删除,优雅版
function preDeep(dir) {
return new Promise((resolve,reject)=>{
fs.stat(dir,function (err,statObj) {
if (statObj.isFile()) {
fs.unlink(dir,resolve);
}else{
fs.readdir(dir,function(err,dirs) {
dirs=dirs.map(item=>preDeep(path.join(dir,item)));
Promise.all(dirs).then(()=>{
//通过all执行完所有子删除之后,此句删除父文件夹
fs.rmdir(dir,resolve);
})
})
}
})
})
}
//案例五:并发删除,await版本
let {unlink,readdir,stat,rmdir}=require('fs').promises;
async function preDeep(dir) {
let statObj=await stat(dir);
if (statObj.isFile()) {
await unlink(dir);
}else{
let dirs=await readdir(dir);
dirs=dirs.map(item=>preDeep(path.join(dir,item)));
await Promise.all(dirs);
await rmdir(dir);
}
}
preDeep("a").then(function() {
console.log('删除成功');
}).catch(err=>{
//或者不要catch,再上面preDeep内部加个大的try catch也行
console.log(err);
})
//案例六:广度删除,性能低一些
const { pathToFileURL } = require("url");
function wide(dir) {
let arr=[dir];
let index=0;
let current;
while (current=arr[index++]) {
let dirs=fs.readdirSync(current);
dirs=dirs.map(item=>pathToFileURL.join(current,item));
arr=[...arr,...dirs];
}
//循环arr删除即可
}
//此案例是同步的,可以实现异步的,原理相同
流
let fs=require('fs');
let rs=fs.createReadStream('./1.txt',{
flags:'r',
encoding:null,//默认buffer
highWaterMark:2,//内部会创建 64k大的buffer 64*1024 每次读取几个
mode:0o666,//可以是各种表示形式,例如十进制438
autoClose:true,
start:0,
end:10
});
// let ws=fs.createWriteStream('./b.txt',{
// highWaterMark:10 //预计占用的内存空间是多少
// })
//默认流的模式是暂停模式
rs.on('open',function () {
console.log('文件打开');
})
let arr=[];
rs.on('data',function (data) {//每次读取到结果
arr.push(data);//只能直接+,因为可能汉字或者各种编码,导致出问题
console.log(data);
})
rs.on('error',function () {
console.log('出错');
})
rs.on('close',function() {
console.log('文件关闭');
})
rs.on('end',function() {
console.log('文件读取完毕');
console.log(Buffer.concat(arr).toString());//读取完毕,用buffer处理即可
})
原理其实就是fs.read 然后加上EventEmitter 例如createReadStream extend EventEmitter
- 补充案例(说明highWaterMark参数产生的效果)
let fs=require('fs');
let ws=fs.createWriteStream('./1.txt',{
highWaterMark:3
})
let index=0;
function write() {
let flag=true;
while (index<10&&flag) {//可写流写入的数据只能是字符串或者buffer
flag=ws.write(index+'');//注意此处flag的功能 可以ws.pause() 暂停独流
index++;
}
if (index>9) {
ws.end();//触发close事件
}
}
write();
//只有当我们写入的个数达到了 预计大小并且被写入到文件后清空了,才会触发drain
//例如现在是0-9 则下面只会触发三次 因为 3 3 3 1
ws.on('drain',function() {
console.log('干了');
})
ws.on('close',function () {
console.log('close');
})
/*
ws.write('1','utf8',()=>{
console.log('1写入');
})
ws.write('11','utf8',()=>{
console.log('11写入');
})
ws.write('111','utf8',()=>{
console.log('111写入');
})
可以发现,是顺序写入的,异步为什么顺序呢,因为createWriteStream再内存中有缓冲,会一个个取,不是代码的表象
*/
- pipe(异步的)
let fs=require('fs');
let rs=fs.createReadStream('./1.txt',{
highWaterMark:4
});
let ws=fs.createWriteStream('./b.txt',{
highWaterMark:1
})
rs.on('data',function(chunk) {
let flag=ws.write(chunk);
if (!flag) {
rs.pause();//暂停读流
}
})
ws.on('drain',function() {
console.log('干了');
rs.resume();//继续读取
})
// rs.pipe(ws); 异步的 一行等于上面两个回调,默认会调用可写流的write和最终会调用end方法
//干了:输出三次
// let {Readable,Writable}=require('stream');
//内部就是靠Readable,Writable实现的,具体可以调试源码
//图3
补充一个第三方库(iconv-lite)
node.js当中的Buffer对象支持的编码格式的种类有限,大概有ascii、utf8、utf16le、ucs2、base64、binary、hex。不支持GBK的编码形式。对于windows系统来说,由于历史原因,许多文件默认的编码格式均为GBK。这个包可以解决node不支持gbk的问题
判断文件是否存在
不推荐使用 fs.exists 可以选择 fs.stat 或 fs.access
- fs.exists示例
不符合错误优先的原则,而且新版本被废弃,同步版本的倒是可以使用 existSync
fs.exists('/etc/passwd', (exists) => {
console.log(exists ? '存在' : '不存在');
});
另外一个是 不推荐在 fs.open()、 fs.readFile() 或 fs.writeFile() 之前使用 fs.exists() 判断文件是否存在,因为这样会引起 竞态条件,如果是在多进程下,程序的执行不完全是线性的,当程序的一个进程在执行 fs.exists 和 fs.writeFile() 时,其它进程是有可能在这之间更改文件的状态,这样就会造成一些非预期的结果。
- 不推荐
(async () => {
const exists = await util.promisify(fs.exists)('text.txt');
console.log(exists);
await sleep(10000);
if (exists) {
try {
const res = await util.promisify(fs.readFile)('text.txt', { encoding: 'utf-8' });
console.log(res);
} catch (err) {
console.error(err.code, err.message);
throw err;
}
}
})();
- 推荐
(async () => {
try {
const data = await util.promisify(fs.readFile)('text.txt', { encoding: 'utf-8' });
console.log(data);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('File does not exists');
} else {
throw err;
}
}
})();
目前 fs.exists 已被废弃,另外需要清楚, 只有在文件不直接使用时才去检查文件是否存在
,下面推荐几个检查文件是否存在的方法。
- 使用 fs.stat
fs.stat 返回一个 fs.Stats 对象,该对象提供了关于文件的很多信息,例如文件大小、创建时间等。其中有两个方法 stats.isDirectory()、stats.isFile() 用来判断是否是一个目录、是否是一个文件。
const stats = await util.promisify(fs.stat)('text1.txt');
console.log(stats.isDirectory()); // false
console.log(stats.isFile()); // true
若只是检查文件是否存在,推荐使用下面的 fs.access。
- 使用 fs.access
fs.access 接收一个 mode参数可以判断一个文件是否存在、是否可读、是否可写,返回值为一个 err 参数。
const file = 'text.txt';
// 检查文件是否存在于当前目录中。
fs.access(file, fs.constants.F_OK, (err) => {
console.log(`${file} ${err ? '不存在' : '存在'}`);
});
// 检查文件是否可读。
fs.access(file, fs.constants.R_OK, (err) => {
console.log(`${file} ${err ? '不可读' : '可读'}`);
});
// 检查文件是否可写。
fs.access(file, fs.constants.W_OK, (err) => {
console.log(`${file} ${err ? '不可写' : '可写'}`);
});
// 检查文件是否存在于当前目录中、以及是否可写。
fs.access(file, fs.constants.F_OK | fs.constants.W_OK, (err) => {
if (err) {
console.error(
`${file} ${err.code === 'ENOENT' ? '不存在' : '只可读'}`);
} else {
console.log(`${file} 存在,且可写`);
}
});
同样的也不推荐在 fs.open()、 fs.readFile() 或 fs.writeFile() 之前使用 fs.access()或fs.stat() 判断文件是否存在,会引起竞态条件。这种情况直接读写文件即可,通过回调参数做处理
Promise(不全面,后期补充尚硅谷的视频)
- promise本身不是异步,then是异步的
- promise resolve成功 reject 失败 pending 等待
- 每一个promise的实例上都有一个then方法,then方法有俩参数
- promise中发生错误,就会直接执行失败态
- Promise只有一个参数,叫excutor执行器,默认new时就会调用
let p = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(3);
//注意这个回调内部代码如果报错,则即使不调用reject也会走错误回调
//例如:throw new Error();
})
p.then((value) => {
//then方法异步调用
console.log('成功');
}, (err) => {
console.log(err);
})
console.log(2);
//输出依次 1 3 2 成功
- 多个then
let p = new Promise((resolve, reject) => {
resolve('成功')
})
p.then(data => {
console.log(data);
})
p.then(data => {
console.log(data);
})
p.then(data => {
console.log(data);
})
//输出三个成功 一个promise可以then多次
- 多层then的区别(待补充)
- Promise.all
并发,全成功才成功,一个失败就全失败,而且事件返回值顺序不会乱
//并发-全成功才成功
Promise.all([read('1.txt'), read('2.txt')]).then(([r1, r2]) => {
//此时r1就是1.txt返回值,r2是2.txt返回值,不论哪个先返回,顺序不会变
console.log(r1, r2);
}, err => {
//只要有一个错误就直接错误
console.log(err);
})
- Promise.race
赛跑 谁先回来用谁, 处理多个请求只取最快的一个
//race
Promise.race([read('1.txt'), read('2.txt')]).then(data => {
console.log(r1, r2);
}, err => {
console.log(err);
})
- Promise.resolve 上来就成功
Promise.resolve('123').then(data => {
console.log(data);
})
- Promise.reject 上来就失败
Promise.reject('123').then(null, data => {
//null是忽略错误信息,也可以不忽略
console.log(data);
})
Obecjt
let name={name:'zq'};
let age={age:9};
let obj=Object.assign(name,age);//合并对象,浅拷贝,不常用,因为es7出现下面
//一般都是解构赋值就行
console.log({...name,...age}); //{ name: 'zq', age: 9 }
Obejct的简洁方式
let name="qiang",age=20;
let info={name,age};
let str="hehe";
let obj={
//fn:function(){}
fn(){}, //等效于上面
//属性名是字符串,属性名使用[]里面可以写变量
[str]:name,
["my"+str]:name,
"str":name
}
console.log(obj.str,obj.hehe,obj.myhehe);//qiang qiang qiang
Object的方法扩展
//1.Object ()将参数变成对象
console.log(Object(1));//Number {1}
console.log(Object(true));//Boolean {true}
//2.Object.is 判断两个值是否相等,除了下面两种,其他和===相同
//=== NaN跟NaN不相等 -0===0 true
console.log(Object.is(NaN,NaN))//true
console.log(Object.is(-0,0))//false
//3.Object.assign(obj1,obj2) 合并对象 把obj1合并到obj2上,返回obj1
let obj1={name:"qiang"};
let obj2={age:12};
let obj=Object.assign(obj1,obj2);
console.log(obj); //{name: "qiang", age: 12}
console.log(obj1);//{name: "qiang", age: 12}
//ES7提供了对象的扩展运算符... 如果属性重复,只保留一份
let a1={name:"hehe"};
let a2={info:"ascaa"};
let a={...a1,...a2};
console.log(a); //{name: "hehe", info: "ascaa"}
//4. Obejct.getOwnPropertyDescriptor 获取一个对象某个属性的描述
console.log(Object.getOwnPropertyDescriptor("123","length"));//{value: 3, writable: false, enumerable: false, configurable: false}
//其他
Object.keys
Object.values
Object.entries
Object的get和set
let obj={
_name:"AA",
get name(){
// console.log("获取");
return this._name;
},
set name(val){
// console.log("设置");
// console.log(this==obj);//true
this._name=val;
}
}
console.log(obj.name); //AA
obj.name="测试";
console.log(obj.name);//测试
错误案例:
let obj={
name:"AA",
get name(){
// console.log("获取");
return this.name;
},
set name(val){
// console.log("设置");
// console.log(this==obj);//true
this.name=val;
}
}
obj.name="测试";
说明:这样会无限死循环,set利用是自己设置自己
Object.setPrototypeOf(一般用于继承静态方法)
Object.setPrototypeOf方法的作用与proto相同,用来设置一个对象(注意是对象)的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法,避免obj1.proto=obj2这样不优雅的写法;obj1.proto其实就是指向Object1的prototype属性(对象)
let obj1={name:'zq'};
let obj2={age:9};
obj1.__proto__=obj2;
console.log(obj1.age); //9
//es6
Object.setPrototypeOf(obj1,obj2);//等效于obj1.__proto__=obj2
Object.getPrototypeOf(obj1);//等效于obj1.__proto__
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。
- Object.getPrototypeOf()
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
也可以这样修改原型链指向
let obj={
name:'zq',
getPName(){
return super.name //返回__proto__上面的name,如果有的话
},
__proto__:obj1
}
Object.create()详解
Object.create() 方法会使用指定的原型对象及其属性去创建一个新的对象。
- proto
一个对象,应该是新创建的对象的原型
- propertiesObject
可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与Object.defineProperties()的第二个参数一样)。注意:该参数对象不能是 undefined,另外只有该对象中自身拥有的可枚举的属性才有效,也就是说该对象的原型链上属性是无效的。
- 抛出异常
如果 propertiesObject 参数不是 null 也不是对象,则抛出一个 TypeError 异常
var o;
// 创建一个原型为null的空对象
o = Object.create(null);
o = {};
// 以字面量方式创建的空对象就相当于:
o = Object.create(Object.prototype);
o = Object.create(Object.prototype, {
// foo会成为所创建对象的数据属性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});
补充:Object.create(null)创建的对象是不会以Object的原型为构造函数的,因为这个对象就没有原型。和Object.create(null)是有本质区别的
通过Obejct.create实现继承,但是又不影响之前的功能
//例如:想添加特定功能,但是又不想影响Array的原型
let oldProto = Array.prototype;
let update = () => {
console.log('更新');
}
let proto = Object.create(oldProto);//脱离原来的原型,创建一个新的原型,但是方法什么的都有
['push', 'unshift', 'shift'].forEach(method => {
proto[method] = function (...args) {
//新逻辑:这也算是aop的实现
update();
//调用老方法
oldProto[method](...args);
}
})
function observer(obj) {
if (Array.isArray(obj)) {
obj.__proto__ = proto;
}
}
let arr = [];
observer(arr);
arr.push(1)//更新
原型
- 任何对象都有proto属性
- 任何函数都有prototype属性,同理也有proto属性
- animal的proto属性指向Animal的prototype
- Animal的prototype(原型)有个constructor属性又指回Animal
function Animal() {
this.type='1'
this.a='a'
}
Animal.prototype.type='11'
Animal.prototype.b='b'
let animal=new Animal();
console.log(animal.__proto__===Animal.prototype); //true
console.log(animal.type);//1
console.log(animal.__proto__.__proto__===Object.prototype);//true
console.log(Animal.prototype.constructor===Animal);//true
console.log(Object.prototype.__proto__);//null
特殊情况
- Function和Object既可以充当对象也可以充当函数
- 约定:Function.proto===Function.prototype
- 约定:Object.proto===Function.prototype
- 所以:Object.proto===Function.proto
console.log(Object.__proto__===Function.prototype);//true
console.log(Object.__proto__===Function.__proto__);//true
//判断属性是在原型上还是实例内部
console.log(animal.hasOwnProperty('a')); //true
console.log(animal.hasOwnProperty('b')); //false
//in关键字 会判断这个属性是否属于原型 或者实例上的属性
console.log('a' in animal);
继承es5
原型链继承
缺点:无法传递new对象的时候参数
基本案例:
function Person(name,age){
this.name=name;
this.age=age;
this.run=function(){
console.log(this.name,this.age);
}
}
Person.prototype.work=function(){
console.log("work");
}
function Student(name,age){
}
Student.prototype=new Person();
var s=new Student("qiang",12);
s.run();//undefined undefined
s.work();//work
对象冒充继承
无法使用静态方法和原型上面的方法,可以使用私有属性
基本案例:
function Person(name,age){
this.name=name;
this.age=age;
this.run=function(){
console.log(this.name,this.age);
}
}
function Student(name,age){
Person.call(this,name,age);
}
//Student.prototype=new Person();
var s=new Student("qiang",12);
s.run(); //qiang 12
s.work();//报错
综合案例(原型链继承和对象冒充继承一起用)
function Person(name,age){
this.name=name;
this.age=age;
this.run=function(){
console.log(this.name,this.age);
}
}
Person.prototype.work=function(){
console.log("work");
}
function Student(name,age){
Person.call(this,name,age);
}
Student.prototype=new Person();
var s=new Student("qiang",12);
s.run(); //qiang 12
s.work();//work
继承es6
es5的继承
- 公有(原型上面的):Object.create 一般就是prototype
- 私有:call
- 静态:Object.setPrototypeOf 等效于(obj1.__proto__=obj2)
- 类只能new,不能直接调用
- 类可以继承公有私有和静态方法
- 父类构造函数返回引用类型,会把这个引用类型作为子类的this
class Parent{
constructor(){
this.name='pp';//私有
// return {};
}
}
class Child extends Parent{
constructor(){
super();
this.age=9;//私有属性
}
}
let c=new Child();
console.log(c); 父类返回的{}会和子类中this合并,最终实例是{ age: 9 }
使用示例
class Parent{
constructor(){
this.name='pp';//私有
// return {};
}
static b(){
return 2;
}
}
class Child extends Parent{
constructor(){
super();//如果想继承父类私有属性,必须写这句, 相当于Parent.call(this); 私有继承
this.age=9;//私有属性
}
//es6只支持静态方法,不支持静态属性;es7的语法,不同版本支持力度不同,不讨论
static a(){
//静态方法
}
// static b=9; 不支持
smoking(){
//原型上的方法
}
}
let c=new Child();
// console.log(c.b); //报错,不支持静态属性
// console.log(c.name);
// console.log(Child.b); //[Function: b]
实现
//检测实例是不是new出来的,
function __classCallCheck(instance, constructor) {
//说白了检测传递进来的this是不是构造的实例,例如直接调用,this其实是window或者global
if (!(instance instanceof constructor)) {
throw new Error('类不能直接作为函数调用');
}
}
function definePropertys(targets, arr) {
for (let i = 0; i < arr.length; i++) {
Object.defineProperty(targets, arr[i].key, {
...arr[i],
configurable: true, //可配置
enumerable: true,//可查询
writable: true//可修改
})
}
}
//构造函数, 原型方法的描述 静态方法的描述
function __createClass(constructor, protoPropertys, staticPropertys) {
if (protoPropertys.length > 0) {
definePropertys(constructor.prototype, protoPropertys);
}
if (staticPropertys.length > 0) {
definePropertys(constructor, staticPropertys);
}
}
let Parent = function () {
//逻辑
function P() {
__classCallCheck(this, P);
this.name = 'parent'; //私有属性
__createClass(P, [
{
key: 'eat',
value: function () {
console.log('eat');
}
}
], [
{
key: 'b',
value: function () {
console.log('b');
}
}
]);
}
return P;
}();
let p = new Parent();
// p.eat();
// Parent.b(); //但是此时只是简单案例,直接p.b会报错
//这样的结果就是
function _inherits(subClass, superClass) {
//继承公有属性-说白了设置prototype
// Object.create的参数二是可迭代
subClass.prototype = Object.create(superClass.prototype, {
constructor: { value: subClass }
})
//继承静态方法-说白了设置__proto__
Object.setPrototypeOf(subClass, superClass);
}
let Child = (function (Parent) {
//先实现继承父类的公有属性和静态方法
_inherits(C, Parent);
function C() {
__classCallCheck(this, C);
//继承私有属性
let obj = Parent.call(this);
let that = this;
if (typeof obj === 'object') {
that = obj;
}
that.age = 9;//解决父类构造返回引用类型的问题
return that;
}
return C;
})(Parent);
// let child=new Child();
Child.b()
node的util包
- util.promisify 把方法转promise
<!--ncp 第三方复制包-->
let ncp = require('ncp');
let path = require('path');
const { resolve } = require('path');
let {inherits,inspect,isNumber}=require('util');
// let {promisify}=require('util');
//promisify包的实现原理
// const promisify = fn => (...args) => {
// return new Promise((resolve, reject) => {
// fn(...args, function (err) {
// if (err) reject(err);
// resolve();
// })
// })
// }
/**
* 上面是简略写法,因为使用发现,promisify(ncp)返回值还要再被调用,
* 所以返回的不应该是promise,调用之后返回才是promise,所以封装了两层
*/
const promisify = fn => {
return (...args) => {
return new Promise((resolve, reject) => {
fn(...args, function (err) {//只针对node,因为node是err-first的形式
if (err) reject(err);
resolve();
})
})
}
}
ncp = promisify(ncp);
/**
* 这种第三方的包,很多都是老node的写法,callback转promise就利用到了promisify
* ncp(path.resolve(__dirname,'1.js'),path.resolve(__dirname,'11.js'),err=>{})
*/
// (async () => {
// await ncp(path.resolve(__dirname, '1.js'), path.resolve(__dirname, '11.js'));
// console.log('拷贝成功');
// })();
- util util.inherits 继承
let {inherits,inspect}=require('util');
function Parent() {
}
function Child() {
Person.call(this);
}
/**
三种继承公共方法的方式
Child.prototype.__proto__=Parent.prototype;
Object.setPrototypeOf等效于Reflect.setPrototypeOf
Reflect.setPrototypeOf(Child.prototype,Parent.prototype);
Child.prototype=Object.create(Parent.prototype);
*/
inherits(Child,Parent);//继承公共属性
// inspect:显示隐藏属性
//例如:Array.prototype是不可枚举的,但是可以通过Inspect实现
// inspect(Array.prototype,{showHidden:true})
实现发布订阅模块
/**
* 自己实现发布订阅
*/
function EventEmitter() {
//Object.create(null) 这样创建对象是没有属性的,如果{}会发现内部还是很多属性的
this._events=Object.create(null);
}
EventEmitter.prototype.on=function(eventName,callback) {
if (!this._events) this._events=Object.create(null);
if (eventName!=='newListener') {
this.emit('newListener',eventName);
}
if (this._events[eventName]) {
this._events[eventName].push(callback)
}else{
this._events[eventName]=[callback];
}
}
//只执行一次
EventEmitter.prototype.once=function(eventName,callback) {
//绑定 执行后,删除
let one=()=>{//2. 触发once函数
callback();//触发原有逻辑
//删除自己
this.off(eventName,one);//再将one删除掉,其实就是传递的函数,指向同一个地址===比较是否相同
}
one.l=callback;
this.on(eventName,one);//1.先绑定
}
EventEmitter.prototype.off=function (eventName,callback) {
if (this._events[eventName]) {
this._events[eventName]=this._events[eventName].filter(fn=>{
return fn!==callback&&fn.l!==callback;
})
}
}
EventEmitter.prototype.emit=function(eventName,...args) {
if (this._events[eventName]) {
this._events[eventName].forEach(fn => fn(...args));
}
}
module.exports=EventEmitter;
<!--使用-->
//发布订阅模块
let EventEmitter=require('./3');
// let EventEmitter=require('events');
//on emit
// let e=new EventEmitter();
// let listener1=data=>{
// console.log(data);
// }
// let listener2=data=>{
// console.log(data);
// }
// e.on('hello',listener1);
// e.once('hello1',listener2);
// e.once('hello1',listener2);
// e.emit('hello','data');
// e.emit('hello1','data1');
let util=require('util');
function Girl() {
}
util.inherits(Girl,EventEmitter);
let girl=new Girl();
girl.on('girl',data=>{
console.log(data); //girl
})
girl.on('newListener',type=>{
//监听用户做了哪些监听-需要特殊实现,就有点类似于过滤器,监听都进来
console.log(type);
})
girl.emit('girl','gril')
补充一道面试题
function fn() {
return new Promise((resolve, reject) => {
resolve([1, 2, 3]);
})
}
async function getData() {
await fn();
console.log(1);
}
getData();
该题目要区分node环境还是浏览器环境
- node环境
function fn() {
return new Promise((resolve, reject) => {
resolve([1, 2, 3]);
})
}
async function getData() {
//resolve中如果放一个(return)promise 会等待这个promise成功后再继续执行
//此处resolve(fn())会等待fn执行完毕,fn是promise执行完相当于编译出一个then,而该句后面还有then,相当于两个then
new Promise((resolve, reject) => resolve(fn())).then(() => {
console.log(1);
})
}
getData();
Promise.resolve().then(data => {
console.log(2);
})
//可以理解成,node中最终 console.log(1);是发生在,第二次清空微任务队列的时候
// 2 1
node环境其实相当于转化成两层promise.then
- 浏览器环境
做了优化,只有一层promise.then
Promise.resolve(fn()).then(()=>{
console.log(1);
})
// 1 2
JS的with关键字
例如模板引擎的传参,直接再模板里面可用就用到了这个原理
with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式。也就是在指定的代码区域, 直接通过节点名称调用对象.用变量的作用域和作用域链(即一个按顺序检索的对象列表)来进行变量名解析,而with语句就是用于暂修改作用域链的,其语法为with(object) statement.
该语句可以有效地将object添加到作用域链的头部,然后执行statement,再把作用域链恢复到原始状态。
with(frames[1].document.forms[0]){
//此处直接访问表单元素。例如:
name.value = ‘小小子’;
address.value = ‘http://www.xiaoxiaozi.com/’;
email.value =’yufulong@gmail.com’;
}
表单属性名前的前缀——frames[1].document.forms[0] 就不用重复写。
这个对象不过是作用域链的一个临时部分,当JavaScript需要解析像 address这样的标识符时就会自动搜索它
但是with语句有个很大的缺陷:
使用with语句的JavaScript代码很难优化,因此它的运算速度比不使用with语句的等价代码要慢得多。
而且,在with语句中的函数定义和变量初始化可能会产生令人惊讶的、相抵触的行为。(虽然作者没有举例,不过这话可够吓人的)。
因此我们避免使用with语句。
小数转换(为什么js中小数相加不准确)
- 小数在内存中也是按照二进制存储的
- 进制转换 小数*2 取整法
//0.1 转二进制 (可通过工具计算,或者网上的网站)
//0.0001100110011001100110011001100110011001100110011001101
//计算过程如下:
0.1 *2 =0.2=》0
0.2 *2=0.4=》0
0.4*2=0.8=》0
0.8*2=1.6=》1
0.6*2=1.2=>1
0.2*2=0.4=>0
就会发现永远都是无穷尽的,最后只能进一位作为结果
所以才会出现js中0.1+0.2值不是0.3 因为运算先转二进制,二进制转换过程已经是真,所以结果自然不是0.3
进制转换
//将十进制转换成其他进制 255 0xff 0b 0o
console.log( (0xff).toString(2));//值变成了字符串
//进制转化,将任意进制转化成10进制
console.log(parseInt('0xff',16));
Base64加密
- base64二进制的值不能超过64(核心是进制的转化)
- base64可以反解 加密 加密后特殊人才可以解密
- 在浏览器header中,任意的url都可以采用base64,前端实现文件预览也是
- 转码后的结果 比原来的内容大(经 base64 编码后的文件大约变大(长) 1/3 )
- 优势:base64就是编码转化,不需要发送http请求,大小会比以前大
- base64的实现过程
let str='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
str+='ABCDEFGHIJKLMNOPQRSTUVWXYZ'.toLowerCase();
str+='0123456789+/';
let result=str[0b111001]+str[0b001011]+str[0b110110]+str[0b001111];
console.log(result); //5L2P 可以通过base64解码发现就是 住 这个字
// console.log(Buffer.from('住').toString('base64')); //5L2P
因为base64编码的数据不能超过64 所以上面 3*8形式要转换成6*4
111001 001011 110110 001111 这也是base64名字的由来
因为6个字节最大值是64 而按照8*3分的话,8个字节最大值超过64
base64 编码:用 64 个可打印字符来表示二进制数据(例如图片)的一种编码方案,它能把所有的二进制数据转换成字符串形式保存或显示。这写可打印字符是英文字母、数字和 2 个符号,一共 64 个,编号 063。063 对应的二进制数:000000 ~ 111111 ,即使用 6 位二进制就能表示一个 base64 编码字符。base64 编码过程:对二进制数据进行处理,每 3 个字节一组,一组就是 3 * 8 = 24 位,再分为 4 组,每组就是 6 位,这样每组就刚好可以用一个 base64 可打印字符来表示,一共 4 组。
这样原来的 3 个字节进过编码后会变成 4 个字节。文件大约变大(长) 1/3 。
Buffer
Buffer存储二进制数据
打印结果,其实就是十六进制
console.log(Buffer.from('住')); //<Buffer e4 bd 8f>
console.log(0xe4.toString(2)); //11100100
console.log(0xbd.toString(2)); //10111101
console.log(0x8f.toString(2)); //10001111
buffer的声明方式
- buffer不能扩展大小
let buf=Buffer.allocUnsafe(5);
buf.fill(0);//默认alloc就是填充0 所以这两步等效于alloc
//allocUnsafe+fill =>alloc
buf=Buffer.from([100,120,130]);//很少用到
console.log(buf); //<Buffer 64 78 82> 都是十六进制
buf=Buffer.from('zf');
console.log(buf);
常见方法
let arr=[[1,2,3],3,4,5];
let newArr=arr.slice(0);//浅拷贝
newArr[0][1]=100;
console.log(arr); //[ [ 1, 100, 3 ], 3, 4, 5 ]
let buffer=Buffer.from('zf');
let newBuffer=buffer.slice(0); //浅拷贝
newBuffer[0]=100;
// console.log(buffer);
//判断是不是buffer
console.log(Buffer.isBuffer(buffer));//true
//copy 通过拷贝去实现
let buff=Buffer.alloc(6);
let b1=Buffer.from("珠");
let b2=Buffer.from("峰");
//目标buffer 目标开始位置 源开始 源结束
b1.copy(buff,0,0,3);
b2.copy(buff,3,0,3);
console.log(buff.toString());//珠峰
//concat 拼接- 第二个参数length,不填写默认是传参长度的和,传递的话大于则补了一堆0,小于则截取
console.log(Buffer.concat([b1,b2]).toString());//珠峰
console.log(Buffer.concat([b1,b2]).toString('base64'));//54+g5bOw
- concat实现原理
Buffer.concat=function(list,length=list.reduce((a,b)=>a+b.length,0)) {
let buff=Buffer.alloc(length);
let offset=0;
list.forEach(b=>{
b.copy(buff,offset);
offset+=b.length;
});
return
}
func.length和arguments.length
length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,那些已定义了默认值的参数不算在内,比如function(xx = 0)的length是0.
函数内部:arguments.length 是函数被调用时实际传参的个数
V8垃圾回收
V8内存限制
- 在64位操作系统可以使用1.4G内存
- 在32位操作系统可以使用0.7G内存
V8内存管理
- js对象都是通过V8进行分配管理内存的
- process.memoryUsage返回一个对象,包含了Node进程的内存占用信息
- heapUsed一般越来越大就是发生内存泄漏了
- 内存回收都是针对堆的
为什么限制内存大小
- 因为V8的垃圾收集工作原理导致的,1.4G内存完全一次垃圾收集需要1s以上
- 这个暂停时间称为Stop The World,在这个期间,应用的性能和响应能力会下降
如何打开内存限制
- 一旦初始化成功,生效后不能再修改
- -max-new-space-size,最大new space大小,执行scavenge回收,默认16m,单位kb
- -max-old-space-size,最大old space大小,执行MarkSweep回收,默认1G,单位kb
node --max-old-space-size=2000 app.js 单位是M
node --max-new-space-size=1024 app.js 单位是KB
V8的垃圾回收机制
- V8是基于分代的垃圾回收
- 不同垃圾回收机制也不一样
- 按存活的时间分为新生代和老生代
分代
- 年龄小的是新生代,由From区域和To区域两个区域组成
- 在64位系统里,新生代内存是32M,From区域和To区域各占用16M
- 在32位系统里,新生代内存是16M,From区域和To区域各占用8M
- 年龄大的是老生代,默认情况如下
- 64位系统下老生代内存是1400M
- 32位系统下老生代内存是700M
注意:垃圾回收都是针对堆内存
新生代垃圾回收
- 新生代区域一分为二,每个16M,一个使用,一个空闲
- 开始垃圾回收的时候,会检查FROM区域中的存活对象,如果还活着,拷贝到TO空间,完成后释放空间
- 完成FROM和TO互换
- 新生代扫描的时候是一种广度优先的扫描策略
- 新生代的空间小,存活对象少
- 当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较长的对象会被移动到老生代,这个移动过程被称为晋升或者升级
- 经过5次以上的回收还存在
- TO空间的使用占比超过25%,或者超大对象
引用计数法(怎么判断是否还活着)
- 语言引擎有一张表,保存了内存里面所有资源的引用次数
- 如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放
GC流程
分为扫描指针和分配指针,分配指针指向下一个可用的内存地址,当两者重合则GC结束
老生代
- mark-sweep(标记清除)mark-compact(标记整理)
- 老生代空间大,大部分都是活着的对象,GC耗时长
- 在GC期间无法响应,STOP-THE-WORLD
-
V8有一个优化方案,增量处理,把一个大的暂停替换成多个小的暂停INCREMENT-GC
Mark-sweep(标记清除)
- 标记活着的对象,随后清除在标记阶段没有标记的对象,只清理死亡对象
- 问题在于清除后会出现内存不连续的情况,这种内存碎片会对后续内存的分配产生影响
- 如果要分配一个大对象,碎片空间无法分配
Mark-compact(标记整理)
- 标记死亡后会对对象进行整理,活着的对象向左移动,移动完成后直接清理掉边界外的内存
Incremental marking(增量标记)
- 以上
三种(包括新生代)
回收时都需要暂停程序执行,收集完成之后才能恢复,STOP-THE-WORLD在新生代影响不大,但是老生代影响很大 - 增量标记就是把标记改为增量标记,把一口气的停顿拆分成多个小步骤,做完一步程序运行一会,垃圾回收和应用程序运行交替进行,停顿时间可以减少1/6到左右
三种垃圾回收算法对比
Mark-Compact需要移动对象,执行速度不快,V8主要使用Mark-Sweep,空间不足以应对新生代升级过来的对象时候才会使用Mark-Compact