种瓜南山下

种瓜南山下 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 hexo.xienanbo.com 编辑
编辑

南瓜

个人动态

种瓜南山下 发布了文章 · 2019-03-10

JS模块化编程

前言

什么是模块化?

模块就是实现特定功能的一组方法,而模块化是将模块的代码创造自己的作用域,只向外部暴露公开的方法和变量,而这些方法之间高度解耦。

写 JS 为什么需要模块化编程?
当写前端还只是处理网页的一些表单提交,点击交互的时候,还没有强化 JS 模块化的概念,当前端逻辑开始复杂,交互变得更多,数据量越来越庞大时,前端对 JS 模块化编程的需求就越加强烈。

在很多场景中,我们需要考虑模块化:

  1. 团队多人协作,需要引用别人的代码
  2. 项目交接,我们在阅读和重构别人的代码
  3. 代码审查时,检验你的代码是否规范,是否存在问题
  4. 写完代码,回顾自己写的代码是否美观:)
  5. 不同的环境,环境变量不同

基于以上场景,所以,当前 JS 模块化主要是这几个目的:

  1. 代码复用性
  2. 功能代码松耦合
  3. 解决命名冲突
  4. 代码可维护性
  5. 代码可阅读性

先给结论:JS 的模块化编程经历了几个阶段:

  1. 命名空间形式的代码封装
  2. 通过立即执行函数(IIFE)创建的命名空间
  3. 服务器端运行时 Nodejs 的 CommonJS 规范
  4. 将模块化运行在浏览器端的 AMD/CMD 规范
  5. 兼容 CMD 和 AMD 的 UMD 规范
  6. 通过语言标准支持的 ES Module

先给结论图:

一、命名空间

我们知道,在 ES6 之前,JS 是没有块作用域的,私有变量和方法的隔离主要靠函数作用域,公开变量和方法的隔离主要靠对象的属性引用。

封装函数

在 JS 还没有模块化规范的时候,将一些通用的、底层的功能抽象出来,独立成一个个函数来实现模块化:
比方写一个 utils.js 工具函数文件

//  utils.js
function add(x, y) {
    if(typeof x !== "number" || typeof y !== "number") return;
    return x + y;
}

function square(x) {
    if(typeof x !== "number") return;
    return x * x;
}

<script data-original="./utils.js"></script>
<script>
    add(2, 3);
    square(4);
</script>

通过 js 函数文件划分的方式,此时的公开函数其实是挂载到了全局对象 window 下,当在别人也想定义一个叫 add 函数,或者多个 js 文件合并压缩的时候,会存在命名冲突的问题。

挂载到全局变量下:

后来我们想到通过挂载函数到全局对象字面量下的方式,利用 JAVA 包的概念,希望减轻命名冲突的严重性。

var mathUtils1 = {
    add: function(x, y) {
        return x + y;
    },
}

var mathUtils2 = {
    add: function(x, y, z) {
        return x + y + z;
    },
}

mathUtils.add();

mathUtils.square();

这种方式仍然创建了全局变量,但如果包的路径很长,那么到最后引用方法可能就会以module1.subModule.subSubModule.add 的方式引用代码了。

IIFE
考虑模块存在私有变量,于是我们利用IIFE(立即执行表达式)创建闭包来封装私有变量:

var module = (function(){
    var count = 0;
    return {
        inc: function(){
            count += 1;
        },
        dec: function(){
            count += -1;
        }
    }
})()

module.inc();
module.dec();

这样私有变量对于外部来说就是不可访问的,那如果模块需要引入其他依赖呢?

var utils = (function ($) {
    var $body = $("body"); 
    var _private = 0;
    var foo = function() {
        ...
    }
    var bar = function () {
        ...
    }
    
    return {
        foo: foo,
        bar: bar
    }
})(jQuery);

以上封装模块的方式叫作:模块模式,在 jQuery 时代,大量使用了模块模式:

<script data-original="jquery.js"></script>
<script data-original="underscore.js"></script>
<script data-original="utils.js"></script>
<script data-original="base.js"></script>
<script data-original="main.js"></script>

jQuery 的插件必须在 JQuery.js 文件之后 ,文件的加载顺序被严格限制住,依赖越多,依赖关系越混乱,越容易出错。

二、CommonJS

Nodejs 的出现,让 JavaScript 能够运行在服务端环境中,此时迫切需要建立一个标准来实现统一的模块系统,也就是后来的 CommonJS。

// math.js
exports.add = function(x, y) {
    return x + y;
}

// base.js
var math = require("./math.js");
math.add(2, 3);  // 5

// 引用核心模块
var http = require('http');
http.createServer(...).listen(3000);

CommonJS 规定每个模块内部,module 代表当前模块,这个模块是一个对象,有 id,filename, loaded,parent, children, exports 等属性,module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports 变量。

// utils.js
// 直接赋值给 module.exports 变量
module.exports = function () {
    console.log("I'm utils.js module");
}

// base.js
var util = require("./utils.js")
util();  // I'm utils.js module

或者挂载到 module.exports 对象下
module.exports.say = function () {
    console.log("I'm utils.js module");
}

// base.js
var util = require("./utils.js")
util.say();

为了方便,Node 为每个模块提供一个 exports 自由变量,指向 module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

exports 和 module.exports 共享了同个引用地址,如果直接对 exports 赋值会导致两者不再指向同一个内存地址,但最终不会对 module.exports 起效。

// module.exports 可以直接赋值
module.exports = 'Hello world';  

// exports 不能直接赋值
exports = 'Hello world';

CommonJS 总结:
CommonJS 规范加载模块是同步的,用于服务端,由于 CommonJS 会在启动时把内置模块加载到内存中,也会把加载过的模块放在内存中。所以在 Node 环境中用同步加载的方式不会有很大问题。

另,CommonJS模块加载的是输出值的拷贝。也就是说,外部模块输出值变了,当前模块的导入值不会发生变化。

三、AMD

CommonJS 规范的出现,使得 JS 模块化在 NodeJS 环境中得到了施展机会。但 CommonJS 如果应用在浏览器端,同步加载的机制会使得 JS 阻塞 UI 线程,造成页面卡顿。

利用模块加载后执行回调的机制,有了后面的 RequireJS 模块加载器, 由于加载机制不同,我们称这种模块规范为 AMD(Asynchromous Module Definition 异步模块定义)规范, 异步模块定义诞生于使用 XHR + eval 的开发经验,是 RequireJS 模块加载器对模块定义的规范化产出。

AMD 的模块写法:

// 模块名 utils
// 依赖 jQuery, underscore
// 模块导出 foo, bar 属性
<script data-main="scripts/main" data-original="scripts/require.js"></script>

// main.js
require.config({
  baseUrl: "script",
  paths: {
    "jquery": "jquery.min",
    "underscore": "underscore.min",
  }
});

// 定义 utils 模块,使用 jQuery 模块
define("utils", ["jQuery", "underscore"], function($, _) {
    var body = $("body");
    var deepClone = _.deepClone({...});
    return {
        foo: "hello",
        bar: "world"
    }
})
</script>

AMD 的特点在于:

  1. 延迟加载
  2. 依赖前置

AMD 支持兼容 CommonJS 写法:

