使用DocumentFragment加快DOM渲染速度

js中提供了一个DocumentFragment机制,可以提供一个缓冲机制,将DOM节点先放在内存中,当节点都构造完成后,再将DocumentFragment对象添加到页面中,这时所有的节点都会一次渲染出来,这样就能减少浏览器很多的负担,明显的提高页面渲染速度。下面的代码给出了两个函数,分别是用普通DOM方法和DocumentFragment两种方式向页面添加一万个div节点,使用第二种方式比第一种快很多。

function CreateNodes(){ 
    for(var i = 0;i < 10000;i++){
        var tmpNode = document.createElement("div");
        tmpNode.innerHTML = "test" + i + " <br />";
        document.body.appendChild(tmpNode);      
    }
}

function CreateFragments(){
    var fragment = document.createDocumentFragment();
    for(var i = 0;i < 10000;i++){
        var tmpNode = document.createElement("div");
        tmpNode.innerHTML = "test" + i + "<br />";
        fragment.appendChild(tmpNode);
    }
    document.body.appendChild(fragment);
}

web缓存机制

Cache-Control

max-age

max-age(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。
我们来找个资源看下。比如shang.qq.com上的css资源,max-age=2592000,也就是说缓存有效期为2592000秒(也就是30天)。于是在30天内都会使用这个版本的资源,即使服务器上的资源发生了变化,浏览器也不会得到通知。max-age会覆盖掉Expires,后面会有讨论。

s-maxage

s-maxage(单位为s)同max-age,只用于共享缓存(比如CDN缓存)。
比如,当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。也就是说max-age用于普通缓存,而s-maxage用于代理缓存。如果存在s-maxage,则会覆盖掉max-age和Expires header。

public

public 指定响应会被缓存,并且在多用户间共享。如果没有指定public还是private,则默认为public。

private

private 响应只作为私有的缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private。

no-cache

no-cache 指定不缓存响应,表明资源不进行缓存,但是设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。因此有的时候只设置no-cache防止缓存还是不够保险,还可以加上private指令,将过期时间设为过去的时间。

no-store

no-store 绝对禁止缓存,一看就知道如果用了这个命令当然就是不会进行缓存啦~每次请求资源都要从服务器重新获取。

must-revalidate

must-revalidate指定如果页面是过期的,则去服务器进行获取。这个指令并不常用,就不做过多的讨论了。

Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。但在上面我们提到过,cache-control的优先级更高。 Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Last-modified

服务器端文件的最后修改时间,需要和cache-control共同使用,是检查服务器端资源是否更新的一种方式。当浏览器再次进行请求时,会向服务器传送If-Modified-Since报头,询问Last-Modified时间点之后资源是否被修改过。如果没有修改,则返回码为304,使用缓存;如果修改过,则再次去服务器请求资源,返回码和首次请求相同为200,资源为服务器最新资源。

ETag

根据实体内容生成一段hash字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改。使用ETag可以解决Last-modified存在的一些问题:

  1. 某些服务器不能精确得到资源的最后修改时间,这样就无法通过最后修改时间判断资源是否更新
  2. 如果资源修改非常频繁,在秒以下的时间内进行修改,而Last-modified只能精确到秒
  3. 一些资源的最后修改时间改变了,但是内容没改变,使用ETag就认为资源还是没有修改的。

IndexedDB存储机制

IndexedDB存储与WebSQL存储的比较及选择

IndexedDB

IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法。IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作。IndexedDB里数据以对象的形式存储,每个对象都有一个key值索引。IndexedDB里的操作都是事务性的。一种对象存储在一个objectStore里,objectStore就相当于关系数据库里的表。IndexedDB可以有很多objectStore,objectStore里可以有很多对象。每个对象可以用key值获取。

特点:

  1. 使用NoSQL数据库
  2. 支持事务、游标、索引等数据库操作
  3. 一般浏览器会分配50M-250M不等的内存
  4. 持久化存储,清除浏览器缓存不会被删除(localStorage是会被删除的)
  5. 支持多种数据格式:arrayBuffer、String、Object、Array、File、Blob、ImageData都ok
  6. 不支持跨域,一个域可以有多个数据库
  7. 开发中需要谨记的一个特性:异步操作,换句话说,所有操作都需要在回调函数中完成
  8. 存储空间大,IndexedDB的存储空间比localStorage大得多,一般来说不少于250MB。IE的储存上限是250MB,Chrome和Opera是硬盘剩余空间的某个百分比,Firefox则没有上限。

浏览器支持:

  1. IE10+
  2. Firefox 10+、Chrome 23+、Opera 15+
  3. iPhone ios8-ios10 safari支持(X5内核不支持)
  4. Android X5内核支持

WebSQL

Web SQL Database API 实际上并不包含在 HTML5 规范之中。它是一个独立的规范,它引入了一套使用 SQL 操作客户端数据库的 API。最新版本的 Chrome,Safari 和 Opera 浏览器都支持 Web SQL Database。

特点:

  1. 使用MySQL数据库
  2. 相比传统存储,能方便的进行对象存储,能进行大量的数据的处理
  3. w3c规范已经不再支持
  4. 浏览器支持较差,Web Sql API的主要实现者是Chrome、Safari、Opera、Android、IOS、BB。IE和FF都不支持Web Sql API。

比较

  1. IndexedDB对浏览器的兼容性比WebSQL更好
  2. WebSQL已经被w3c规范摒弃

创建IndexedDB数据库

调用indexedDB.open方法就可以创建或者打开一个indexedDB。代码中定义了一个myDB对象,在创建indexedDB request的成功毁掉函数中,把request获取的DB对象赋值给了myDB的db属性,这样就可以使用myDB.db来访问创建的indexedDB了。

function openDB (name) {
    var request=window.indexedDB.open(name);
    request.onerror=function(e){
        console.log('OPen Error!');
    };
    request.onsuccess=function(e){
        myDB.db=e.target.result;
    };
}

var myDB={
    name:'test',
    version:1,
    db:null
};
openDB(myDB.name);

我们注意到除了onerror和onsuccess,IDBOpenDBRequest还有一个类似回调函数句柄——onupgradeneeded。这个句柄在我们请求打开的数据库的版本号和已经存在的数据库版本号不一致的时候调用。
indexedDB.open()方法还有第二个可选参数,数据库版本号,数据库创建的时候默认版本号为1,当我们传入的版本号和数据库当前版本号不一致的时候onupgradeneeded就会被调用,当然我们不能试图打开比当前数据库版本低的version,否则调用的就是onerror了,修改一下刚才例子

function openDB (name,version) {
    var version=version || 1;
    var request=window.indexedDB.open(name,version);
    request.onerror=function(e){
        console.log(e.currentTarget.error.message);
    };
    request.onsuccess=function(e){
        myDB.db=e.target.result;
    };
    request.onupgradeneeded=function(e){
        console.log('DB version changed to '+version);
    };
}

var myDB={
    name:'test',
    version:3,
    db:null
};
openDB(myDB.name,myDB.version);

关闭与删除数据库

关闭数据库可以直接调用数据库对象的close方法

function closeDB(db){
    db.close();
}

删除数据库使用indexedDB对象的deleteDatabase方法

function deleteDB(name){
    indexedDB.deleteDatabase(name);
}

object store

有了数据库后我们自然希望创建一个表用来存储数据,但indexedDB中没有表的概念,而是objectStore,一个数据库中可以包含多个objectStore,objectStore是一个灵活的数据结构,可以存放多种类型数据。也就是说一个objectStore相当于一张表,里面存储的每条数据和一个键相关联。我们可以使用每条记录中的某个指定字段作为键值(keyPath),也可以使用自动生成的递增数字作为键值(keyGenerator),也可以不指定。选择键的类型不同,objectStore可以存储的数据结构也有差异。

给object store添加数据

因为对新数据的操作都需要在transaction中进行,而transaction又要求指定object store,所以我们只能在创建数据库的时候初始化object store以供后面使用,这正是onupgradeneeded的一个重要作用。这样在创建数据库的时候我们就为其添加了一个名为students的object store,准备一些数据以供添加。

function openDB (name,version) {
    var version=version || 1;
    var request=window.indexedDB.open(name,version);
    request.onerror=function(e){
        console.log(e.currentTarget.error.message);
    };
    request.onsuccess=function(e){
        myDB.db=e.target.result;
    };
    request.onupgradeneeded=function(e){
        var db=e.target.result;
        if(!db.objectStoreNames.contains('students')){
            db.createObjectStore('students',{keyPath:"id"});
        }
        console.log('DB version changed to '+version);
    };
}

var students=[{ 
    id:1001, 
    name:"Byron", 
    age:24 
},{ 
    id:1002, 
    name:"Frank", 
    age:30 
},{ 
    id:1003, 
    name:"Aaron", 
    age:26 
}];

function addData(db,storeName){
    var transaction=db.transaction(storeName,'readwrite'); 
    var store=transaction.objectStore(storeName); 

    for(var i=0;i<students.length;i++){
        store.add(students[i]);
    }
}


openDB(myDB.name,myDB.version);
setTimeout(function(){
    addData(myDB.db,'students');
},1000);

查找数据

可以调用object store的get方法通过键获取数据,以使用keyPath做键为例

function getDataByKey(db,storeName,value){
    var transaction=db.transaction(storeName,'readwrite'); 
    var store=transaction.objectStore(storeName); 
    var request=store.get(value); 
    request.onsuccess=function(e){ 
        var student=e.target.result; 
        console.log(student.name); 
    };
}

更新数据

可以调用object store的put方法更新数据,会自动替换键值相同的记录,达到更新目的,没有相同的则添加,以使用keyPath做键为例

function updateDataByKey(db,storeName,value){
    var transaction=db.transaction(storeName,'readwrite'); 
    var store=transaction.objectStore(storeName); 
    var request=store.get(value); 
    request.onsuccess=function(e){ 
        var student=e.target.result; 
        student.age=35;
        store.put(student); 
    };
}

删除数据及object store

调用object store的delete方法根据键值删除记录

function deleteDataByKey(db,storeName,value){
    var transaction=db.transaction(storeName,'readwrite'); 
    var store=transaction.objectStore(storeName); 
    store.delete(value); 
}

调用object store的clear方法可以清空object store

function clearObjectStore(db,storeName){
    var transaction=db.transaction(storeName,'readwrite'); 
    var store=transaction.objectStore(storeName); 
    store.clear();
}

调用数据库实例的deleteObjectStore方法可以删除一个object store,这个就得在onupgradeneeded里面调用了

if(db.objectStoreNames.contains('students')){ 
    db.deleteObjectStore('students'); 
}

JS类的继承

ES5

原型继承

// 声明父类
function SuperClass () {
    this.superValue = true;
}
// 为父类添加共有方法
SuperClass.prototype.getSuperValue = function () {
    return this.superValue;
}
// 声明子类
function SubClass () {
    this.subValue = false;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function () {
    return this.subValue;
}

var instance = new SubClass();
console.log(instance.getSuperValue()); // true
console.log(instance.getSubValue()); // false

console.log(instance instanceof SuperClass); // true
console.log(instance instanceof SubClass); // true
console.log(SubClass instanceof SuperClass); // false instanceof只是判断前面的对象是否是后面类(对象)的实例,并不表示两者的继承。

原型继承有两个缺点。其一,由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。其二,由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。

function SuperClass () {
    this.books = ['js', 'html', 'css']
}
function SubClass () {}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books); // ['js', 'html', 'css']
instance1.books.push('blog');
console.log(instance2.books); // ['js', 'html', 'css', 'blog']

构造函数继承

// 声明父类
function SuperClass (id) {
    // 引用类型共有属性
    this.books = ['js', 'html', 'css'];
    this.id = id;
}
// 父类声明原型方法
SuperClass.prototype.showBooks = function () {
    console.log(this.books);
}
// 声明子类
function SubClass (id) {
    // 继承父类
    SuperClass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new SubClass(10);
// 创建第二个子类的实例
var instance2 = new SubClass(11);

instance1.books.push('blog');
console.log(instance1.books); // ['js', 'html', 'css', 'blog']
console.log(instance1.id); // 10
console.log(instance2.books); // ['js', 'html', 'css']
console.log(instance2.id); // 11

instance1.showBooks); // TypeError

SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境。因此在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。为了综合这两种模式的优点,后来有了组合式继承。

组合继承

// 声明父类
function SuperClass (name) {
    // 值类型共有属性
    this.name = name;
    // 引用类型共有属性
    this.books = ['html', 'css', 'js'];
}
// 父类原型共有方法
SuperClass.prototype.getName = function () {
    console.log(this.name);
}
// 声明子类
function SubClass (name, time) {
    // 构造函数式继承父类name属性
    SuperClass.call(this, name);
    // 子类中新增共有属性
    this.time = time;
}
// 类式继承 子类型原型继承父类
SubClass.prototype = new SuperClass();
// 子类原型方法
SubClass.prototype.getTime = function () {
    console.log(this.time);
}

子类的实例中更改父类继承下来的引用类型属性如books,不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中,如name。

var instance1 = new SubClass('js book', 2017);
instance1.books.push('blog');
console.log(instance1.books); // ['html', 'css', 'js', 'blog']
instance1.getName(); // js book
instance1.getTime(); // 2017

var instance2 = new SubClass('css book', 2016);
console.log(instance2.books); // ['html', 'css', 'js']
instance2.getName(); // css book
instance2.getTime(); // 2016

原型式继承

寄生式继承

寄生组合式继承

多继承

在js中继承是依赖于原型prototype链实现的,只有一条原型链,所以理论上是不能继承多个父类的。然后js是灵活的,通过一些方式可以实现多继承,比如extend方法。

// 单继承 属性复制
var extend = function (target, source) {
    // 遍历源对象中的属性
    for (var property in source) {
        // 将源对象中的属性复制到目标对象中
        target[property] = source[property];
    }
    // 返回目标对象
    return target;
}
var book = {
    name: 'JavaScript',
    alike: ['css', 'html', 'js']
}
var anotherBook = {
    color: 'blue'
}
extend(anotherBook, book);
console.log(anotherBook, name); // JavaScript
console.log(anotherBook.alike); // ['css', 'html', 'js']

anotherBook.alike.push('ajax');
anotherBook.name = 'blog';
console.log(anotherBook.name); // blog
console.log(anotherBook.alike); // ['css', 'html', 'js', 'ajax']
console.log(book.name); // JavaScript
console.log(book.alike); // ['css', 'html', 'js', 'ajax']

实现多继承的方式在上面方法上多传递几个对象

// 多继承 属性复制
var mix = function () {
    var i = 1, // 从第二个参数起为被继承的对象
        len = arguments.length, // 获取参数长度
        target = arguments[0], // 从第一个对象为目标对象
        arg; // 缓存参数对象
    // 遍历被继承的对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i];
        // 遍历被继承对象中的属性
        for (var property in arg) {
            // 将被继承对象中的属性复制到目标对象中
            target[property] = arg[property];
        }
    }
    // 返回目标对象
    return target;
}

