Node补充

2020-08-21  本文已影响0人  强某某

global

node再任意模块都可以访问global,但是默认声明的属性不是挂载在global下面,例如:let a=1;=>global.a是underfined

console.log(this===global); //false
console.log(this===module.exports); //true
(function() {
    console.log(this);
})();

这样自执行函数,默认是global点的形式调用的,自然内部的this就是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(进程)

参数一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
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 这样可以不区分系统设置

事件轮询

1.jpg

上图全是宏任务,poll阶段:例如读文件写文件

注意:此处只讨论node环境下的事件循环,新版本node和浏览器基本一样,但是和浏览器没什么关系,是自己实现的

事件循环的步骤:

2.jpg

微任务

补充

 window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,
 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,
 该回调函数会在浏览器下一次重绘之前执行
 
 宏任务:script  ajax 事件  requestFrameAnimation
 setTimeout setInterval setImmediate 
 MessageChannel      I/O       UI rendering
process.nextTick和setImmediate区别

这正是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区分不同情况详解

 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

3.jpg
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再内存中有缓冲,会一个个取,不是代码的表象
*/
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('/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 已被废弃,另外需要清楚, 只有在文件不直接使用时才去检查文件是否存在,下面推荐几个检查文件是否存在的方法。

const stats = await util.promisify(fs.stat)('text1.txt');
console.log(stats.isDirectory()); // false
console.log(stats.isFile()); // true

若只是检查文件是否存在,推荐使用下面的 fs.access。

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(不全面,后期补充尚硅谷的视频)

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 成功
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多次

并发,全成功才成功,一个失败就全失败,而且事件返回值顺序不会乱

//并发-全成功才成功
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);
})

赛跑 谁先回来用谁, 处理多个请求只取最快的一个

//race 
Promise.race([read('1.txt'), read('2.txt')]).then(data => {
    console.log(r1, r2);
}, err => {
    console.log(err);
})
Promise.resolve('123').then(data => {
    console.log(data);
})
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(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() 方法会使用指定的原型对象及其属性去创建一个新的对象。

一个对象,应该是新创建的对象的原型

可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与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)//更新

原型

1.jpg
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

特殊情况

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)
  1. 类只能new,不能直接调用
  2. 类可以继承公有私有和静态方法
  3. 父类构造函数返回引用类型,会把这个引用类型作为子类的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包

<!--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('拷贝成功');
// })();
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环境还是浏览器环境

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.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中小数相加不准确)

//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加密

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的声明方式

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
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内存限制

V8内存管理

4.jpg

为什么限制内存大小

如何打开内存限制

node --max-old-space-size=2000 app.js 单位是M
node --max-new-space-size=1024 app.js 单位是KB

V8的垃圾回收机制

分代

5.jpg

注意:垃圾回收都是针对堆内存

新生代垃圾回收

引用计数法(怎么判断是否还活着)

6.jpg

GC流程

7.jpg 8.jpg

分为扫描指针和分配指针,分配指针指向下一个可用的内存地址,当两者重合则GC结束

老生代

Mark-sweep(标记清除)

Mark-compact(标记整理)

Incremental marking(增量标记)

三种垃圾回收算法对比

10.jpg

Mark-Compact需要移动对象,执行速度不快,V8主要使用Mark-Sweep,空间不足以应对新生代升级过来的对象时候才会使用Mark-Compact

上一篇 下一篇

猜你喜欢

热点阅读