define(function (require, exports, module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.sayHi();
  anotherModule.sayBye();

  exports.asplode = function (){
    someModule.eat();
    anotherModule.play();
  };
});

四、CMD

SeaJS 是国内 JS 大神玉伯开发的模块加载器,基于 SeaJS 的模块机制,所有 JavaScript 模块都遵循 CMD(Common Module Definition) 模块定义规范.

CMD 模块的写法:

<script data-original="scripts/sea.js"></script>
<script>
// seajs 的简单配置
seajs.config({
  base: "./script/",
  alias: {
    "jquery": "script/jquery/3.3.1/jquery.js"
  }
})

// 加载入口模块
seajs.use("./main")
</script>

// 定义模块
// utils.js
define(function(require, exports, module) {
  exports.each = function (arr) {
    // 实现代码 
  };

  exports.log = function (str) {
    // 实现代码
  };
});

// 输出模块
define(function(require, exports, module) {
  var util = require('./util.js');
  
  var a = require('./a'); //在需要时申明,依赖就近
  a.doSomething();
  
  exports.init = function() {
    // 实现代码
    util.log();
  };
});

CMD 和 AMD 规范的区别:
AMD推崇依赖前置,CMD推崇依赖就近:
AMD 的依赖需要提前定义,加载完后就会执行。
CMD 依赖可以就近书写,只有在用到某个模块的时候再去执行相应模块。
举个例子:

// main.js
define(function(require, exports, module) {
  console.log("I'm main");
  var mod1 = require("./mod1");
  mod1.say();
  var mod2 = require("./mod2");
  mod2.say();

  return {
    hello: function() {
      console.log("hello main");
    }
  };
});

// mod1.js
define(function() {
  console.log("I'm mod1");
  return {
    say: function() {
      console.log("say: I'm mod1");
    }
  };
});

// mod2.js
define(function() {
  console.log("I'm mod2");
  return {
    say: function() {
      console.log("say: I'm mod2");
    }
  };
});

以上代码分别用 Require.js 和 Sea.js 执行,打印结果如下:
Require.js:
先执行所有依赖中的代码

I'm mod1
I'm mod2
I'm main
say: I'm mod1
say: I'm mod2

Sea.js:
用到依赖时,再执行依赖中的代码

I'm main

I'm mod1
say: I'm mod1
I'm mod2
say: I'm mod2

五、UMD

umd(Universal Module Definition) 是 AMD 和 CommonJS 的兼容性处理,提出了跨平台的解决方案。

(function (root, factory) {
    if (typeof exports === 'object') {
        // commonJS
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        // AMD
        define(factory);
    } else {
        // 挂载到全局
        root.eventUtil = factory();
    }
})(this, function () {
    function myFunc(){};

    return {
        foo: myFunc
    };
});

应用 UMD 规范的 JS 文件其实就是一个立即执行函数,通过检验 JS 环境是否支持 CommonJS 或 AMD 再进行模块化定义。

六、ES6 Module

CommonJS 和 AMD 规范都只能在运行时确定依赖。而 ES6 在语言层面提出了模块化方案, ES6 module 模块编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块化这种加载称为“编译时加载”或者静态加载。

写法:

// math.js
// 命名导出
export function add(a, b){
    return a + b;
}
export function sub(a, b){
    return a - b;
}
// 命名导入
import { add, sub } from "./math.js";
add(2, 3);
sub(7, 2);

// 默认导出
export default function foo() {
  console.log('foo');
}
// 默认导入
import someModule from "./utils.js";
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

另,在 webpack 对 ES Module 打包, ES Module 会编译成 require/exports 来执行的。

总结

JS 的模块化规范经过了模块模式、CommonJS、AMD/CMD、ES6 的演进,利用现在常用的 gulp、webpack 打包工具,非常方便我们编写模块化代码。掌握这几种模块化规范的区别和联系有助于提高代码的模块化质量,比如,CommonJS 输出的是值拷贝,ES6 Module 在静态代码解析时输出只读接口,AMD 是异步加载,推崇依赖前置,CMD 是依赖就近,延迟执行,在使用到模块时才去加载相应的依赖。

@Starbucks 2019/03/10
查看原文

赞 3 收藏 1 评论 0

种瓜南山下 评论了文章 · 2019-03-03

JavaScript原型与原型链

本文共 1475 字,读完只需 6 分钟

一、概述

在 JavaScript 中,是一种面向对象的程序设计语言,但是 JS 本身是没有 “类” 的概念,JS 是靠原型和原型链实现对象属性的继承。

在理解原型前,需要先知道对象的构造函数是什么,构造函数都有什么特点?

1. 构造函数

// 构造函数 Person()
function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}

var person = new Person("周杰伦", "男");

// 最后创建出来的对象实例 person
person

{
    name: "周杰伦",
    gender: "男"
}

以上代码,普通函数 Person(),加上 new 关键字后,就构造了一个对象 person

所以构造函数的定义就是普通函数加上 new 关键字,并总会返回一个对象。

2. 函数对象
同时,JS 中的对象分为一般对象和函数对象。那什么是一般对象,什么又是函数对象呢?

JavaScript 的类型分为基本数据类型和引用数据类型,基本数据类型目前有 6 种(null, undefined, string, number, boolean, Symbol)。 其余的数据类型都统称为 object 数据类型,其中,包括 Array, Date, Function等,所以函数可以称为函数对象。

let foo = function(){

}
foo.name = "bar";
foo.age = 24;
console.log(foo instanceof Function)  //true
console.log(foo.age)  // 24

以上代码就说明了函数其实是一个对象,也可以具有属性。

二、原型链

JavaScript 中的对象,有一个特殊的 [[prototype]] 属性, 其实就是对于其他对象的引用(委托)。当我们在获取一个对象的属性时,如果这个对象上没有这个属性,那么 JS 会沿着对象的 [[prototype]]链 一层一层地去找,最后如果没找到就返回 undefined;

这条一层一层的查找属性的方式,就叫做原型链。

var o1 = {
    age: 6
}

那么,为什么一个对象要引用,或者说要委托另外一个对象来寻找属性呢?

本文开篇的第一句话,就指出来的,JavaScript 中,和一般的 OOP 语言不同,它没有 '类'的概念,也就没有 '模板' 来创建对象,而是通过字面量或者构造函数的方式直接创建对象。那么也就不存在所谓的类的复制继承。

三、原型

那什么又是原型呢?

既然我们没有类,就用其他的方式实现类的行为吧,看下面这句话↓↓。

1. 每个函数都有一个原型属性 prototype 对象

function Person() {

}

Person.prototype.name = 'JayChou';

// person1 和 person2 都是空对象
var person1 = new Person();
var person2 = new Person();

console.log(person1.name) // JayChou
console.log(person2.name) // JayChou

通过构造函数创造的对象,对象在寻找 name 属性时,找到了 构造函数的 prototype 对象上。

这个构造函数的 prototype 对象,就是 原型

用示意图来表示:

查找对象实例属性时,会沿着原型链向上找,在现代浏览器中,标准让每个对象都有一个 __proto__ 属性,指向原型对象。那么,我们可以知道对象实例和函数原型对象之间的关系。

2. 每个原型对象都有一个 constructor 属性指向关联的构造函数

为了验证这一说话,举个例子。

function Person() {}

Person === Person.prototype.constructor; // true