但是在使用的时候需要传入目标对象(第一个参数–需要继承的对象)。可以将它绑定到原生对象Object上,这样所有的对象就可以拥有这个方法。

Object.prototype.mix = function () {
    var i = 0, // 从第一个参数起为被继承的对象
        len = arguments.length, // 获取参数长度
        arg; // 缓存参数对象
    // 遍历被继承的对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i];
        // 遍历被继承对象中的属性
        for (var property in arg) {
            // 将被继承对象中的属性复制到目标对象中
            this[property] = arg[property];
        }
    }
}

otherBook.mix(book1, book2);
console.log(otherBook); // Object {color: 'blue', name: 'JavaScript', mix: function}

多态

多态,就是同一个方法多种调用方法。

function add () {
    // 获取参数
    var arg = arguments,
        // 获取参数长度
        len = arg.length;
    switch (len) {
        // 如果没有参数
        case 0:
            return 10;
        // 如果只有一个参数
        case 1:
            return 10 + arg[0];
        // 如果有两个参数
        case 2:
            return arg[0] + arg[1];
    }
}

console.log(add()); // 10
console.log(add(5)); // 15
console.log(add(6, 7)); // 13

或者转化成类形式,将不同运算方法封装在类内,这样代码更易懂

function Add () {
    // 无参数算法
    function zero () {
        return 10;
    }
    // 一个参数算法
    function one (num) {
        return 10 + num;
    }
    // 两个参数算法
    function two (num1, num2) {
        return num1 + num2;
    }
    // 相加共有方法
    this.add = function () {
        var arg = arguments,
        // 获取参数长度
        len = arg.length;
        switch (len) {
            // 如果没有参数
            case 0:
                return zero();
            // 如果只有一个参数
            case 1:
                return one(arg[0]);
            //如果有两个参数
            case 2:
                return two(arg[0], arg[1]);
        }
    }
}

