JS学习一波

Posted by 梁小生 on 2019-04-12

JS学习一波

最近看了一下《JavaScript高级编程》、《Node深入浅出》,挑了一些自己认为重要的、不熟悉的地方做了一下笔记。

《Node深入浅出》笔记

起源

期初:一个外国人”Ryan Dahl”想写一个高性能的Web服务器,在他失败了几次以后,找到了几个要点:事件驱动、异步 I/O。挑语言的时候,经历了这样的选择:

  • C语言的开发门太高,不会有ܹ多的开发者能把它用于日常的务开发,放弃了。
  • Haskell(我不知道这个是啥)”Ryan Dahl”觉得这个好难,放弃了。
  • Ruby性能不咋滴,放弃了。

相比之下JavaScript:

  • 比 C的开发门槛低;

  • 导入异步I/O库没有啥压力;

  • Chrome引擎V8牛皮(高性能);

  • 符合事件驱动。

因此JavaScript成为了Node的实现语言。而不是说直接想JavaScript写后端,这中间是有这样一个过程的。

特点

  1. 异步I/O(利用回调函数)
  2. 单线程:
    • Node是单线程,JavaScript和其他线程不能共享任何状态。所以没有死锁,没有上下文切换
    • 不能利用多核CPU
    • 大量计算占用CPU会导致无法调用异步I/O (长时间的计算,将导致CPU时间片不能释放)
    • 如果有大计算阻塞UI,可以使用Web Workers工作线程。但只能通过消息传递的方式来传递结果,但是工作线程不能访问到UI主线程。我的理解是:C#、Java等主线程和子线程常用的思想是不能在这里实现。

CommonJS

之前有人问我CommonJS 和 es 有啥区别,我是不知道CommonJS 和 es有啥区别。刚好看到这里,如下图:

CommonJS和es关系--来自《node.js深入浅出》

因为ECMAScript仅定了部分核心代码,对于文件系统, I/O等库没有相关标准。缺点有如下:

  • 没有模块系统
  • 标准库较少
  • 没有标准接口
  • 缺乏包管理系统(没有自动加载能力和安装依赖能力)

  • Node借鉴 CommonJS的Modules规范实现了一个模块系统: NPM。一个模块文件中有:require、exports、module三个变量

  • Node性能不够的时候呢,可以编写c/c++扩展模块

异步I/O

  • Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中。在Node中,无论是linux还是Windowsࣰ中,内部完成I/O任务的是另有线程的。

  • 事件循环:在进程启动时, Node会会创建一个类似于while(true)的循环,判断是否有事件待处理,有就取出事件和相关回调函数。如果存在关联的回调函数,就执行他们,然后进入下一个循环。没有事件就退出进程。如下图:

异步调用--来自《node.js深入浅出》

  • 浏览器采用了类似的机制。都有对应的观察者。在Node中,网络请求、文件I/O都有自己的观察者。事件循环从观察者中读取数据。(给我的感觉是,有观察者类似于队列,然后一个循环不断从队列里头去读数据,有点读写分离的感觉)

异步编程

  • promise是咋整的,这里就不展开了,自己看吧。

内存控制

  • V8堆内存的最大值在64位系统上为1464 MB, 32系统则为732 MB

  • Scavenge算法

    Scavenge算法进行垃圾回收。它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这 些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空 间和To空间的角色发生对换。 简而言之, 在垃圾回收的过程中, 就是通过将存活对象在两个 semispace 空间之间进行复制。

    Scavenge算法通过牺牲空间换时间的算法非常适合生命周期短的新生代,但是,当一个对象经过多次复制,生命周期较长的时候或则To空间不足的时候,对象会被分配到进入到老生代中,需要采用新的算法进行垃圾回收。

  • Mark-Sweep算法

    Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge 中只复制活着的对象,而 Mark-Sweep 只清理死亡对象。
    Mark-Sweep 在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。Mark-Compact 对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存

  • Mark-Compacts算法

    Mark-Compacts算法:mark-compact对于标记死亡对象的整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。(简单理解–整理活着对象向一段移动,另一端直接清空)

其他:GC无法立即回收内存中的闭包和全局变量

  • Buffer对象并非通过V8分配,在Node需要处理网络流或者文件I/O流的时候,可能操作字符串不能满足传输,可以用Buffer。Buffer属性指向的是C++层面上的Buffer对象,所以不在V8堆中。

  • Session因为V8内存的问题,不适合放在内部维护,可以放在外部,比如说:redis、Memcached中。

《JavaScript高级编程》笔记

面向对象程序设计