那么对象实例是构造函数构造而来,那么对象实例是不是也应该有一个 constructor 呢?

function Person() {}

const person = new Person();
person.constructor === Person // true

但事实上,对象实例本身并没有 constructor 属性,对象实例的 constructor 属性来自于引用了原型对象的 constructor 属性

person.constructor === Person.prototype.constructor // true

3. 原型链顶层:Object.prototype.__proto__ == null

既然 JS 通过原型链查找属性,那么链的顶层是什么呢,答案就是 Object 对象,Object 对象其实也有 __proto__属性,比较特殊的是 Object.prototype.__proto__ 指向 null, 也就是空。

Object.prototype.__proto__ === null

我们回过头来看函数对象:

所有函数对象的proto都指向Function.prototype,它是一个空函数(Empty function)
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true
所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了·Function.prototype·的属性及方法。如length、call、apply、bind

以图会友,这就是网上经常看到的 JS 原型和原型链关系图:

对于以上看似很复杂的关系图,只需要理解 5 点:

  1. 每个函数都有一个原型属性 prototype 对象
  2. 普通对象的构造函数是 Object(),所以 Person.prototype.__proto__ === Object.prototype
  3. 函数对象都来自于 Function.prototype
  4. 函数对象也是对象,所有 Function.prototype.__proto__ === Object.prototype
  5. 记住,所有函数的默认原型都是 Object() 的实例,所以,Function.prototype.__proto__ === Object.prototype
  6. Object.prototype.__proto__ 是 null

总结

以上就是 JavaScript 中原型和原型链的知识。由于 JS 没有'类', 所以采用了原型的方式实现继承,正确的说法是引用或者委托,因为对象之间的关系不是复制,而是委托。在查找属性的时候,引用(委托)原型对象的属性,也就是我们常说的原型继承。

欢迎关注个人微信订阅号,专注分享原创文章

图片描述

查看原文

种瓜南山下 评论了文章 · 2019-03-02

JavaScript原型与原型链

本文共 1475 字,读完只需 6 分钟

一、概述

在 JavaScript 中,是一种面向对象的程序设计语言,但是 JS 本身是没有 “类” 的概念,JS 是靠原型和原型链实现对象属性的继承。

在理解原型前,需要先知道对象的构造函数是什么,构造函数都有什么特点?

1. 构造函数

// 构造函数 Person()
function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}

var person = new Person("周杰伦", "男");

// 最后创建出来的对象实例 person
person

{
    name: "周杰伦",
    gender: "男"
}

以上代码,普通函数 Person(),加上 new 关键字后,就构造了一个对象 person

所以构造函数的定义就是普通函数加上 new 关键字,并总会返回一个对象。

2. 函数对象
同时,JS 中的对象分为一般对象和函数对象。那什么是一般对象,什么又是函数对象呢?

JavaScript 的类型分为基本数据类型和引用数据类型,基本数据类型目前有 6 种(null, undefined, string, number, boolean, Symbol)。 其余的数据类型都统称为 object 数据类型,其中,包括 Array, Date, Function等,所以函数可以称为函数对象。

let foo = function(){

}
foo.name = "bar";
foo.age = 24;
console.log(foo instanceof Function)  //true
console.log(foo.age)  // 24

以上代码就说明了函数其实是一个对象,也可以具有属性。

二、原型链

JavaScript 中的对象,有一个特殊的 [[prototype]] 属性, 其实就是对于其他对象的引用(委托)。当我们在获取一个对象的属性时,如果这个对象上没有这个属性,那么 JS 会沿着对象的 [[prototype]]链 一层一层地去找,最后如果没找到就返回 undefined;

这条一层一层的查找属性的方式,就叫做原型链。

var o1 = {
    age: 6
}

那么,为什么一个对象要引用,或者说要委托另外一个对象来寻找属性呢?

本文开篇的第一句话,就指出来的,JavaScript 中,和一般的 OOP 语言不同,它没有 '类'的概念,也就没有 '模板' 来创建对象,而是通过字面量或者构造函数的方式直接创建对象。那么也就不存在所谓的类的复制继承。

三、原型

那什么又是原型呢?

既然我们没有类,就用其他的方式实现类的行为吧,看下面这句话↓↓。

1. 每个函数都有一个原型属性 prototype 对象

function Person() {

}

Person.prototype.name = 'JayChou';

// person1 和 person2 都是空对象
var person1 = new Person();
var person2 = new Person();

console.log(person1.name) // JayChou
console.log(person2.name) // JayChou

通过构造函数创造的对象,对象在寻找 name 属性时,找到了 构造函数的 prototype 对象上。

这个构造函数的 prototype 对象,就是 原型

用示意图来表示:

查找对象实例属性时,会沿着原型链向上找,在现代浏览器中,标准让每个对象都有一个 __proto__ 属性,指向原型对象。那么,我们可以知道对象实例和函数原型对象之间的关系。

2. 每个原型对象都有一个 constructor 属性指向关联的构造函数

为了验证这一说话,举个例子。

function Person() {}

Person === Person.prototype.constructor; // true

那么对象实例是构造函数构造而来,那么对象实例是不是也应该有一个 constructor 呢?

function Person() {}

const person = new Person();
person.constructor === Person // true

但事实上,对象实例本身并没有 constructor 属性,对象实例的 constructor 属性来自于引用了原型对象的 constructor 属性

person.constructor === Person.prototype.constructor // true

3. 原型链顶层:Object.prototype.__proto__ == null

既然 JS 通过原型链查找属性,那么链的顶层是什么呢,答案就是 Object 对象,Object 对象其实也有 __proto__属性,比较特殊的是 Object.prototype.__proto__ 指向 null, 也就是空。

Object.prototype.__proto__ === null

我们回过头来看函数对象:

所有函数对象的proto都指向Function.prototype,它是一个空函数(Empty function)
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true
所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了·Function.prototype·的属性及方法。如length、call、apply、bind

以图会友,这就是网上经常看到的 JS 原型和原型链关系图:

对于以上看似很复杂的关系图,只需要理解 5 点:

  1. 每个函数都有一个原型属性 prototype 对象
  2. 普通对象的构造函数是 Object(),所以 Person.prototype.__proto__ === Object.prototype
  3. 函数对象都来自于 Function.prototype
  4. 函数对象也是对象,所有 Function.prototype.__proto__ === Object.prototype
  5. 记住,所有函数的默认原型都是 Object() 的实例,所以,Function.prototype.__proto__ === Object.prototype
  6. Object.prototype.__proto__ 是 null

总结

以上就是 JavaScript 中原型和原型链的知识。由于 JS 没有'类', 所以采用了原型的方式实现继承,正确的说法是引用或者委托,因为对象之间的关系不是复制,而是委托。在查找属性的时候,引用(委托)原型对象的属性,也就是我们常说的原型继承。

欢迎关注个人微信订阅号,专注分享原创文章

图片描述

查看原文

种瓜南山下 评论了文章 · 2019-03-02

JavaScript原型与原型链

本文共 1475 字,读完只需 6 分钟

一、概述

在 JavaScript 中,是一种面向对象的程序设计语言,但是 JS 本身是没有 “类” 的概念,JS 是靠原型和原型链实现对象属性的继承。