var A = new Add();
console.log(A.add()); // 10
console.log(A.add(5)); // 15
console.log(A.add(6, 7)); // 13

ES6

简介

Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰方便的多。下面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

class Point {
}

class ColorPoint extends Point {
}

下面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。下面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。下面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)。下面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。下面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。p是父类A实例的属性,super.p就引用不到它。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

ES6 规定,通过super调用父类的方法时,super会绑定子类的this。下面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

JS类

ES5

构造函数方法

var Book = function (id, name, price) {
    var num = 1; // 私有属性(外界访问不到)
    function checkId () {} // 私有方法(外界访问不到)
    this.id = id; // 公有属性(外界可以访问)
    this.copy = function () {} // 公有方法(外界可以访问)
    this.getNum = function () {} // 特权方法(可以通过该方法访问私有属性)
}
Book.prototype = {
    isJSBook: false, // 共有属性
    display: function () {} // 共有方法
}
Book.isChinese = true; // 类的静态共有属性(对象不能访问)
Book.resetTime = function () {} // 类的静态共有方法(对象不能访问)

var b = new Book(11, 'js', 50);
console.log(b.num); // undefined
console.log(b.isJSBook); // false
console.log(b.id); // 11
console.log(b.isChinese); // undefined

Object.create()方法

用这个方法,“类”就是一个对象,不是函数。