因为接触的多数是面向对象的语言,对js里面的面向对象的实现不是很了解,故一个记录。

  • 原型的关键

    • 所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)
    • 所有的引用类型都有一个’_ _ proto_ _’属性(也叫隐式原型,它是一个普通的对象)
    • 所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)
    • 所有引用类型,它的’_ _ proto_ _’属性指向它的构造函数的’prototype’属性
    • 当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _’属性(也就是它的构造函数的’prototype’属性)中去寻找
  • prototype的作用:如果我们要通过Foo()来创建很多个对象,我们创建出来的每一个对象里面都有showName和showAge方法,会占用很多的资源。而通过原型来实现的话,只需要在构造函数里面给属性赋值,而把方法写在Foo.prototype属性里面。每个对象都可以使用prototype属性里面的showName、showAge方法,并且节省资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Foo(name,age){
    this.name=name;
    this.age=age;
    this.showName=function(){
    console.log("I'm "+this.name);
    }
    this.showAge=function(){
    console.log("And I'm "+this.age);
    }
    }

    如果使用prototype的话,将变成这样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Foo(name,age){
    this.name=name;
    this.age=age;
    }
    Foo.prototype={
    // prototype对象里面又有其他的属性
    showName:function(){
    console.log("I'm "+this.name);//this是什么要看执行的时候谁调用了这个函数
    },
    showAge:function(){
    console.log("And I'm "+this.age);//this是什么要看执行的时候谁调用了这个函数
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 构造函数
    function Foo(name,age){
    this.name=name;
    this.age=age;
    }
    Object.prototype.toString=function(){
    //this是什么要看执行的时候谁调用了这个函数。
    console.log("I'm "+this.name+" And I'm "+this.age);
    }
    var fn=new Foo('小明',19);
    fn.toString(); //I'm 小明 And I'm 19
    console.log(fn.toString===Foo.prototype.__proto__.toString); //true

    console.log(fn.__proto__ ===Foo.prototype)//true
    console.log(Foo.prototype.__proto__===Object.prototype)//true
    console.log(Object.prototype.__proto__===null)//true

    原型链

    创建对象的方式

  • 工厂模式(不能对象的类型)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function createPerson(name, job) { 
    var o = new Object()
    o.name = name
    o.job = job
    o.sayName = function() {
    console.log(this.name)
    }
    return o
    }
    // person1 为 Object
    var person1 = createPerson('Jiang', 'student')
  • 构造函数模式(每个方法都要在每个实例上重新创建一次)

1
2
3
4
5
6
7
8
9
10
function Person(name, job) {
this.name = name
this.job = job
this.sayName = function() {
console.log(this.name)
}
}
// person1 为 Person
var person1 = new Person('Jiang', 'student')
var person2 = new Person('X', 'Doctor')
  • 原型模式 (原型的好处是可以让所有的实例对象共享它所包含的属性和方法,引用类型值会有问题)
    1
    2
    3
    4
    5
    6
    7
    8
    function Person() {
    }
    Person.prototype.name = 'Jiang'
    Person.prototype.job = 'student'
    Person.prototype.sayName = function() {
    console.log(this.name)
    }
    var person1 = new Person()

person2中没有加friends的,输出也是有的

1
2
3
4
5
6
var person1 = new Person()
var person2 = new Person()
person1.friends.push('Van')
console.log(person1.friends) //["Shelby", "Court", "Van"]
console.log(person2.friends) //["Shelby", "Court", "Van"]
console.log(person1.friends === person2.friends) // true

所以,对于引用类型的,尽量是放在对象里头。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name
this.friends = ['Shelby', 'Court']
}
Person.prototype.sayName = function() {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('Van')
console.log(person1.friends) //["Shelby", "Court", "Van"]
console.log(person2.friends) // ["Shelby", "Court"]
console.log(person1.friends === person2.friends) //false

  • 动态原型模式 ,态原型模式将所有信息都封装在了构造函数中,初始化的时候,通过检测某个应该存在的方法时候有效,来决定是否需要初始化原型。只有在sayName方法不存在的时候,才会将它添加到原型中。这段代码只会初次调用构造函数的时候才会执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, job) {
// 属性
this.name = name
this.job = job

// 方法
if(typeof this.sayName !== 'function') {
Person.prototype.sayName = function() {
console.log(this.name)
}
}
}
var person1 = new Person('Jiang', 'Student')
person1.sayName()
  • 寄生构造函数模式,构造函数如果不返回对象,默认也会返回一个新的对象,通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name, job) {
    var o = new Object()
    o.name = name
    o.job = job
    o.sayName = function() {
    console.log(this.name)
    }
    return o
    }
    var person1 = new Person('Jiang', 'student')
    person1.sayName()
  • 稳妥构造函数模式: 稳妥构造函数模式和寄生模式类似,有两点不同:一是创建对象的实例方法不引用this,而是不使用new操作符调用构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name, job) {
    var o = new Object()
    o.name = name
    o.job = job
    o.sayName = function() {
    console.log(name)
    }
    return o
    }
    var person1 = Person('Jiang', 'student')
    person1.sayName()