在理解原型前,需要先知道对象的构造函数是什么,构造函数都有什么特点?

1. 构造函数

// 构造函数 Person()
function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}

var person = new Person("周杰伦", "男");

// 最后创建出来的对象实例 person
person

{
    name: "周杰伦",
    gender: "男"
}

以上代码,普通函数 Person(),加上 new 关键字后,就构造了一个对象 person

所以构造函数的定义就是普通函数加上 new 关键字,并总会返回一个对象。

2. 函数对象
同时,JS 中的对象分为一般对象和函数对象。那什么是一般对象,什么又是函数对象呢?

JavaScript 的类型分为基本数据类型和引用数据类型,基本数据类型目前有 6 种(null, undefined, string, number, boolean, Symbol)。 其余的数据类型都统称为 object 数据类型,其中,包括 Array, Date, Function等,所以函数可以称为函数对象。

let foo = function(){

}
foo.name = "bar";
foo.age = 24;
console.log(foo instanceof Function)  //true
console.log(foo.age)  // 24

以上代码就说明了函数其实是一个对象,也可以具有属性。

二、原型链

JavaScript 中的对象,有一个特殊的 [[prototype]] 属性, 其实就是对于其他对象的引用(委托)。当我们在获取一个对象的属性时,如果这个对象上没有这个属性,那么 JS 会沿着对象的 [[prototype]]链 一层一层地去找,最后如果没找到就返回 undefined;

这条一层一层的查找属性的方式,就叫做原型链。

var o1 = {
    age: 6
}

那么,为什么一个对象要引用,或者说要委托另外一个对象来寻找属性呢?

本文开篇的第一句话,就指出来的,JavaScript 中,和一般的 OOP 语言不同,它没有 '类'的概念,也就没有 '模板' 来创建对象,而是通过字面量或者构造函数的方式直接创建对象。那么也就不存在所谓的类的复制继承。

三、原型

那什么又是原型呢?

既然我们没有类,就用其他的方式实现类的行为吧,看下面这句话↓↓。

1. 每个函数都有一个原型属性 prototype 对象

function Person() {

}

Person.prototype.name = 'JayChou';

// person1 和 person2 都是空对象
var person1 = new Person();
var person2 = new Person();

console.log(person1.name) // JayChou
console.log(person2.name) // JayChou

通过构造函数创造的对象,对象在寻找 name 属性时,找到了 构造函数的 prototype 对象上。

这个构造函数的 prototype 对象,就是 原型

用示意图来表示:

查找对象实例属性时,会沿着原型链向上找,在现代浏览器中,标准让每个对象都有一个 __proto__ 属性,指向原型对象。那么,我们可以知道对象实例和函数原型对象之间的关系。

2. 每个原型对象都有一个 constructor 属性指向关联的构造函数

为了验证这一说话,举个例子。

function Person() {}

Person === Person.prototype.constructor; // true

那么对象实例是构造函数构造而来,那么对象实例是不是也应该有一个 constructor 呢?

function Person() {}

const person = new Person();
person.constructor === Person // true

但事实上,对象实例本身并没有 constructor 属性,对象实例的 constructor 属性来自于引用了原型对象的 constructor 属性

person.constructor === Person.prototype.constructor // true

3. 原型链顶层:Object.prototype.__proto__ == null

既然 JS 通过原型链查找属性,那么链的顶层是什么呢,答案就是 Object 对象,Object 对象其实也有 __proto__属性,比较特殊的是 Object.prototype.__proto__ 指向 null, 也就是空。

Object.prototype.__proto__ === null

我们回过头来看函数对象:

所有函数对象的proto都指向Function.prototype,它是一个空函数(Empty function)
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true
所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了·Function.prototype·的属性及方法。如length、call、apply、bind

以图会友,这就是网上经常看到的 JS 原型和原型链关系图:

对于以上看似很复杂的关系图,只需要理解 5 点:

  1. 每个函数都有一个原型属性 prototype 对象
  2. 普通对象的构造函数是 Object(),所以 Person.prototype.__proto__ === Object.prototype
  3. 函数对象都来自于 Function.prototype
  4. 函数对象也是对象,所有 Function.prototype.__proto__ === Object.prototype
  5. 记住,所有函数的默认原型都是 Object() 的实例,所以,Function.prototype.__proto__ === Object.prototype
  6. Object.prototype.__proto__ 是 null

总结

以上就是 JavaScript 中原型和原型链的知识。由于 JS 没有'类', 所以采用了原型的方式实现继承,正确的说法是引用或者委托,因为对象之间的关系不是复制,而是委托。在查找属性的时候,引用(委托)原型对象的属性,也就是我们常说的原型继承。

欢迎关注个人微信订阅号,专注分享原创文章

图片描述

查看原文

种瓜南山下 发布了文章 · 2019-02-24

JS异步编程之Promise

前言

《JS异步编程之 callback》一文我们了解了“JS 是基于单线程事件循环”的概念构建的,回调函数不会立即执行,由事件轮询去检测事件是否执行完毕,当执行完有结果后,将结果放入回调函数的参数中,然后将回调函数添加到事件队列中等待被执行。

同时也讲了回调函数的问题:

一是“回调地狱”,因为异步回调函数的特点:回调函数是作为异步函数的参数,一层一层嵌套,当嵌套过多,将使代码逻辑变得混乱,也无法做好错误捕捉和处理(只能在回调函数内部 try catch)。

二是回调的执行方式不符合自然语言的线性思维方式,不容易被理解。

三是控制反转(控制权在其他人的代码上),假如异步函数是别人提供的库,我们把回调函数传进去,我们并不能知道异步函数在调用回调函数之外做了什么事情。

func1(() => {
    func2(() => {
        func3(() => {
            func4(() => {
                try {
                    ...
                } catch (err){
                    ...
                }
            })
        });
    });
});

一、Promise 原理

首先,Promise 中文翻译为“承诺”, 是 JavaScript 的一种对象,表示承诺终将返回一个结果,无论成功还是失败。

Promise 有三个状态:等待中(pending),完成(fullfilled),失败(rejected), Promise 的设计具有原子性,状态一旦从 pending 状态转换为 fullfilled 状态或者 rejected 状态后,将不能被改变。

var promise1 = new Promise((resolve, reject) => {
    console.log("Promise 构造器会立即执行");
    setTimeout(function (){
        if(true) {
            resolve("完成");
        } else {
            reject("失败");
        }
    }, 1000);
})
promise1
.then((result) => {
    // do something
    console.log(result);
    return 1
    
    // return Promise.resolve(1);  // 返回一个决议为成功的 promise 实例
    // return Promise.reject("error");  // 返回一个决议为拒绝的 Promise 实例
})
.then((result) => {
    // .then() 方法会返回一个 promise, 完成调用的参数为前一个 promise 的返回值或者决议值。
    // do other things
    console.log(result);
    throw new Error("错误")  // 抛出错误是隐式拒绝
})
.catch((error) => {
    // 捕捉错误
    console.log(error)
})
.then(() => {
    // 还能继续执行!
})
.finally(() => {
    // always do somethings
    console.log("finally!")
})

二、Promise 的优势

  1. 链式调用

Promise 使用 then 方法后还会返回一个新的 Promise 对象,便于我们传递状态数据,同时链式写法接近于同步写法,更符合线性思维。

  1. 错误捕捉