var Book = {
    id: 1,
    checkId: function () {}
}
var b = Object.create(Book);

极简主义方法


ES6

class Book {
    #id; // 私有属性
    #getId() {return #id}; // 私有方法
    constructor(id, name) {
        #id = id;
        this.name = name;
    }
}

私有方法的实现

私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
一种做法是在命名上加以区别。_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

this的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

另一种解决方法是使用箭头函数。

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }

  // ...
}

还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());

Class的取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

存值函数和取值函数是设置在属性的 Descriptor 对象上的。存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

Class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}

Foo.bar() // hello

JS的算法--排序算法

冒泡排序

最简单的排序方式,但是运行性能是最差的。
冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,因此得名。
首先,声明一个名为length的变量,用来存储数组的长度。接着,外循环会从数组的第一位迭代至最后一位,它控制了在数组中经过多少轮排序。然后,内循环从第一位开始,每一轮减少(i+1)位,内循环实际上进行当前项和下一项的比较。如果这两项顺序不对(当前项比下一项大),则交换它们,意思是位置为j+1的值将会被换置到位置j处,反之亦然。

this.bubbleSort = function(array){
    var length = array.length;
    var swap = function(index1, index2){
        var aux = array[index1];
        array[index1] = array[index2];
        array[index2] = aux;
    };
    for (var i=0; i<length; i++){
        for (var j=0; j<length-i-1; j++ ){
            if (array[j] > array[j+1]){
                swap(j, j+1);
            }
        }
    }
    console.log(array);
};

选择排序

选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
首先声明一些将在算法内使用的变量。接着,外循环迭代数组,并控制迭代轮次(数组的第n个值–下一个最小值)。我们假设本迭代轮次的第一个值为数组最小值;如果是,则改变最小值至新最小值。当内循环结束,将得出数组第n小的值。最后,如果该最小值和原最小值不同,则交换其值。

this.selectionSort = function(){
    var length = array.length,
        indexMin;
    for (var i=0; i<length-1; i++){
        indexMin = i;
        for (var j=i; j<length; j++){
            if(array[indexMin]>array[j]){
                indexMin = j;
            }
        }
        if (i !== indexMin){
            swap(i, indexMin);
        }
    }
};

未完待续。。。

JS数据结构学习(三)

树是一种分层数据的抽象模型。一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点。位于树顶部的节点叫作根节点。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。至少有一个子节点的节点称为内部节点。没有子元素的节点称为外部节点。

二叉树和二叉搜索树

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
二叉搜索树是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。

实现二叉搜索树

对于树,存在两个指针,一个指向左侧子节点,另一个指向右侧子节点。

  1. insert(key):向树中插入一个新的键。
  2. search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false。
  3. inOrderTraverse:通过中序遍历方式遍历所有节点。
  4. preOrderTraverse:通过先序遍历方式遍历所有节点。
  5. postOrderTraverse:通过后序遍历方式遍历所有节点。
  6. min:返回树中最小的值/键。
  7. max:返回树中最大的值/键。
  8. remove(key):从树中移除某个键。

    未完待续。。。