相比回调函数的错误无法在外部捕捉的问题,Promise 能够为一连串的异步调用提供错误处理。

  1. 控制反转再反转

由于第三方提供的异步函数,无法保证回调函数如何被执行,但是 Promise 的特点,能够保证异步函数只能被 resolve 一次,以及始终以异步的形式执行代码。

  1. 可以利用 Promise.all 和 Promise.race 来解决 Promise 始终未决议和并行 Promise 嵌套的问题

三、Promise 的不足

  1. 每个 .then() 都是一个独立的作用域

加入有很多个 .then() 方法,就会创建很多个独立的作用域,那么将只能通过外面包裹一层函数作用域的闭包来共享状态数据

  1. 无法取消单个 .then()

当 Promise 链中任意一个 .then() 方法中有语句执行错误后,尽管经过 catch 方法的错误处理,还是并不会中断整个 Promise 链的执行。

  1. 无法得知进度

由于 Promise 只能从 pending 到 fullfilled 或 rejected 状态,无法得知 pending 阶段的进度。

四、Promise 应用

// Promise 封装 ajax
function fetch(method, url, data){
    return new Promise((resolve, reject) => {
        var xhr = new XMLHttpRequest();
        var method = method || "GET";
        var data = data || null;
        xhr.open(method, url, true);
        xhr.onreadystatechange = function() {
            if(xhr.status === 200 && xhr.readyState === 4){
                resolve(xhr.responseText);
            } else {
                reject(xhr.responseText);
            }
        }
        xhr.send(data);
        })
}

// 使用
fetch("GET", "/some/url.json", null)
.then(result => {
    console.log(result);
})

// 封装 nodejs error first 风格回调
function readFile(url) {
    return new Promise((resolve, reject) => {
       fs.readFile(url,'utf8', (err, data) => {
        if(err) {
            reject(err);
            return;
        }
        resolve(data)
        }) 
    })
}

五、总结

Promise 是 ES6 提出的简化异步流程控制的新规范,强调异步任务的完成状态且具有原子性,这使得我们的代码更容易追踪和维护。Promise 在事件轮询中属于异步事件队列中的微任务,而微任务总是一次性全部执行,而宏任务是每轮轮询执行一个,此节内容参考我之前的文章《JS专题之事件循环》。

2019/02/24 @Manncoffee

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

查看原文

赞 1 收藏 1 评论 0

种瓜南山下 发布了文章 · 2019-02-17

JS异步编程之callback

为什么 JS 是单线程?

众所周知,Javascript 语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

而浏览器是多线程的,JS 线程就是其中一个:

  • 浏览器 GUI 渲染线程
  • JavaScript 引擎线程
  • 浏览器定时触发器线程
  • 浏览器事件触发线程
  • 浏览器 http 异步请求线程

浏览器线程知识中重要的一点是:

GUI渲染进程和 JavaScript 引擎进程是互斥的,因为如果这两个线程可以同时运行的话, JavaScript 的 DOM 操作将会扰乱渲染线程执行渲染前后的数据一致性。而且如果 DOM 一变化,界面就立刻重新渲染,效率必然很低

所以 JS 主线程执行任务时,浏览器渲染线程处于挂起状态。

同理,如果 JS 采用多线程同步的模型,那么如何保证同一时间修改了 DOM, 到底是哪个线程先生效呢?从操作系统调度多线程的上下文开销,到实际编程里的锁、线程同步等问题,都让开发变得比较困难。

所以 JS 最终采用了单线程的事件模型。

我之前的文章《JS专题之事件循环》也有讲过这块内容,欢迎翻阅。

一、同步与异步

单线程模式这种排队执行的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

那同步和异步的区别是什么?

我们想象一个很常见的场景:我们去面馆吃牛肉面,柜台人很多,前面在排队下单。

这个时候,同步就是,收银员收了你的钱,告诉你要在柜台站着等面煮好,煮好后,就端面开吃,后面的人也只能等前面的人面煮好了才能付款下单然后等着面煮好端走~

而异步就是,收银员收了你的钱,然后给了你一张小票,小票上有一个你的编号,收银员告诉你,可以去座位上,你的面一煮好,会大声叫你,你就来端面开吃。

我们可以看出,我们是过程的调用者,面馆是被调用者,牛肉面煮好,是我们想要的结果,同步是调用者需要主动地等待这个结果。异步是被动的等待结果,当被调用者有结果了,就会通过消息机制或者回调机制告诉调用者结果。

同步和异步关注的是消息通信机制,同步就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果, 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

以上:

  • 下单吃面是发起调用函数
  • 端面开吃的回调函数
  • 煮好的面是调用的结果,也是回调函数的参数

将例子抽象成伪代码:

orderNoodle("牛肉面", function(noodle) {
        // 端面
        getNoodle();
        // 吃面
        eatNoodle();
});

三、事件循环

关于事件循环如何执行异步代码可以翻阅前面的文章《JS专题之事件循环》,这里大概提一下。

如果遇到异步事件,JS 引擎会把事件函数压入执行调用栈,但浏览器识别到它是异步事件后,会将其弹出执行栈,当异步函数有返回结果后,JS 引擎将异步事件的回调函数放入事件队列中,如果执行调用栈为空,就将回调函数压入执行调用栈执行。

四、回调函数

在 JavaScript 中,函数 function 作为一等公民,使用上非常自由,无论调用它,或者作为参数,或者作为返回值都可以。

因为单线程异步的特点,后来在 JS 中,慢慢将函数的业务重点转移到了回调函数中。

function step1(cb) {
    console.log("step1");
    cb()
}

function step2(){
    console.log("step2");
}

step1(step2);  // step1  step2

代码会按先后顺序执行 step1, step2。

现在假设我们有这样的需求:请求文件1后,获取文件1 中的数据后请求文件2,获取文件 2 中的数据后,又请求文件三。

var fs = require("fs");

fs.readFile("./file1.json", function(err, data1) {
    fs.readFile("./file2.json", function (err, data2) {
        fs.readFile("./file3.json", function(err, data3) {
            
        })
    })
})

五、回调函数的问题

由第四节可以看出,回调函数的写法存在很多问题。

  1. 回调地狱(洋葱模型)

当多个异步事务多级依赖时,回调函数会形成多级的嵌套,被花括号一层层包括,代码变成
金字塔型结构,也被称为回调地狱和洋葱模型。

在回调地狱的情况下,代码逻辑的梳理,流程的控制,代码封装维护,错误处理都变得越来越困难。

  1. 异常处理

try...catch 是被设计成捕获当前执行环境的异常,意思是只能捕获同步代码里面的异常,异步调用里面的异常无法捕获。

function readFile(fileName) {
    setTimeout(function () {
      throw new Error("类型错误");
    }, 1000);
}
try {
    readFile('./file1.json');
} catch (e) {
    // 如果异步事件出错,打印不出来错误信息
    console.log('err', e);
}

在 nodejs 对回调函数采用 error first 的思想,回调函数的第一个参数保留给一个错误error对象,如果有错误发生,错误将通过第一个参数err返回。

原因是一个有回调函数的函数,执行分两段,第一段执行完之后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文已经无法捕捉,只能当做参数,传入第二阶段。

fs.readFile('/etc/passwd', 'utf8', function (err, data) {
    if(err) {
        console.log(err)
        return;
    }
});

总结

回调函数是 JS 异步编程中的基石,但同时也存在很多问题,不太适合人类自然语言的线性思维习惯。

接下来几篇文章,我将梳理 JS 中异步编程中的历史演进中 Promise, generator, async&await 相关的内容,欢迎关注。

图片描述

查看原文

赞 4 收藏 3 评论 1

种瓜南山下 评论了文章 · 2019-02-16

JS专题之数组展开

前言

首先什么是数组展开?
假如现在有这样一个需求:将后台的一个多重 List 数据,展开成一个 List 后,并去重后排序;

["a", "b", ["c", "d"], [["d"],"e"], "f"] => ["a", "b", "c", "d", "e"];

数组去重我们前面在《JS专题之数组去重》已经讲过了,那么前一步的多重数组展开成单层数组,该如何处理呢?

这就来到我们所要探讨的数组展开了。
根据需求的特点,数组展开需要进行迭代和递归。

回答文章开头的问题:

将多重数组转化成单层数组的过程就是数组展开,也叫作数组扁平化(flatten)

一、循环加递归

最简单的思路:循环中判断,如果子元素是数组则递归。

function flatten(origin) {
    var result = [];
    for(var i = 0; i< origin.length; i++) {
        var item = origin[i];
        if(Array.isArray(item)) {
            result = result.concat(flatten(item))
        } else {
            result.push(item);
        }
    }
    return result;
}

var arr = ["a", "b", ["c", "d"], [["d"],"e"], "f"];
flatten(arr);  // ["a", "b", "c", "d", "d", "e", "f"]

二、toString()

数组的原型对象上有一个方法,toString, 它能把数组的所以元素转化成用逗号隔开的字符串。

var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];  
arr.toString()  // "1,2,3,4,a,b,c,d,d,e,f"

// 所以,利用 split 先把字符串转化为单层数组,再进行处理。
const flatten = (origin) => origin.toString().split(',');  // ["1", "2", "3", "4", "a", "b", "c", "d", "d", "e", "f"]

由于 toString 转化为字符串的时候,不会区分字符串和数字类型,在进行区分数据类型的时候要注意。

三、split

上面的方法,我们用 toString() 将数组转化为字符串,那么我们也可以用 split 来做:

var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];
function flatten(arr) {
    return arr.join(',').split(',');  
} 
console.log(flatten(arr))。 // ["1", "2", "3", "4", "a", "b", "c", "d", "d", "e", "f"]

同样,这种字符串和数组互转的过程,不适合多种数据类型同时处理。

四、reduce

我们注意到其实数组扁平化其实就是“迭代 + 拼接(累加) + 递归”的过程,数组 reduce 方法既可以迭代又可以累加,适合做数组扁平化。

function flatten(origin){
  return origin.reduce(function(init, item){
    return init.concat(Array.isArray(item) ? flatten(item) : item)
  }, [])
}
var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];  
console.log(flatten(arr)) // [1, 2, 3, 4, "a", "b", "c", "d", "d", "e", "f"]

五、some + concat

some 会遍历数组的每一个元素,判断是否有元素都满足条件,最后返回布尔值。some 一旦返回真值后,其后的元素就不会继续监测。

function flatten(origin) {
    while (origin.some(item => Array.isArray(item))){
    origin = [].concat.apply([], origin);
  }
  return origin;
}
var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];  
console.log(flatten(arr)) // [1, 2, 3, 4, "a", "b", "c", "d", "d", "e", "f"]

六、some + 扩展运算符

ES6 扩展运算符...可以将两重数组转换为单层数组:

[].concat(...[1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"]);  // [1, 2, 3, Array(1), "a", "b", "c", "d", Array(1), "e", "f"]

// 利用 some 方法,我们可以实现多重转换为单层:

function flatten(origin) { while(origin.some(item=> Array.isArray(item))) {
        origin = [].concat(...origin);
    } return origin;
}

var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];  
console.log(flatten(arr)) // [1, 2, 3, 4, "a", "b", "c", "d", "d", "e", "f"]

总结

数组扁平化其实就是利用元素迭代 + 元素拼接(叠加)+ 递归调用来对数组进行处理,达到将多层数组转换为单层数组的过程。

查看原文

种瓜南山下 发布了文章 · 2019-02-10

JS专题之继承

前言

众所周知,JavaScript 中,没有 JAVA 等主流语言“类”的概念,更没有“父子类继承”的概念,而是通过原型对象和原型链的方式实现继承。

于是,我们这一篇讲一讲 JS 中的继承(委托)。

一、为什么要有继承?

JavaScript 是面向对象编程的语言,里面全是对象,而如果不通过继承的机制将对象联系起来,势必会造成程序代码的冗余,不方便书写。

二、为什么又是原型链继承?

好,既然是 OO 语言,那么就加继承属性吧。但是 JS 创造者并不打算引用 class,不然 JS 就是一个完整的 OOP 语言了,而创造者 JS 更容易让新手开发。

后来,JS 创造者就将 new 关键字创建对象后面不接 class,改成构造函数,又考虑到继承,于是在构造函数上加一个原型对象,最后让所有通过 new 构造函数 创建出来的对象,就继承构造函函数的原型对象的属性。

function Person() {
    // 构造函数
    this.name = "jay";
}

Person.prototype = {
    sex: "male"
}

var person1 = new Person();
console.log(person1.name);  // jay
console.log(person1.sex);  // male

所以,就有了 JavaScript 畸形的继承方式:原型链继承~

三、原型链继承

function Parent() {
    this.names = ["aa", "bb", "cc"];
    this.age = 18;
}

function Child() {
    // ...
}

Child.prototype = new Parent();  // 改变构造函数的原型对象

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
console.log(child1.age);   // 18
child1.names.push("dd");
child1.age = 20;
var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age);  // 18

以上例子中,暴露出原型链继承的两个问题:

  1. 包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。
  2. 在创建子类型实例时,无法向父类型的构造函数中传递参数。

四、call 或 apply 继承

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}
function Child() {
    Parent.call(this, 18);
}

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age);  // 18

call 或 apply 的原理是在子类型的构造函数中,“借调”父类型的构造函数,最终实现子类型中拥有父类型中属性的副本了。

call 或 apply 这种继承方式在《JavaScript 高级程序设计》中叫作“借用构造函数(constructor stealing)”,解决了原型链继承中,引用数据类型被所有子实例共享的问题,也能够实现传递参数到构造函数中,但唯一的问题在于业务代码也写在了构造函数中,函数得不到复用。

五、组合继承

组合继承(combination inheritance)也叫作伪经典继承,指的是,前面两种方法:原型链继承和 call 或 apply 继承 组合起来,保证了实例都有自己的属性,同时也能够实现函数复用:

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}

Parent.prototype.sayName = function () {
    console.log(this.names);
}

function Child() {
    Parent.call(this, 18);  // 第一次调用
}

Child.prototype = new Parent();  // 第二次调用:通过原型链继承 sayName 方法
Child.prototype.constructor = Child;  // 改变 constructor 为子类型构造函数

var child1 = new Child();
child1.sayName();   // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age); 
child2.sayName();  // ["aa", "bb", "cc"]