JS数据结构学习(二)

集合(ES6 set)

集合是由一组无序且唯一(不能重复)的项组成的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
空集是不包含任何元素的集合。
可以把集合想象成一个既没有重复元素,也没有顺序概念的数组。

实现

声明集合的方法:

  1. add(value):向集合添加一个新的项。
  2. remove(value):从集合移除一个值。
  3. has(value):如果值在集合中,返回true,否则返回false。
  4. clear():移除集合中的所有项。
  5. size():返回集合所包含元素的数量。与数组的length属性类似。
  6. values():返回一个包含集合中所有值得数组。
function Set() {
    var items = {};
    this.add = function(value){
        if (!this.has(value)){
            items[value] = value;
            return true;
        }
        return false;
    };
    this.remove = function(value){
        if (this.has(value)){
            delete items[value];
            return true;
        }
        return false;
    };
    this.has = function(value){
        return items.hasOwnProperty(value);
    };
    this.clear = function(){
        items = {};
    };
    this.size = function(){
        return Object.keys(items).length;
    };
    this.values = function(){
        return Object.keys(items);
    };
}

并集

this.union = function(otherSet){
    var unionSet = new Set(); //{1}
    var values = this.values(); //{2}
    for (var i=0; i<values.length; i++){
        unionSet.add(values[i]);
    }
    values = otherSet.values(); //{3}
    for (var i=0; i<values.length; i++){
        unionSet.add(values[i]);
    }
    return unionSet;
};

测试上述代码:

var setA = new Set();
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set();
setB.add(3);
setB.add(4);
setB.add(5);
setB.add(6);

var unionAB = setA.union(setB);
console.log(unionAB.values()); // ["1", "2", "3", "4", "5", "6"]

交集

this.intersection = function(otherSet){
    var intersectionSet = new Set(); //{1}
    var values = this.values();
    for (var i=0; i<values.length; i++){ //{2}
        if (otherSet.has(values[i])){ //{3}
            intersectionSet.add(values[i]); //{4}
        }
    }
    return intersectionSet;
}

测试上述代码:

var setA = new Set();
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set();
setB.add(2);
setB.add(3);
setB.add(4);

var intersectionAB = setA.intersection(setB);
console.log(intersectionAB.values()); // ['2', '3']

差集

this.difference = function(otherSet){
    var differenceSet = new Set();
    var values = this.values();
    for (var i=0; i<values.length; i++){
        if (!otherSet.has(values[i])){
            differenceSet.add(values[i]);
        }
    }
return differenceSet;
};

测试上述代码:

var setA = new Set();
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set();
setB.add(2);
setB.add(3);
setB.add(4);

var differenceAB = setA.difference(setB);
console.log(differenceAB.values()); // ['1']

子集

this.subset = function(otherSet){
    if (this.size() > otherSet.size()){
        return false;
    } else {
        var values = this.values();
        for (var i=0; i<values.length; i++){
            if (!otherSet.has(values[i])){
                return false;
            }
        }
        return true; //{5}
    }
};

测试上述代码:

var setA = new Set();
setA.add(1);
setA.add(2);

var setB = new Set();
setB.add(1);
setB.add(2);
setB.add(3);

var setC = new Set();
setC.add(2);
setC.add(3);
setC.add(4);

console.log(setA.subset(setB));
console.log(setA.subset(setC));
// setA是setB的子集(因此输出为true),然而setA不是setC的子集(setC
只包含了setA中的2,而不包含1),因此输出为false。

字典(ES6 map)

字典和集合很相似,集合以[值, 值]的形式存储元素,字典则是以[键, 值]的形式来存储元素,也称作映射。

散列表

散列算法的作用是尽可能快地在数据结构中找到一个值。如果要在数据结构中获得一个值,需要遍历整个数据结构来找到它。如果使用散列函数,就知道值得具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址。