组合继承将继承分为两步,一次是创建子类型关联父类型原型对象的时候,另一次是在子类型构造函数的内部。是 JS 最常用的继承方式。

六、原型式继承

原型式继承说白了,就是将父类型作为一个对象,直接变成子类型的原型对象。

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
 
var parent = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 

var child1 = object(parent);

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = object(parent);
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18

原型式继承其实就是对原型链继承的一种封装,它要求你有一个已有的对象作为基础,但是原型式继承也有共享父类引用属性,无法传递参数的缺点。

这个方法后来有了正式的 API: Object.create({...})

所以当有一个对象,想让子实例继承的时候,可以直接用 Object.create() 方法。

七、寄生式继承

寄生式继承是把原型式 + 工厂模式结合起来,目的是为了封装创建的过程。

function createAnother(original){ 
    var clone= object(original);    //通过调用函数创建一个新对象
    clone.sayHi = function(){      //以某种方式来增强这个对象
        console.log("hi");
    };
    return clone;                  //返回这个对象
}
 
var person = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

八、 寄生组合式继承

刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); // 创建了父类原型的浅复制
    prototype.constructor = subType;             // 修正原型的构造函数
    subType.prototype = prototype;               // 将子类的原型替换为这个原型
}
 
function SuperType(age){
    this.age = age;
    this.names = ["aa", "bb", "cc"];
}
 
SuperType.prototype.sayName = function(){
    console.log(this.names);
};
 
function SubType(age){
    SuperType.call(this, age);
    this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var child1 = new SubType(22)
child1.sayAge()  // 22
child1.sayName()  // ["aa", "bb", "cc"]

九、ES6 class extends

class Parent {
    constructor(name) {
    this.name = name;
    }
    doSomething() {
            console.log('parent do something!');
    }
    sayName() {
        console.log('parent name:', this.name);
    }
}

class Child extends Parent {
    constructor(name, parentName) {
    super(parentName);
    this.name = name;
    }
    sayName() {
         console.log('child name:', this.name);
    }
}

const child = new Child('son', 'father');
child.sayName();            // child name: son
child.doSomething();        // parent do something!

const parent = new Parent('father');
parent.sayName();   // parent name: father

ES6 的 class extends 本质上是 ES5 的语法糖。
ES6实现继承的具体原理:

class Parent {
}
 
class Child {
}
 
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
 
// B 的实例继承 A 的实例
Object.setPrototypeOf(Child.prototype, parent.prototype);
 
// B 继承 A 的静态属性
Object.setPrototypeOf(Child, Parent);

总结

javascript 由于历史发展原因,继承方式实际上是通过原型链属性查找的方式,但正规的叫法不叫继承而叫“委托”,ES6 的 class extends 关键字也不过是 ES5 的语法糖。所以,了解 JS 的原型和原型链非常重要,详情请翻看我之前的文章《JavaScript原型与原型链》

参考:
《JavaScript 高级程序设计》

2019/02/10 @Starbucks

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

查看原文

赞 1 收藏 1 评论 0

种瓜南山下 发布了文章 · 2019-02-09

JS专题之垃圾回收

前言

在讲 JS 的垃圾回收(Garbage Collection)之前,我们回顾上一篇《JS专题之memoization》,memoization 的原理是以参数作为 key,函数结果作为 value, 用对象进行缓存起来,以内存空间换 CPU 执行事件。memoization 的潜在陷阱即是严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。

用闭包进行缓存的对象的内存空间,不会在函数执行完后被清除,在执行量大和参数多样性的情况下,会造成内存占用且得不到释放。

于是,本篇文章就来讲讲 JS 的垃圾回收。

JS 的垃圾回收机制的基本原理是:

找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。

那我们怎么知道变量是不是在继续使用呢?

首先,我之前的文章,《JavaScript之变量及作用域》,《JavaScript之作用域链》和《JavaScript之闭包》都有提到过,局部变量的生存周期是在函数声明和执行阶段,函数执行完毕后,局部变量就没有存在的必要了。全局变量会在浏览器关闭或进程关闭才能释放。

但还有一些场景,比如闭包,通过作用域链访问到函数外部的自由变量,使得自由变量保存在内存中,不会随着函数执行完毕而结束,以及对象的相互引用等,垃圾收集器就没这么容易判断哪个变量有用,哪个变量没用了。

// 经典闭包
function closure() {
    var name = "innerName";
    return function() {
        console.log(name);
    }
}

var inner = closure();
inner();  // innerName;

所以,对于标识无用的变量的策略可能会因实现而已,但目前在浏览器中,通常有两种策略:标记清除和引用计数。

二、标记-清除(Mark-Sweep)

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法, 那什么叫标记-清除呢?

当变量进入执行环境时,就标记这个变量为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。

然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

另外,标记-清除有一个问题,就是在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理(Mark-Compact)方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一端移动,最后清理掉边界的内存。

三、引用计数

另外一种不太常见的垃圾收集策略叫引用计数(Reference Counting),此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存。

而引用计数的不继续被使用,是因为循环引用的问题会引发内存泄漏。

function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someObject = objB;
    objB.anotherObject = objA;
}

objA 和 objB 通过各自的属性相互引用,也就是说,两个对象的引用次数都是 2。在函数执行完毕后,objA, objB 还将继续存在,因为他们的引用计数永远不会是 0。假如这个函数被多次执行,就会导致大量的内存得不到释放。

四、NodeJs V8 中的垃圾回收机制

在 Node 中,通过 JS 使用内存时就会发现只能使用部分内存(64 位系统下约为 1.4 GB, 32 位系统下约为 0.7 GB),这导致 Node 无法直接操作大内存对象。

这是因为,以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量式的垃圾回收要 1 秒以上,而垃圾回收过程会引起 JS 线程暂停执行这么多时间。因此,在当时的考虑下,直接限制堆内存是一个好的选择。

那么,在这样的内存限制下,V8 的垃圾回收机制又有什么特点?

4.1、内存分代算法

V8 的垃圾回收策略主要基于分代式垃圾回收机制,在 V8 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。

V8 堆的整体大小等于新生代所用内存空间加上老生代的内存空间,而只能在启动时指定,意味着运行时无法自动扩充,如果超过了极限值,就会引起进程出错。

4.2 Scavenge 算法

在分代的基础上,新生代的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 具体实现中,主要采用了一种复制的方式的方法—— Cheney 算法。

Cheney 算法将堆内存一分为二,一个处于使用状态的空间叫 From 空间,一个处于闲置状态的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配。

当开始进行垃圾回收时,会检查 From 空间中的存活对象,将其复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生对换。

当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理。

还有一种情况是,如果复制一个对象到 To 空间时,To 空间占用超过了 25%,则这个对象会被直接晋升到老生代空间中。

4.3 标记-清除和标记-整理算法

对于老生代中的对象,主要采用标记-清除和标记-整理算法。标记-清除 和前文提到的标记一样,与 Scavenge 算法相比,标记清除不会将内存空间划为两半,标记清除在标记阶段会标记活着的对象,而在内存回收阶段,它会清除没有被标记的对象。

而标记整理是为了解决标记清除后留下的内存碎片问题。

4.4 增量标记(Incremental Marking)算法