JS数据结构学习(一)

是一种遵从后进先出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

实现

声明栈的方法:

  1. push():添加一个(或多个)新元素到栈顶。
  2. pop():移除栈顶的元素,同时返回被移除的元素。
  3. peek():返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)。
  4. isEmpty():如果栈里没有任何元素就返回true,否则返回false。
  5. clear():移除栈里的所有元素。
  6. size():返回栈里的元素个数。这个方法和数组的length属性很类似。
function Stack() {
    var items = [];

    this.push = function(element) {
        items.push(element);
    };

    this.pop() = function() {
        return items.pop();
    };

    this.peek = function() {
        return items[items.length-1];
    };

    this.isEmpty = function() {
        return items.length === 0;
    };

    this.clear = function() {
        items = [];
    };

    this.size = function() {
        return items.length;
    }
}

队列

队列是遵循先进先出(FIFO)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。

实现

声明队列的方法:

  1. enqueue(element(s)):向队列尾部添加一个(或多个)新的项。
  2. dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
  3. front():返回队列中第一个元素。队列不做任何变动。
  4. isEmpty():如果队列中不包含任何元素,返回true,否则返回false。
  5. clear():移除栈里的所有元素。
  6. size():返回队列包含的元素个数,与数组的length属性类似。
function Queue() {
    var items = [];

    this.enqueue = function(element) {
        items.push(element);
    };

    this.dequeue = function() {
        return items.shift();
    };

    this.front = function() {
        return items[0];
    };

    this.isEmpty = function() {
        return items.length === 0;
    };

    this.clear = function() {
        items = [];
    };

    this.size = function() {
        return items.length;
    }
}

优先队列

队列实现的一个修改版,元素的添加和移除是基于优先级的。实现一个优先队列,有两种选项:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照优先级移除它们。

function PriorityQueue() {
    var items = [];

    function QueueElement(element, priority) {
        this.element = element;
        this.priority = priority;
    }

    this.enqueue = function(element, priority) {
        var queueElement = new QueueElement(element, priority);
        if(this.isEmpty()) {
            items.push(queueElement);
        } else {
            var added = false;
            for(var i=0;i<items.length;i++) {
                if(queueElement.priority < items[i].priority) {
                    items.splice(i, 0, queueElement);
                    added = true;
                    break;
                }
            }
            if(!added) {
                items.push(queueElement);
            }
        }
    }
}

循环队列–击鼓传花

还有另一个修改版的队列实现,就是循环队列。循环队列的一个例子就是击鼓传花游戏。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜者)。

function hotPotato (nameList, num){
    var queue = new Queue(); // {1}
    for (var i=0; i<nameList.length; i++){
        queue.enqueue(nameList[i]); // {2}
    }
    var eliminated = '';
    while (queue.size() > 1){
        for (var i=0; i<num; i++){
            queue.enqueue(queue.dequeue()); // {3}
        }
        eliminated = queue.dequeue();// {4}
    console.log(eliminated + '在击鼓传花游戏中被淘汰。');
    }
    return queue.dequeue();// {5}
}
var names = ['John','Jack','Camila','Ingrid','Carl'];
var winner = hotPotato(names, 7);
console.log('胜利者:' + winner);

链表

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点开始迭代列表直到找到所需的元素。

JS的真值和假值

定义

JS中的每个值都有固定的布尔值,在布尔表达式中评估该值时,该值就会转换为固有的布尔值。这些固有的值称为真值或假值。结果为false的值称为假值,其余的皆为真值。

假值

JS中有六个假值:

false
null
undefined
0
'' // 空字符串
NaN

虽然这六个值都为假值,它们之间并非都相等:

console.log( false == null )      // false
console.log( false == undefined ) // false
console.log( false == 0 )         // true
console.log( false == '' )        // true
console.log( false == NaN )       // false

console.log( null == undefined ) // true
console.log( null == 0 )         // false
console.log( null == '' )        // false
console.log( null == NaN )       // false

console.log( undefined == 0)   // false
console.log( undefined == '')  // false
console.log( undefined == NaN) // false

console.log( 0 == '' )  // true
console.log( 0 == NaN ) // false