前面的三种算法,都需要将正在执行的 JavaScript 应用逻辑暂停下来,待垃圾回收完毕后再恢复。这种行为叫作“全停顿”(stop-the-world)。

在 V8 新生代的分代回收中,只收集新生代,而新生代通常配置较小,且存活对象较少,所以全停顿的影响不大,而老生代就相反了。

为了降低全部老生代全堆垃圾回收带来的停顿时间,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直到标记阶段完成。

经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右。

五、内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

六、内存泄漏的常见场景

6.1 缓存

文章前言部分就有说到,JS 开发者喜欢用对象的键值对来缓存函数的计算结果,但是缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

6.2 作用域未释放(闭包)
var leakArray = [];
exports.leak = function () {
    leakArray.push("leak" + Math.random());
}

以上代码,模块在编译执行后形成的作用域因为模块缓存的原因,不被释放,每次调用 leak 方法,都会导致局部变量 leakArray 不停增加且不被释放。

闭包可以维持函数内部变量驻留内存,使其得不到释放。

6.3 没必要的全局变量

声明过多的全局变量,会导致变量常驻内存,要直到进程结束才能够释放内存。

6.4 无效的 DOM 引用
//dom still exist
function click(){
    // 但是 button 变量的引用仍然在内存当中。
    const button = document.getElementById('button');
    button.click();
}

// 移除 button 元素
function removeBtn(){
    document.body.removeChild(document.getElementById('button'));
}
6.5 定时器未清除

// vue 的 mounted 或 react 的 componentDidMount
componentDidMount() {
    setInterval(function () {
        // ...do something
    }, 1000)
}

vue 或 react 的页面生命周期初始化时,定义了定时器,但是在离开页面后,未清除定时器,就会导致内存泄漏。

6.6 事件监听为清空
componentDidMount() {
    window.addEventListener("scroll", function () {
        // do something...
    });
}

同 6.5, 在页面生命周期初始化时,绑定了事件监听器,但在离开页面后,未清除事件监听器,同样也会导致内存泄漏。

七、内存泄漏优化

  1. 解除引用

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}

var globalPerson = createPerson("Nicholas");

// 手动解除 globalPerson 的引用
globalPerson = null;

解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

  1. 提供手动清空变量的方法
var leakArray = [];
exports.clear = function () {
    leakArray = [];
}
  1. 在业务不需要用到的内部函数,可以重构在函数外,实现解除闭包
  2. 避免创建过多生命周期较长的对象,或将对象分解成多个子对象
  3. 避免过多使用闭包
  4. 注意清除定时器和事件监听器
  5. Nodejs 中使用 stream 或 buffer 来操作大文件,不会受 Nodejs 内存限制
  6. 使用 redis 等外部工具缓存数据

总结

JS 是一门具有自动垃圾收集的编程语言,在浏览器中主要通过标记清除方法来回收垃圾,NodeJs 中主要通过分代回收、Scavenge、标记清除、增量标记等算法来回收垃圾。在日常开发中,有一些不引人注意的书写方式可能会导致内存泄漏,需要多注意自己的代码规范。

2019/02/09 @Starbucks
查看原文

赞 1 收藏 1 评论 0

种瓜南山下 发布了文章 · 2019-02-08

JS专题之memoization

前言

在计算机领域,记忆(memoization)是主要用于加速程序计算的一种优化技术,它使得函数避免重复演算之前已被处理过的输入,而返回已缓存的结果。 -- wikipedia

Memoization 的原理就是把函数的每次执行结果都放入一个对象中,在接下来的执行中,在对象中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才真正执行函数体的求值部分。在对象里找值是要比执行函数的速度要快的。

另外,Memoization 只适用于确定性算法,对于相同的输入总是生成相同的输出,即纯函数。

一、简单实现

通过 Memoization 的定义和原理,我们就可以初步实现 Memoization 了。

let memoize = function(func) {
  let cache = {};
  return function(key) {
    if (!cache[key])
      cache[key] = func.apply(this, arguments);
    return cache[key];
  }
}

是不是很简单~ 函数记忆其实就是利用闭包,将函数参数作为存储对象的键(key),函数结果作为存储对象的 value 值。

二、underscore 实现

underscore 的源码中有 Memoization 方法的封装,它支持传入一个 hasher 用来计算缓存对象 key 的计算方式。

_.memoize = function(func, hasher) {
  var memoize = function(key) {
    // 把存储对象的引用拿出来,便于后面代码使用
    var cache = memoize.cache;

    // hasher 是计算 key 值的方法函数。
    // 如果传入了 hasher,则用 hasher 函数来计算 key
    // 否则用 参数 key(即 memoize 方法传入的第一个参数)当 key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key);

    // 如果 key 还没有对应的 hash 值(意味着没有缓存值,没有计算过该输入)
    // 就执行回调函数,并缓存结果值
    if (!_.has(cache, address))
      cache[address] = func.apply(this, arguments);

    // 从缓存对象中取结果值
    return cache[address];
  };

  // cache 对象被当做 key-value 键值对缓存中间运算结果
  memoize.cache = {};

  // 返回 momoize 函数, 由于返回函数内部引用了 memoize.cache, 构成了闭包,变量保存在了内存中。
  return memoize;
};

三、应用 - 判断素数

质数为在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数。

我们通过判断素数的函数,看看使用了函数记忆后的效果。

function isPrime(value) {
  console.log("isPrime 被执行了!");
  var prime = value != 1; // 1 不是素数,其他数字默认是素数。
  for (var i = 2; i < value; i++) {
    if (value % i == 0) {
      prime = false;
      break;
    }
  }
  return prime
}

let momoizedIsPrime = memoize(isPrime);

momoizedIsPrime(5) // isPrime 被执行了!
momoizedIsPrime(5) // 第二次执行,没有打印日志!

四、应用 - 计算斐波那契数列

斐波那契数列的特点是后一个数等于前面两个数的和

指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波那契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=Fn-1+Fn-2

计算斐波那契数列是用来演示函数记忆很好的例子,因为计算斐波那契数列函数里面用了大量的递归。

var count = 0;
var fibonacci = function(n) {
  count++;
  return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}

for(var i = 0; i<= 10; i++) {
    console.log(`i: ${i}, ` + fibonacci(i));
}

// i: 0, 0
// i: 1, 1
// i: 2, 1
// i: 3, 2
// i: 4, 3
// i: 5, 5
// i: 6, 8
// i: 7, 13
// i: 8, 21
// i: 9, 34
// i: 10, 55

console.log(count);  // 453 !!!

我们可以看出,如果从 0 开始打印斐波那契数列,fibonacci 函数被执行了 453 次。那我们就牺牲一小部分内存,用来缓存每次计算的值。

fibonacci = memoize(fibonacci);

for(var i = 0; i<= 10; i++) {
    console.log(`i: ${i}, ` + fibonacci(i));
}
console.log(count); // 12

通过 memoize 函数记忆,使得函数执行次数只需要 12 次,大大优化了函数执行计算的性能消耗,

总结

函数记忆(memoization)将函数的参数和结果值,保存在对象当中,用一部分的内存开销来提高程序计算的性能,常用在递归和重复运算较多的场景。

查看原文

赞 4 收藏 2 评论 0

认证与成就

  • 获得 73 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-09-20
个人主页被 1.1k 人浏览