Giter Club home page Giter Club logo

blog's Introduction

通知!!!

新增博客会在语雀平台更新,地址:https://www.yuque.com/liuyanping-uxwgm

HTML

你不知道的HTML

CSS

CSS世界

JavaScript

JavaScript基础专题(完结)
JavaScript进阶专题(进行中)
ES6专题(进行中)
TypeScript专题

计算机基础

设计模式专题(完结)
HTTP网络专题(进行中)
数据结构专题(进行中)
算法专题(进行中)
操作系统专题(完结)

前端框架

Vue专题(进行中)
React专题(进行中)

全栈之路

Node专题
Python专题
Golang专题
数据库专题

移动端

小程序专题
ReactNative专题
Flutter专题

其他

Web性能优化与安全
浏览器工作原理
前端工程化(进行中)
年终总结

blog's People

Contributors

timelessover avatar

Stargazers

 avatar  avatar

Forkers

chaoxiaochao

blog's Issues

设计模式专题之工厂模式(六)

工厂模式(Factory Pattern)

定义:创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象

简单的说

  • 将 new 操作单独封装,只对外提供相应接口
  • 遇到new 时,就要考虑是否应该使用工厂模式

工厂模式有三种形式:简单工厂模式(Simple Factory)、工厂方法模式(Factory Method)和抽象工厂模式(Abstract Factory)。在 javascript 中我们最常见的当属简单工厂模式。

作用

  • 主要用于隐藏创建实例的复杂度,只需对外提供一个接口
  • 实现构造函数和创建者的分离,满足开放封闭的原则

实例

// 定义产品
class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('初始化产品')
    }
}

// 定义工厂
class Factory {
    create(name) {
        return new Product(name) // 重点!!!
    }
}
let c = new Factory()
let p = c.create('p1')
p.init()

工厂模式最直观的地方在于,创建产品对象不是通过直接new 产品类实现,而是通过工厂方法实现。现在再用一个稍微有些好看的例子描述一下简单工厂:

//User类
class User {
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通讯录'] });
        break;
      default:
        throw new Error('params error')
    }
  }
}

//调用
let superAdmin = user.getInstance('superAdmin');
let admin = user.getInstance('admin');

通过上例,我们可以看到,每次创建新的对象实例时,只需要传入相应的参数,就可以得到指定的对象实例。最直观的例子是如果不用工厂模式,那代码中是不是就会多出好多个new,这样看着也不太舒服。

其实简单工厂模式已经能满足我们前端大部分业务场景了,如果非要说其一个缺陷,那就是每次有新实例时,我们需要重写这个User大类,总归感觉和后面所述的装饰器模式有一些冲突。此时,工厂方法模式就出来了,其核心**就是独立出一个大的User类,将创建实例对象的过程用其子类来实现:

class User {
  constructor(name = '', viewPage = []) {
    this.name = name;
    this.viewPage = viewPage;
  }
}

class UserFactory extends User {
  constructor(name, viewPage) {
    super(name, viewPage)
  }
  create(role) {
    switch (role) {
      case 'superAdmin': 
        return new UserFactory( '超级管理员', ['首页', '通讯录', '发现页', '应用数据', '权限管理'] );
        break;
      case 'admin':
        return new UserFactory( '管理员', ['首页', '通讯录'] );
        break;
      default:
        throw new Error('params error');
    }
  }
}
let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');

这样,虽然也得通过 new 一个实例,但至少我们可以无需修改User类里面的东西,虽说代码量上感觉和简单模式差不了多少,但**主体确实就是这样。

应用场景

jQuery选择器

$('div')new $('div') 有何区别? 为什么 $('div') 就能直接实现 new的效果,同时去除了 new $('div') 这种$('div') 去除了 new 书写繁杂的弊端,还能实现完美的链式操作代码简介,就是因为$内置的实现机制是工厂模式。其底层代码如下:

class jQuery {
    constructor(selector) {
        super(selector)
    }
    // ...
}

window.$ = function(selector) {
    return new jQuery(selector)
}

Vue 异步组件

Vue.component('async-example' , (resolve , reject) => {
    setTimeout(function() {
        resolve({
            template: `<div>I am async!</div>`
        })
    }, 1000)
})

除了上述两个常见的实例场景,还有React.createElement() 也是工厂原理。所以,当我们平时遇到要创建实例的时候,就可以想想能否用工厂模式实现了。

设计模式专题之单例模式(一)

单例模式(Singleton Pattern)

定义:这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

可以概括为两点:

  • 确保只有一个实例
  • 可以全局访问

实现单例模式

const singleton = function(name) {
  this.name = name
  this.instance = null
}

singleton.getInstance = function(name) {
  if (!this.instance) { // 创建过一次就能再次创建了
    this.instance = new singleton(name)
  }
  return this.instance
}

// 只能创建一遍
const a = singleton.getInstance('a') // 通过 getInstance 来获取实例
const b = singleton.getInstance('b')
console.log(a === b) //true 

JavaScript 中的单例模式

因为 JavaScript 没有类得概念,而且全局对象也符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用会更方便一些。

var obj = {}

适用范围

适用于弹框的实现, 全局缓存

弹框层的实现

实现一个弹框一种做法是先创建好弹框,然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销,另一种是我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例,,从而节省部分 DOM 开销。下列为弹框部分代码:

const createLayer = function() {
  const div = document.createElement('div')
  div.innerHTML = '我出现了'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}

使单例模式和创建弹框代码进行解耦

const getSingle = function(fn) {
  const result
  return function() {
    return result || result = fn.apply(this, arguments)
  }
}
//创建一次,可多次调用
const createSingleLayer = getSingle(createLayer)

document.getElementById('Btn').onclick = function() {
  createSingleLayer()
}

设计模式专题之命令模式(十一)

命令模式(Command Pattern)

定义:一种数据驱动的设计模式,请求以命令的形式包裹在对象中,并传给调用对象,调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

  • 行为与数据的分离
  • 对象中含有多个命令进行调用

JavaScript 中的命令模式

命令模式在 JavaScript 中也比较简单, 下面代码中对按钮命令进行了抽离, 因此可以复杂项目中可以使用命令模式将界面的代码和功能的代码交付给不同的人去写。

const setCommand = function(button, command) {
  button.onClick = function() {
    command.excute()
  }
}

// 上面的界面逻辑由A完成, 下面的由B完成

// 对象中含有多个命令
const menu = {
  updateMenu: function() {
    console.log('更新菜单')
  },
  addMenu:fuction() {
  	 console.log('增加菜品')
  }
}
// 对不同命令进行解耦合
const UpdateCommand = function(receive) {
  return {
    excute: receive.updateMenu
  }
}
const AddCommand = function(receive) {
  return {
    excute: receive.addMenu
  }
}

const updateCommand = UpdateCommand(menu) // 创建命令
const addCommand = AddCommand(menu) // 创建命令

const button1 = document.getElementById('button1')
const button2 = document.getElementById('button2')

setCommand(button1, updateCommand)
setCommand(button2, addCommand)

数据结构专题之哈希表(四)

哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。

有两种不同类型的哈希表:哈希集合和哈希映射。

  • 哈希集合集合数据结构的实现之一,用于存储非重复值
  • 哈希映射映射 数据结构的实现之一,用于存储(key, value)键值对。

设计哈希集合

MyHashSet hashSet = new MyHashSet();
hashSet.add(1);         
hashSet.add(2);         
hashSet.contains(1);    // 返回 true
hashSet.contains(3);    // 返回 false (未找到)
hashSet.add(2);          
hashSet.contains(2);    // 返回 true
hashSet.remove(2);          
hashSet.contains(2);    // 返回  false (已经被删除)
/**
 * Initialize your data structure here.
 */
var MyHashSet = function() {
    this.hash = {}
};

/** 
 * @param {number} key
 * @return {void}
 */
MyHashSet.prototype.add = function(key) {
    if(!this.hash[key]){
       this.hash[key] = true
    }
};

/** 
 * @param {number} key
 * @return {void}
 */
MyHashSet.prototype.remove = function(key) {
    if(this.hash[key]){
      delete(this.hash[key])
    }
};

/**
 * Returns true if this set contains the specified element 
 * @param {number} key
 * @return {boolean}
 */
MyHashSet.prototype.contains = function(key) {
    return !!this.hash[key]
};

/** 
 * Your MyHashSet object will be instantiated and called as such:
 * var obj = new MyHashSet()
 * obj.add(key)
 * obj.remove(key)
 * var param_3 = obj.contains(key)
 */

哈希集

哈希集是集合的实现之一,它是一种存储不重复值的数据结构。

ES6 引入的 Set 就是哈希集的数据结构类型。

简易版 Set 实现:

add(value)

delete(value)

has(value)

clear()

function Set() {
  this.items = {}
  this.size = 0
}

Set.prototype.add = function(value) {
  if (!this.items[value]) {
    this.items[value] = value // 这样子不能实现存储数组、对象
    this.size = Object.keys(this.items).length
  }
}

Set.prototype.has = function(value) {
  if (this.items.hasOwnProperty(value)) {
    return true
  } else {
    return false
  }
}

Set.prototype.delete = function(value) {
  for (let i in this.items) {
    if (this.items.hasOwnProperty(i)) {
      if (i === value.toString()) {
        delete(this.items[i])
        this.size = Object.keys(this.items).length
        return true
      }
    }
  }
}

Set.prototype.clear = function() {
  this.items = {}
  this.size = 0
}

set 类型的可以用 Array.from 将之转为数组类型

let unique = (likeArr)=>{
	return Array.from(new Set(likeArr))
}

创建 Set 对象

// 方法一
const set1 = new Set()
set.add(1)

// 方法二
const set2 = new Set([1])

哈希映射

哈希映射是用于存储 (key, value) 键值对的一种实现。

ES6 引入的 Map 就是字典的数据类型。

简易版 Map 实现

function Map() {
  this.items = {}
  this.size = 0
}

Map.prototype.has = function(key) {
  for (let i in this.items) {
    if (this.items.hasOwnProperty(i)) {
      return true
    }
  }
  return false
}

Map.prototype.delete = function(key) {
  if (this.has(key)) {
    delete(this.items[key])
    this.size--
    return true
  }
  return false
}

Map.prototype.set = function(key, value) {
  this.items[key] = value // 这里是不严谨的实现
  this.size++
}

Map.prototype.get = function(key) {
  return this.items[key]
}

Map.prototype.clear = function() {
  this.items = {}
  this.size = 0
}

Map.prototype.values = function() {
  const arr = []
  Object.keys(this.items).forEach(r => {
    arr.push(this.items[r])
  })
  return arr
}

创建一个 Map 对象

// 方式一
const map1 = new Map()
map.set('a', 1)

// 方式二
const map2 = new Map([['a', 1]])

设计模式专题之适配器模式(十六)

适配器模式(Adapter Pattern)

定义:作为两个不兼容的接口之间的桥梁。

情景:

读卡器是作为内存卡和笔记本之间的适配器。将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

实例

// 老接口
const zhejiangCityOld = (function() {
  return [
    {
      name: 'hangzhou',
      id: 11,
    },
    {
      name: 'jinhua',
      id: 12
    }
  ]
}())

console.log(getZhejiangCityOld())

// 新接口希望是下面形式
{
  hangzhou: 11,
  jinhua: 12,
}

// 这时候就可采用适配者模式
const adaptor = (function(oldCity) {
  const obj = {}
  for (let city of zhejiangCityOld) {
    obj[city.name] = city.id
  }
  return obj
}())

设计模式专题之桥接模式(十九)

桥接模式(Bridge Pattern)

定义:桥接模式是用于把抽象化与实现化解耦,使得二者可以独立变化。在系统沿着多个维度变化的时候,不增加复杂度以达到解耦的目的。

在我们日常开发中,需要对相同的逻辑做抽象的处理。桥接模式就是为了解决这类的需求。

//运动单元
class Speed {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  run() {
    console.log('动起来');
  }
}

// 着色单元
class Color {
  constructor(cl) {
    this.color = cl;
  }
  draw() {
    console.log('绘制色彩');
  }
}

//说话单元
class Speak {
  constructor(wd) {
    this.word = wd;
  }
  say() {
    console.log('请开始你的表演');
  }
}

//创建球类,并且它可以运动可以着色
class Ball {
  constructor(x, y, c) {
    this.speed = new Speed(x, y);
    this.color = new Color(c);
  }
  init() {
    //实现运动和着色
    this.speed.run();
    this.color.draw();
  }
}

class People {
  constructor(x, y, f) {
    this.speed = new Speed(x, y);
    this.speak = new Speak(f);
  }
  init() {
    this.speed.run();
    this.speak.say();
  }
}

// 当我们实例化一个人物对象的时候,他就可以有对应的方法实现了

var p = new People(10, 12, '我是一个人');
p.init();
var ball = new Ball(10, 12, 'red');
ball.init();

Vue专题之从生命周期(一)

概览

lifecycle

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue-demo</title>
    <script src="https://cdn.bootcss.com/vue/2.6.11/vue.js"></script>
</head>

<body>
    <div id="root">
        {{name}}
    </div>
    <script>

        const vm = new Vue({
            el:"#root",
            data: {
                name: 'Chris'
            },
            // 初始化Vue基本属性,数据没有初始化
            beforeCreate() {
                console.log(this.name)  // undefined
            },
            // 数据初始化完毕,可以打印data,但是视图不渲染,用于请求
            created() {
                console.log(this.name)
            },
            // 进行template与render函数选择编译,为渲染 VDOM 做准备。
            beforeMount() {
                console.log(this.name)
            },
            // 执行render,生产真实 DOM , 渲染视图
            mounted() {
                console.log(this.name)
            },
            // 数据改变,之后执行diff与render函数,生产新的 VDOM
            beforeUpdate () {
                console.log(this.name)
            },
            // render函数已经执行完毕,生产新的 DOM
            updated() {
                console.log(this.name)
            },
            // 销毁前对象所有对象属性前调用
            beforeDestroy() {
                console.log(this.name)
            },
            // 组件对象完全销毁调用
            destroyed() {
                console.log(this.name) // undefined
            },

        })
        //vm.$mount("#root")
    </script>
</body>

</html>

入口

function Vue (options) {
	// 没用调用new初始化提示警告
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    // 初始化内部对象,options就是我们所需要渲染的数据和方法
    this._init(options);
  }
  // _init在initMixin内部,主要是处理了beforeCreate与created两个钩子
  initMixin(Vue);
  // data中对象的响应式的处理,原型上添加了$set,$del,$watch等处理对象的方法
  stateMixin(Vue);
  // Vue的eventEmitter
  eventsMixin(Vue);
  // Vue实例销毁,
  lifecycleMixin(Vue);
  renderMixin(Vue);

initMixin 方法中内置很多原型方法,执行了 beforeCreatecreate两个钩子

function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // 每个初始化的组件都会又自己的uid,方便以后diff算法
      vm._uid = uid$3++;
        
      // 合并 options,因为vue会内置一些属性在原型链上
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      // 保存外部vm对象
      vm._self = vm;
      // 初始化生命周期,
      initLifecycle(vm);
      // 初始化父子组件通信事件
      initEvents(vm);
      // 初始化render函数  
      initRender(vm);
      // 调用beforeCreate钩子,这个时候数据还没初始化,this.data并没有数据
      callHook(vm, 'beforeCreate');
      // 检测是有inject属性,可以进行data注入
      initInjections(vm); // resolve injections before data/props
      // 执行该函数后我们才可以在this中获取data数据,方法,计算属性等
      initState(vm);
      // 这一步需要在组件数据初始化后才调用,主要因为需要拿到this上的数据
      initProvide(vm); // resolve provide after data/props
      // 执行created钩子,这个时候可以通过this.data拿到数据,但是没进行页面渲染
      callHook(vm, 'created');

	  // 实例中有el,就挂在为根节点,没有的话在mounted之前检测是否手动绑定
      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
  }

initLifecycle 只要挂载了一些基本属性到原型链上

function initLifecycle (vm) {
    var options = vm.$options;

    // 这里判断是否为父组件,有父组件就往成为父组件的$children
    var parent = options.parent;
    if (parent && !options.abstract) {
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
      }
      parent.$children.push(vm);
    }

    vm.$parent = parent;
    // 这里判断无父组件的化,该组件为根节点
    vm.$root = parent ? parent.$root : vm;
	
    // 初始化vm对象的一些原型链属性
    vm.$children = [];
    vm.$refs = {};
	
    // 目前组件的状态,包括组件生命周期和keep-alive周期
    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    vm._isMounted = false;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
  }

initEvents 监听子组件的方法

 function initEvents (vm) {
    vm._events = Object.create(null);
    vm._hasHookEvent = false;
    // 监听父组件是否刷新,之后更新子组件。所以父组件更新在子组件之前
    var listeners = vm.$options._parentListeners;
    if (listeners) {
      updateComponentListeners(vm, listeners);
    }
  }

initRender 将slots相关与编译相关的方法挂载到原型链上

function initRender (vm) {
    vm._vnode = null; // the root of the child tree
    vm._staticTrees = null; // v-once cached trees
    var options = vm.$options;
    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
    var renderContext = parentVnode && parentVnode.context;
    // slots 为组件包括的组件
    vm.$slots = resolveSlots(options._renderChildren, renderContext);
    vm.$scopedSlots = emptyObject;
    // bind the createElement fn to this instance
    // so that we get proper render context inside it.
    // args order: tag, data, children, normalizationType, alwaysNormalize
    // internal version is used by render functions compiled from templates
    // 编译模板render函数,,这个里使用createElement创建节点
    vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
    // normalization is always applied for the public version, used in
    // user-written render functions.
    // 用户手写的render函数
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

    // $attrs & $listeners are exposed for easier HOC creation.
    // they need to be reactive so that HOCs using them are always updated
    // $attrs,$listeners创建,两各属性暴露早于组件创建
    var parentData = parentVnode && parentVnode.data;

    /* istanbul ignore else */
    {
      defineReactive$$1(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
        !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
      }, true);
      defineReactive$$1(vm, '$listeners', options._parentListeners || emptyObject, function () {
        !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
      }, true);
    }
  }

initState 数据初始化,这个时候在 created之前执行

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

callHook 就是调用周期函数的钩子,内部原理就是发布订阅模式

function callHook (vm, hook) {
    // #7573 disable dep collection when invoking lifecycle hooks
    pushTarget();
    var handlers = vm.$options[hook];
    var info = hook + " hook";
    if (handlers) {
      for (var i = 0, j = handlers.length; i < j; i++) {
        invokeWithErrorHandling(handlers[i], vm, null, vm, info);
      }
    }
    // eventbus调用钩子,这里调用的就是我们在该钩子写的代码,
    if (vm._hasHookEvent) {
      vm.$emit('hook:' + hook);
    }
    popTarget();
  }

stateMixin 将$set,$delete,$watch方法挂载到原型链

  function stateMixin (Vue) {
    // flow somehow has problems with directly declared definition object
    // when using Object.defineProperty, so we have to procedurally build up
    // the object here.
    var dataDef = {};
    dataDef.get = function () { return this._data };
    var propsDef = {};
    propsDef.get = function () { return this._props };
    {
      dataDef.set = function () {
        warn(
          'Avoid replacing instance root $data. ' +
          'Use nested data properties instead.',
          this
        );
      };
      propsDef.set = function () {
        warn("$props is readonly.", this);
      };
    }
    Object.defineProperty(Vue.prototype, '$data', dataDef);
    Object.defineProperty(Vue.prototype, '$props', propsDef);

    Vue.prototype.$set = set;
    Vue.prototype.$delete = del;

    Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
      }
      return function unwatchFn () {
        watcher.teardown();
      }
    };
  }

Vue内部的发布订阅模式,用于函数内部通信

  function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    Vue.prototype.$on = function (event, fn) {
      var vm = this;
      if (Array.isArray(event)) {
        for (var i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn);
        }
      } else {
        (vm._events[event] || (vm._events[event] = [])).push(fn);
        // optimize hook:event cost by using a boolean flag marked at registration
        // instead of a hash lookup
        if (hookRE.test(event)) {
          vm._hasHookEvent = true;
        }
      }
      return vm
    };

    Vue.prototype.$once = function (event, fn) {
      var vm = this;
      function on () {
        vm.$off(event, on);
        fn.apply(vm, arguments);
      }
      on.fn = fn;
      vm.$on(event, on);
      return vm
    };

    Vue.prototype.$off = function (event, fn) {
      var vm = this;
      // all
      if (!arguments.length) {
        vm._events = Object.create(null);
        return vm
      }
      // array of events
      if (Array.isArray(event)) {
        for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
          vm.$off(event[i$1], fn);
        }
        return vm
      }
      // specific event
      var cbs = vm._events[event];
      if (!cbs) {
        return vm
      }
      if (!fn) {
        vm._events[event] = null;
        return vm
      }
      // specific handler
      var cb;
      var i = cbs.length;
      while (i--) {
        cb = cbs[i];
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1);
          break
        }
      }
      return vm
    };

    Vue.prototype.$emit = function (event) {
      var vm = this;
      {
        var lowerCaseEvent = event.toLowerCase();
        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
          tip(
            "Event \"" + lowerCaseEvent + "\" is emitted in component " +
            (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
            "Note that HTML attributes are case-insensitive and you cannot use " +
            "v-on to listen to camelCase events when using in-DOM templates. " +
            "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
          );
        }
      }
      var cbs = vm._events[event];
      if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        var args = toArray(arguments, 1);
        var info = "event handler for \"" + event + "\"";
        for (var i = 0, l = cbs.length; i < l; i++) {
          invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
      }
      return vm
    };
  }

$mount会寻找根元素与template模板,一种是传入el,第二种是手动挂载$mount

Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
    el = el && query(el);

    /* istanbul ignore if */
    if (el === document.body || el === document.documentElement) {
      warn(
        "Do not mount Vue to <html> or <body> - mount to normal elements instead."
      );
      return this
    }

    var options = this.$options;
    // 处理 template/el 转换成render函数
    if (!options.render) {
      var template = options.template;
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
            /* istanbul ignore if */
            if (!template) {
              warn(
                ("Template element not found or is empty: " + (options.template)),
                this
              );
            }
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          {
            warn('invalid template option:' + template, this);
          }
          return this
        }
      } else if (el) {
        template = getOuterHTML(el);
      }
      if (template) {
        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile');
        }

        var ref = compileToFunctions(template, {
          outputSourceRange: "development" !== 'production',
          shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;

        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile end');
          measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
        }
      }
    }
    // 编译结束,视图已经和数据绑定
    return mount.call(this, el, hydrating)
  };

mountComponent 内部调用 beforeMountmounted周期,将数据渲染到视图

 function mountComponent (
    vm,
    el,
    hydrating
  ) {
    // 可以拿到$el对象
    vm.$el = el;
     // 这边判断是否有用户手写的render
    if (!vm.$options.render) {
      // render为空节点
      vm.$options.render = createEmptyVNode;
      {
        /* istanbul ignore if */
        if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
          vm.$options.el || el) {
          warn(
            'You are using the runtime-only build of Vue where the template ' +
            'compiler is not available. Either pre-compile the templates into ' +
            'render functions, or use the compiler-included build.',
            vm
          );
        } else {
          warn(
            'Failed to mount component: template or render function not defined.',
            vm
          );
        }
      }
    }
    // 这一步执行用户设定的beforeMount周期,其实之前进行template与render函数选择编译,还没有进行渲染。
    callHook(vm, 'beforeMount');

    var updateComponent;
    /* istanbul ignore if */
    if (config.performance && mark) {
      updateComponent = function () {
        var name = vm._name;
        var id = vm._uid;
        var startTag = "vue-perf-start:" + id;
        var endTag = "vue-perf-end:" + id;

        mark(startTag);
        var vnode = vm._render();
        mark(endTag);
        measure(("vue " + name + " render"), startTag, endTag);

        mark(startTag);
        vm._update(vnode, hydrating);
        mark(endTag);
        measure(("vue " + name + " patch"), startTag, endTag);
      };
    } else {
      updateComponent = function () {
        vm._update(vm._render(), hydrating);
      };
    }
    // 响应数据改变,更新组件会调用beforeUpdate钩子
    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
    // 编译结束,视图已经和数据绑定
    if (vm.$vnode == null) {
      vm._isMounted = true;
      callHook(vm, 'mounted');
    }
    return vm
  }

_update 用于节点对比,用于根据数据改变正常的新旧 VDOM 新旧对比,生产新的Vnode执行 update 钩子更新视图

function _update (oldVnode, vnode) {
    var isCreate = oldVnode === emptyNode;
    var isDestroy = vnode === emptyNode;
    var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
    var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);

    var dirsWithInsert = [];
    var dirsWithPostpatch = [];

    var key, oldDir, dir;
    for (key in newDirs) {
      oldDir = oldDirs[key];
      dir = newDirs[key];
      if (!oldDir) {
        // new directive, bind
        callHook$1(dir, 'bind', vnode, oldVnode);
        if (dir.def && dir.def.inserted) {
          dirsWithInsert.push(dir);
        }
      } else {
        // existing directive, update
        dir.oldValue = oldDir.value;
        dir.oldArg = oldDir.arg;
        callHook$1(dir, 'update', vnode, oldVnode);
        if (dir.def && dir.def.componentUpdated) {
          dirsWithPostpatch.push(dir);
        }
      }
    }

    if (dirsWithInsert.length) {
      var callInsert = function () {
        for (var i = 0; i < dirsWithInsert.length; i++) {
          callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
        }
      };
      if (isCreate) {
        mergeVNodeHook(vnode, 'insert', callInsert);
      } else {
        callInsert();
      }
    }

    if (dirsWithPostpatch.length) {
      mergeVNodeHook(vnode, 'postpatch', function () {
        for (var i = 0; i < dirsWithPostpatch.length; i++) {
          callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
        }
      });
    }

    if (!isCreate) {
      for (key in oldDirs) {
        if (!newDirs[key]) {
          // no longer present, unbind
          callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
        }
      }
    }
  }

lifecycleMixin 内部实现了 $forceUpdate, $destroy 等属性,组件销毁也是内部调用了 $destroy 方法

function lifecycleMixin (Vue) {
    Vue.prototype._update = function (vnode, hydrating) {
      var vm = this;
      var prevEl = vm.$el;
      var prevVnode = vm._vnode;
      var restoreActiveInstance = setActiveInstance(vm);
      vm._vnode = vnode;
      // 这一步进行patch方法,利用diff算法进行新旧节点对比更新dom,没有旧节点会重新创建。
      if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
      } else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode);
      }
      restoreActiveInstance();
      // update __vue__ reference
      if (prevEl) {
        prevEl.__vue__ = null;
      }
      if (vm.$el) {
        vm.$el.__vue__ = vm;
      }
      // if parent is an HOC, update its $el as well
      if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
        vm.$parent.$el = vm.$el;
      }
      // updated hook is called by the scheduler to ensure that children are
      // updated in a parent's updated hook.
    };
	// $forceUpdate 方法强制重新渲染组件,不建议使用。源码内会在特殊情况使用使用,比如:子组件有v-if,v-for的情况;
    Vue.prototype.$forceUpdate = function () {
      var vm = this;
      if (vm._watcher) {
        vm._watcher.update();
      }
    };
	// $destroy  方法销毁组件,内部会调用,beforeDestroy与destroyed钩子
    Vue.prototype.$destroy = function () {
      var vm = this;
      if (vm._isBeingDestroyed) {
        return
      }
      // 这个时候是可以拿到响应数据的,下面是清空所有数据的操作
      callHook(vm, 'beforeDestroy');
      vm._isBeingDestroyed = true;
      // remove self from parent
      var parent = vm.$parent;
      if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
        remove(parent.$children, vm);
      }
      // teardown watchers
      // 销毁组件所有watcher数据
      if (vm._watcher) {
        vm._watcher.teardown();
      }
      var i = vm._watchers.length;
      while (i--) {
        vm._watchers[i].teardown();
      }
      // remove reference from data ob
      // frozen object may not have observer.
      // 去除this.data的observer,这里提供了一种优化手段就是Object.freeze。
      if (vm._data.__ob__) {
        vm._data.__ob__.vmCount--;
      }
      // call the last hook...
      vm._isDestroyed = true;
      // invoke destroy hooks on current rendered tree
      vm.__patch__(vm._vnode, null);
      // fire destroyed hook
      // 组件销毁,this上不再可以获取data数据
      callHook(vm, 'destroyed');
      // turn off all instance listeners.
      vm.$off();
      // remove __vue__ reference
      if (vm.$el) {
        vm.$el.__vue__ = null;
      }
      // release circular reference (#6759)
      if (vm.$vnode) {
        vm.$vnode.parent = null;
      }
    };
  }

总结

  • beforeCreate

在实例初始化之后,数据观测和事件配置之前被调用,此时组件的选项对象还未创建,el 和 data 并未初始化,因此无法访问methods, data, computed等上的方法和数据。

  • created

实例已经创建完成之后被调用,在这一步,实例已完成以下配置:数据监听、计算属性和方法,watch/event事件回调,完成了data 数据的初始化。 可以调用methods中的方法,改变data中的数据。通常用来进行一些 ajax 请求。

  • beforeMount

挂在开始之前被调用,相关的 render 函数首次被调用(虚拟DOM),实例已完成以下的配置: 编译模板,把data里面的数据和模板生成 VDOM,完成了el 和 data 初始化。

  • mounted

DOM 挂载完成,此时一般可以做一些 ajax 操作,mounted只会执行一次。

  • beforeUpdate

在数据更新之前被调用,发生在 VDOM 重新渲染与派发之前,可以在该钩子中进一步地更改状态,不会触发附加地重渲染过程。

  • updated

在由于数据更改导致 VDOM 重新渲染调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作,然后在大多是情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环,该钩子在服务器端渲染期间不被调用。

  • beforeDestroy

在实例销毁之前调用,实例仍然完全可用:

  1. 这一步还可以用this来获取实例,
  2. 一般在这一步做一些重置的操作,比如清除掉组件中的定时器 和 监听的dom事件
  • destroyed

在实例销毁之后调用,调用后,所以的事件监听器会被移出,所有的子实例也会被销毁,该钩子在服务器端渲染期间不被调用。

JavaScript基础专题之类型检测(十一)

基本类型

我们都知道 JavaScript 语言的每一个值都属于某一种数据类型。

JavaScript 的数据类型,共有分为七种:

  • 数值(number):整数和小数(比如1和3.14)
  • 字符串(string):文本(比如Hello World)
  • 布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)
  • undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值
  • null:表示空值,即此处的值为空。
  • 对象(object):各种值组成的集合。
  • symbol:具有唯一性的特殊值

通常数值、字符串、布尔值这三种类型,合称为 原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。

对象则称为 合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于 undefinednull,一般将它们看成两个特殊值。

对象是最复杂的数据类型,又可以分成三个子类型。

  • 狭义的对象(object)
  • 数组(array)
  • 函数(function)

狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本文的”对象“都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。

检测类型

JavaScript中有三种方法,可以确定一个值到底是什么类型。

  • typeof
  • instanceof
  • Object.prototype.toString

typeof

根据 MDN 定义

typeof操作符返回一个字符串,表示未经计算的操作数的类型。

未经计算的操作数又是什么?

举个例子:

typeof 37 //'number'

typeof NaN //'number'

typeof "bla" //'string'

typeof false //boolean

typeof Symbol('foo') //'symbol';

undefined 返回 undefined 。

typeof undefind 
//"undefined"

对象和数组还会返回objet

typeof window // "object"

typeof {} // "object"

typeof [] // "object"

上面代码中,空数组([])的类型也是object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。
关于 instanceof 对象的类型区分。

var o = {};
var a = [];

o instanceof Array // false
a instanceof Array // true

所以,可以粗暴理解为未经计算的操作数是他的基本数据类型,但还是还有几个例外。

比如:

// 函数
typeof function(){} === 'function';
typeof class C{} === 'function'
typeof Math.sin === 'function';
typeof new Function() === 'function';

funtion 并属于基本类型啊,为什么会返回function,大家都知道 function 应该属于 object 中的特殊类型。

在 JavaScript 之中,就将构造函数的未经计算的操作数设置为function了,所以这里比较特殊。

还有一个特殊的点,就是null

typeof null // "object"

null的类型是object,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了。

instanceof

MDN 是这么说的

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

也就是 instanceof 这个方法会遍历原型链,去寻找是否具有该原型

举个例子

function chris(){} 
function james(){} 

var o = new chris();

o instanceof chris; // true,因为 Object.getPrototypeOf(o) === chris.prototype

o instanceof james; // false,因为 james.prototype不在o的原型链上

o instanceof Object // true 原型链上也有Object

需要注意的是,如果表达式 obj instanceof Foo 返回true,则并不意味着该表达式会永远返回true,因为Foo.prototype属性的值有可能会改变,改变之后的值很有可能不存在于obj的原型链上,这时原表达式的值就会成为false

举个例子

function chris(){} 
function james(){} 

var o = new chris();

o instanceof chris //true

chris.prototype = {};

o instanceof chris; // false,C.prototype指向了一个空对象,这个空对象不在o的原型链上.

toString

toString() 用来返回一个表示该对象的字符串,现在大多情况被用来类型检测,toString() 返回 "[object *type*]" ,其中type是对象的类型

举个例子:

var toString = Object.prototype.toString;

toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(2); // [object Number]
toString.call([]); // [object Array]

toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]

我们发现 type 的类型既能返回包装类型,也能返回基本类型,是一个比较全面的类型检测方法。

null和undefined的区别

nullundefined 都可以表示“没有”,含义非常相似。将一个变量赋值为 undefinednull,老实说,语法效果几乎没区别。

var a = undefined;
// 或者
var a = null;

上面代码中,变量a分别被赋值为 undefinednull,这两种写法的效果几乎等价。
if 语句中,它们都会被自动转为 false,相等运算符(==)甚至直接报告两者相等

if (!undefined) {
console.log('undefined is false');
}
// undefined is false

if (!null) {
  console.log('null is false');
}
// null is false

undefined == null
// true

上面代码可见,两者的行为是何等相似!像谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有 null,没有 undefined

既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。

1995年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 表示”无”。根据 C 语言的传统,null 可以自动转为0。

Number(null) // 0
5 + null // 5

上面代码中,null 转为数字时,自动变成0。

但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null 就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。

因此,他又设计了一个 undefined

Number下的区别:null 是一个表示“空”的对象,转为数值时为0;undefined是一个表示”此处无定义”的原始值,转为数值时为NaN。

Number(undefined) // NaN
5 + undefined // NaN

对于null和undefined,还可以这样理解。

null表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入 null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null,表示未发生错误。

var i
function f(i = null){
	return i
}
f(i)//null

undefined表示“未定义”,下面是返回 undefined 的典型场景。

// 变量声明了,但没有赋值
var i;
i // undefined

// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
  return x;
}
f() // undefined

// 对象没有赋值的属性
var  o = new Object();
o.p // undefined

// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined

数据结构专题之栈与队列(二)

前言

在数组中,我们可以通过索引访问随机元素。 但是,在某些情况下,我们可能想要限制处理顺序。

队列分别对应两种处理顺序,先入先出后入先出,对数组操作进行了限制。适用于头部和尾部添加和删除的操作。

栈的核心是 后入先出(FILO):

function Stack() {
  this.items = []
}

Stack.prototype.push = function(item) {
  this.items.push(item)
}

Stack.prototype.pop = function() {
  return this.items.pop()
}

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

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

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

题目: 平衡圆括号

测试用例:
{{([][])}()} => true
[{()] => false


function isBalance(symbol) {
  const stack = new Stack()
  const left = '{[('
  const right = '}])'
  let popValue
  let tag = true

  const match = function(popValue, current) {
    if (left.indexOf(popValue) !== right.indexOf(current)) {
      tag = false
    }
  }

  for (let i = 0; i < symbol.length; i++) {
    if (left.includes(symbol[i])) {
      stack.push(symbol[i])
    } else if (right.includes(symbol[i])) {
      popValue = stack.pop()
      match(popValue, symbol[i])
    }
  }
  return tag
}

深度优先搜索

/**
 * 利用栈先入后出的特性
 * @param {*} node 
 */
//递归
let DFS= (node, nodeList = []) => {
    if (node !== null) {
      nodeList.push(node)
      //获取父节点,拿到子节点
      let children = node.children
      for (let i = 0; i < children.length; i++) {
        //继续遍历
        DFS(children[i], nodeList)
      }
    }
    return nodeList
  }
//非递归
const DFS = (node) => {
    let stack = []
    let nodes = []
    if (node) {
      // 推入当前处理的node
      stack.push(node)
      while (stack.length) {
        let item = stack.pop()
        let children = item.children
        nodes.push(item)
        for (let i = children.length - 1; i >= 0; i--) {
          stack.push(children[i])
        }
      }
    }
    return nodes
  }

队列

队列的核心是 先入先出(FIFO)

function Queue() {
  this.items = []
}

Queue.prototype.push = function(item) {
  this.items.push(item)
}

Queue.prototype.shift = function() {
  return this.items.shift()
}

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

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

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

接着看下以下两种特殊队列:

最小优先队列

最小优先队列在生活中的例子: 比如普通用户上医院需要排队挂号, 但是具有 VIP 的用户能'插队'办理业务。用代码实现如下:

// 继承 Queue 类
const PriorityQueue = function () {
  Queue.apply(this)
}

PriorityQueue.prototype = Object.create(Queue.prototype)

PriorityQueue.prototype.constructor = PriorityQueue

// 修改 push 方法
PriorityQueue.prototype.push = function(item, level) {
  if (this.isEmpty()) {
    this.items.push({ item, level })
  } else {
    let add = true
    for (let i = 0; i < this.size(); i++) {
      if (level < this.items[i].level) {
        add = false
        this.items.splice(i, 0, { item, level })
        return
      }
    }
    add && this.items.push({ item, level })
  }
}

PriorityQueue.prototype.print = function() {
  for (let obj of this.items) {
    console.log(obj.item)
  }
}
// 调用
const queue = new PriorityQueue()
queue.push('张三', 2)
queue.push('李四', 1)
queue.push('赵五', 1)
queue.print() // 李四 赵五 张三

可以看到具有相同权限的李四和赵五依旧遵守队列的先进先出原则, 同时排在了张三的前面。

循环队列

循环队列以击鼓传花为例, 代码实现如下:

const drumGame = function(names, number) {
  const queue = new Queue()
  for (let i = 0; i < names.length; i++) {
    queue.push(names[i])
  }

  while (queue.size() > 1) {
    for (let i = 0; i < number; i++) {
      queue.push(queue.shift())  // 这句是循环队列的核心
    }
    const loser = queue.shift()
    console.log(loser + ' 出局')
  }
  return queue.shift()           // 留下的最后一个就是胜利者
}

const names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl']
const winner = drumGame(names, 7) // 假设每轮传花 7 次
console.log('胜利者是: ' + winner)

// Camila 出局
// Jack 出局
// Carl 出局
// Ingrid 出局
// 胜利者是: John

广度优先搜索(BFS)

/**
 * 利用队列先入先出的特性。
 * @param {*} node 
 */
const BFS = (node) => {
    // 记录节点
    let nodes = []
    // 执行队列
    let queue = []
    if (node) {
      queue.push(node)
      while (stack.length) {
        // 首部先出
        let item = queue.shift()
        // 获得子节点
        let children = item.children
        // 父节点任务完成
        nodes.push(item)
        //把子节点压入执行栈中,重复操作,知道执行栈执行结束
        for (let i = 0; i < children.length; i++) {
          queue.push(children[i])
        }
      }
    }
    return nodes
  }

设计模式专题之状态模式(十)

状态模式(State Pattern)

定义: 在状态模式中,类的行为是基于它的状态改变的。

  • 将事物内部的每个状态分别封装成类,
  • 内部状态改变会产生不同行为

优缺点:

优点: 用对象代替字符串记录当前状态, 状态易维护

缺点: 需编写大量状态类对象

实例

电灯按一下按钮打开弱光, 按两下按钮打开强光, 按三下按钮关闭灯光。

// 将状态封装成不同类
const weakLight = function(light) {
  this.light = light
}

weakLight.prototype.press = function() {
  console.log('打开强光')
  this.light.setState(this.light.strongLight)
}

const strongLight = function(light) {
  this.light = light
}

strongLight.prototype.press = function() {
  console.log('关灯')
  this.light.setState(this.light.offLight)
}

const offLight = function(light) {
  this.light = light
}

offLight.prototype.press = function() {
  console.log('打开弱光')
  this.light.setState(this.light.weakLight)
}

const Light = function() {
  this.weakLight = new weakLight(this)
  this.strongLight = new strongLight(this)
  this.offLight = new offLight(this)
  this.currentState = this.offLight          // 初始状态
}

Light.prototype.init = function() {
  const btn = document.createElement('button')
  btn.innerHTML = '按钮'
  document.body.append(btn)
  const self = this
  btn.addEventListener('click', function() {
    self.currentState.press()
  })
}

Light.prototype.setState = function(state) { // 改变内部状态
  this.currentState = state
}

const light = new Light()
light.init()

// 打开弱光
// 打开强光
// 关灯

非面向对象实现的状态模式

借助于 JavaScript 的委托机制, 可以像如下实现状态模式:

const obj = {
  'weakLight': {
    press: function() {
      console.log('打开强光')
      this.currentState = obj.strongLight
    }
  },
  'strongLight': {
    press: function() {
      console.log('关灯')
      this.currentState = obj.offLight
    }
  },
  'offLight': {
    press: function() {
      console.log('打开弱光')
      this.currentState = obj.weakLight
    }
  },
}

const Light = function() {
  this.currentState = obj.offLight
}

Light.prototype.init = function() {
  const btn = document.createElement('button')
  btn.innerHTML = '按钮'
  document.body.append(btn)
  const self = this
  btn.addEventListener('click', function() {
    self.currentState.press.call(self) // 通过 call 完成委托
  })
}

const light = new Light()
light.init()

JavaScript基础专题之手动实现call、apply、bind(六)

实现自己的call

MDN 定义:

call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

简答的概括就是:

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

举个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

简单的解析一下call都做了什么:

第一步:call 改变了 this 的指向,指向到 foo

第二步:bar 函数执行

函数通过 call 调用后,结构就如下面代码:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

这样this 就指向了 foo,但是我们给foo添加了一个属性,这并不可取。所以我们还要执行一步删除的动作。

所以我们模拟的步骤可以分为:

第一步:将函数设为传入对象的属性

第二步:执行该函数

第三部:删除该函数

以上个例子为例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

注意:fn 是对象的临时属性,因为执行过后要删除滴。

根据这个思路,我们可以尝试着去写一个call

Function.prototype._call = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar._call(foo); // 1

OK,我们可以在控制台看到结果了,和预想的一样。

这样只是将第一个参数作为上下文进行执行,但是并没用传入参数,下面我们尝试传入参数执行。

举个例子:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call(foo, 'chris', 10);
// chris
// 10
// 1

我们会发现参数并不固定,所以要在 Arguments 对象的第二个参数截取,传入到数组中。

比如这样:

// 以上个例子为例,此时的arguments为:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 因为arguments是类数组对象,所以可以用for循环
var args = [];
vae len = arguments.length
for(var i = 1,  i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]

OK,看到这样操作第一反应会想到 ES6 的方法,不过 call 是 ES3 的方法,所以就麻烦一点吧。所以我们这次用 eval 方法拼成一个函数,类似于这样:

eval('context.fn(' + args +')')

这里 args 会自动调用 Array.toString() 这个方法。

代码如下:

Function.prototype._call = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar._call(foo, 'chris', 10); 
// chris
// 10
// 1

OK,这样我们实现了 80% call的功能。

再看看定义:

根据 MDN 对 call 语法的定义:

第一个参数:

fun 函数运行时指定的 this 值*。*需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数在非严格模式下运行,则指定为 nullundefinedthis 值会自动指向全局对象(浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。

执行参数:

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

所以我们还需要注意两个点

1.this 参数可以传 null,当为 null 的时候,视为指向 window

举个例子:

var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1

虽然这个例子本身不使用 call,结果依然一样。

2.函数是可以有返回值

举个例子:

var obj = {
    value: 1
}

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(obj, 'chris', 10)
// Object {
//    value: 1,
//    name: 'chris',
//    age: 10
// }

不过都很好解决,让我们直接看第三版也就是最后一版的代码:

Function.prototype._call = function (context = window) {
    var context = context;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar._call(null); // 2

console.log(bar._call(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

这样我们就成功的完成了一个call函数。

实现自己的apply

apply 的实现跟 call 类似,只是后面传的参数是一个数组或者类数组对象。

Function.prototype.apply = function (context = window, arr) {
    var context = context;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

实现自己的bind

根据 MDN 定义:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

由此我们可以首先得出 bind 函数的三个特点:

  1. 改变this指向
  2. 返回一个函数
  3. 可以传入参数
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

var bindFoo = bar.bind(foo); // 返回了一个函数

bindFoo(); // 1

关于指定 this 的指向,我们可以使用 call 或者 apply 实现。

Function.prototype._bind = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}

之所以是 return self.apply(context) ,是考虑到绑定函数可能是有返回值的,依然是这个例子:

var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind(foo);

console.log(bindFoo()); // 1

第三点,可以传入参数。这个很困惑是在 bind 时传参还是在 bind 之后传参。

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFoo = bar.bind(foo, 'chris');
bindFoo('18');
// 1
// chris
// 18

通过实例,我们发现两者参数是可以累加的,就是第一次 bind 时传的参数和可以在调用的时候传入。

所以我们还是用 arguments 进行处理:

Function.prototype._bind = function (context) {
    var self = this;
    // 获取_bind函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}

完成了上面三步,其实我们还有一个问题没有解决。

根据 MDN 定义:

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'james';

var bindFoo = bar.bind(foo, 'chris');

var obj = new bindFoo('18');
// undefined
// chris
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// james

尽管在全局和 foo 中都声明了 value 值,还是返回了 undefind,说明this已经失效了,如果大家了解 new 的实现,就会知道this是指向 obj 的。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以需要一个空函数来进行中转:

Function.prototype._bind = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

还存在一些问题:

1.调用 bind 的不是函数咋办?

做一个类型判断呗

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

2.我要在线上用

做一下兼容性测试

Function.prototype.bind = Function.prototype.bind || function () {
    ……
};

好了,这样就我们就完成了一个 bind

Function.prototype._bind = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

补充

eval根据 MDN 定义:表示JavaScript表达式,语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。

一个简单的例子:

var x = 2;
var y = 39;
function add(x,y){
	return x + y
}
eval('add('+ ['x','y'] + ')')//等于add(x,y)

也就说eavl调用函数后,字符串会被解析出变量,达到去掉字符串调用变量的目的。

设计模式专题之模板方法模式(九)

模板方法模式(Template Pattern)

定义: 在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

在 JS 中应用

  • 父类中提前定义好执行的方法,称为模板
  • 根据每个实例的行为不同,进行方法继承

泡茶和泡咖啡

来对比下泡茶和泡咖啡过程中的异同

步骤 泡茶 泡咖啡
1 烧开水 烧开水
2 浸泡茶叶 冲泡咖啡
3 倒入杯子 倒入杯子
4 加柠檬 加糖

可以清晰地看出仅仅在步骤 2 和 4 上有细微的差别, 下面着手实现:

const Drinks = function() {}

Drinks.prototype.firstStep = function() {
  console.log('烧开水')
}

Drinks.prototype.secondStep = function() {}

Drinks.prototype.thirdStep = function() {
  console.log('倒入杯子')
}

Drinks.prototype.fourthStep = function() {}

Drinks.prototype.init = function() { // 模板方法模式核心: 在父类上定义好执行算法
  this.firstStep()
  this.secondStep()
  this.thirdStep()
  this.fourthStep()
}

const Tea = function() {}

Tea.prototype = new Drinks

Tea.prototype.secondStep = function() {
  console.log('浸泡茶叶')
}

Tea.prototype.fourthStep = function() {
  console.log('加柠檬')
}

const Coffee = function() {}

Coffee.prototype = new Drinks

Coffee.prototype.secondStep = function() {
  console.log('冲泡咖啡')
}

Coffee.prototype.fourthStep = function() {
  console.log('加糖')
}

const tea = new Tea()
tea.init()

// 烧开水
// 浸泡茶叶
// 倒入杯子
// 加柠檬

const coffee = new Coffee()
coffee.init()

// 烧开水
// 冲泡咖啡
// 倒入杯子
// 加糖

钩子

假如客人不想加佐料(糖、柠檬)怎么办, 这时可以引人钩子来实现之, 实现逻辑如下:

Drinks.prototype.ifNeedFlavour = function() { // 加上钩子
  return true
}

Drinks.prototype.init = function() { // 模板方法模式核心: 在父类上定义好执行算法
  this.firstStep()
  this.secondStep()
  this.thirdStep()
  if (this.ifNeedFlavour()) { // 默认是 true, 也就是要加调料
    this.fourthStep()
  }
}

// ...
const Coffee = function() {}

Coffee.prototype = new Drinks()
// ...

Coffee.prototype.ifNeedFlavour = function() {
  return window.confirm('是否需要佐料吗?') // 弹框选择是否佐料
}

操作系统专题之内存管理(三)

虚拟内存

虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。

分页系统地址映射

内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。

一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。

下图的页表存放着 16 个页,这 16 个页需要用 4 个 byte 来进行索引定位。例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1),页表项最后一位表示是否存在于内存中,1 表示存在。后 12 位存储偏移量。这个页对应的页框的地址为 (110 000000000100)。

页面置换算法

在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。

页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。

页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。

1. 最佳(OPT)

所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。

是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。

举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:

7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1

开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。

2. 最近最久未使用(LRU)

虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。

为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。

3. 最近未使用(NRU)

每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:

  • R=0,M=0
  • R=0,M=1
  • R=1,M=0
  • R=1,M=1

当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。

4. 先进先出(FIFO)

选择换出的页面是最先进入的页面。

该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。

5. 第二次机会算法

FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:

当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。

6. 时钟 (Clock)

第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。

分段

虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。

分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。

段页式

程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。

分页与分段的比较

  • 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。
  • 地址空间的维度:分页是一维地址空间,分段是二维的。
  • 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
  • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

JavaScript基础专题之继承的实现及其优缺点(十)

根据《JavaScript高级程序设计》(红宝书)继续总结一下继承的方式及优缺点

1.原型链继承

function Parent () {
    this.name = 'chris';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child = new Child();

console.log(child.getName()) // chris

问题:

  1. 对象作为引用类型,存储在堆内存,属性会被所有实例共享,举个例子:
function Parent () {
    this.names = ['chris', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('james');

console.log(child1.names); // ["chris", "daisy", "james"]

var child2 = new Child();

console.log(child2.names); // ["chris", "daisy", "james"]
  1. 在创建 Child 的实例时,不能向Parent传参

2.借用构造函数(经典继承)

function Parent () {
    this.names = ['chris', 'daisy'];
}

function Child () {
    Parent.call(this);
}

var child1 = new Child();

child1.names.push('james');

console.log(child1.names); // ["chris", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["chris", "daisy"]

优点:

  1. 避免了引用类型的属性被所有实例共享

  2. 可以在 Child 中向 Parent 传参

举个例子:

function Parent (name) {
    this.name = name;
}

function Child (name) {
    Parent.call(this, name);
}

var child1 = new Child('chris');

console.log(child1.name); // chris

var child2 = new Child('daisy');

console.log(child2.name); // daisy

缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。

3.组合继承

原型链继承和经典继承双剑合璧。

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name);
    
    this.age = age;

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('chris', '18');

child1.colors.push('black');

console.log(child1.name); // chris
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('james', '20');

console.log(child2.name); // james
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

4.原型式继承

ES5 Object.create 方法 :

var person = {
    name: 'chris',
    friends: ['daisy', 'james']
}

var person1 = Object.create(person);
var person2 = Object.create(person);

person1.name = 'person1';
console.log(person2.name); // chris

person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "james", "taylor"]


注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。

模拟实现 Object.create

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

5. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

6. 寄生组合式继承

组合继承与寄生式进行结合,比较常用的继承

为了方便大家阅读,在这里重复一下组合继承的代码:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

console.log(child1)

组合继承最大的缺点是会调用两次父构造函数。

一次是设置子类型实例的原型的时候:

Child.prototype = new Parent();

另一次在创建子类型实例的时候:

var child1 = new Child('kevin', '18');

回想下 new 的模拟实现原理,我们在执行:

Parent.call(this, name);

的时候,我们再一次调用了一次 Parent 构造函数。

所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为['red', 'blue', 'green']

那么我们如何避免这一次重复调用呢?

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?

看看如何实现:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 关键的三步,只获取prototype,不再调用构造函数
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('chris', '18');

console.log(child1);

最后我们封装一下:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。-- 红宝书

7.Class继承

ES6 class 一直认为是寄生组合式继承的语法糖:

class Parent {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}

class Child extends Parent {
  constructor(name,age) {
    super(name)
    this.age = age
  }
}

let child1 = new Child('chris',18)

我们可以看看经过 Babel 编译后的样子:

"use strict";

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

var Parent =
/*#__PURE__*/
function () {
  function Parent(name) {
    this.name = name;
  }

  var _proto = Parent.prototype;

  _proto.sayName = function sayName() {
    console.log(this.name);
  };

  return Parent;
}();

var Child =
/*#__PURE__*/
function (_Parent) {
  _inheritsLoose(Child, _Parent);

  function Child(age) {
    var _this;

    _this = _Parent.call(this, name) || this;
    _this.age = age;
    return _this;
  }

  return Child;
}(Parent);

我们是不是发现 _inheritsLoose 这个函数似曾相识,没错,就是我们的寄生组合式继承。

还有需要注意的是super 这个关键字,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数,否则就会报错。

class A {}

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

super 既可以当作函数使用,也可以当作对象使用.

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

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

let b = new B();
  1. super作为函数调用时,代表父类的构造函数,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B
  2. super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
  3. 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例,这点要特别注意,super是调用的父类的方法,但是内部的this指向的确是子类实例。

设计模式专题之策略模式(二)

策略模式(Strategy Pattern)

定义: 一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法

一句话概括:

根据不同参数可以命中不同的策略

优点

  • 能减少 if/else 语句
  • 增加复用性

JavaScript 中的应用

现在有一个需求,需要按照等级 level 来发放年终奖,小明获得 A 并且月薪是 1w 元,按照等级 S 是四倍,A是三倍 B 是两倍。

if/else实现:

const calculateBonus = function(level,salary){
    if(level === 'S'){
        return salary * 4
    }else if(level === 'A'){
        return salary * 3
    }else if(level === 'B'){
        return salary * 2
    }
  }
 calculateBonus('A', 10000) // 30000

策略模式实现:

const strategy = {
  'S': function(salary) {
    return salary * 4
  },
  'A': function(salary) {
    return salary * 3
  },
  'B': function(salary) {
    return salary * 2
  }
}

const calculateBonus = function(level, salary) {
  return strategy[level](salary)
}

calculateBonus('A', 10000) // 30000

我们再把代码解耦一下,使用高阶函数去解决我们的需求。

const S = function(salary) {
  return salary * 4
}

const A = function(salary) {
  return salary * 3
}

const B = function(salary) {
  return salary * 2
}

const calculateBonus = function(func, salary) {
  return func(salary)
}

calculateBonus(A, 10000) // 30000

JavaScript基础专题之实现自己的new Object(八)

定义

根据MDN定义:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

举个例子:

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.habit = 'Games';
}

Person.prototype.strength = 60;

Person.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

var person = new Person('Kevin', '18');

效果:


从这个例子中,我们可以看到新实例 person 可以做到:

  1. 访问到 Person 构造函数里的属性
  2. 访问到 Person.prototype 中的属性

因为 new 是关键字,无法直接覆盖,所以我们写一个函数,命名为 fakeNew,来模拟 new 的效果。用的时候是这样的:

function Person () {
    ……
}

// 使用 new
var person = new Person(……);
// 使用 objectFactory
var person = fakeNew(Person, ……)

手动实现

基本实现

因为 new 的结果是一个新对象,所以在模拟实现的时候,我们也要建立一个新对象,假设这个对象叫 obj,因为 obj 会具有 Person构造函数里的属性,想想经典继承的例子,我们可以使用 Person.apply(obj, arguments)来给 obj 添加新的属性。

//1. 创建一个新对象

//2. 获取Constructor为传入的第一个参数(Constructor必须大写哦)

//3. obj.__proto__实例下为Constructor,prototype

//4. 将Constructor所有属性继承到obj上

//5. 返回这个obj

function fakeNew() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    Constructor.apply(obj, arguments);

    return obj;

};

复制以下的代码,到浏览器中,我们可以做一下测试:

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.habit = 'Games';
}

Person.prototype.strength = 60;

Person.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

function fakeNew() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    Constructor.apply(obj, arguments);
    return obj;
};

var person = fakeNew(Person, 'Kevin', '18')

效果:

返回值情况实现

上面的实现有一个问题,那就是无法返回有返回值的情况。

接下来我们再来看一种情况,假如构造函数有返回值,举个例子:

function Person (name, age) {
    this.strength = 60;
    this.age = age;

    return {
        name: name,
        habit: 'Games'
    }
}

var person = new Person('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

这个例子中,构造函数返回了一个对象,但在实例 person 中只能访问返回的对象中的属性。

需要注意的是,在这里我们是返回了一个对象,假如返回一个基本类型呢?

function Person (name, age) {
    this.strength = 60;
    this.age = age;

    return 'handsome boy';
}

var person = new Person('Kevin', '18');

console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18

我们发现这样并不能返回,返回值会被忽略。

所以我们还需要对象进行特殊处理,如果是一个对象,我们就返回这个对象,如果没有,就直接返回。

function fakeNew() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    var ret = Constructor.apply(obj, arguments);

    return typeof ret === 'object' ? ret : obj;

};

测试一下:

这样我们就实现了自己的 new ,YES ~~~

设计模式专题之装饰器模式(十三)

装饰器模式(Decorator Pattern)

定义: 允许向一个现有的对象添加新的功能,同时又不改变其结构。

JavaScript 的装饰者模式

生活中的例子: 天气冷了, 就添加衣服来保暖;天气热了, 就将外套脱下;这个例子很形象地含盖了装饰器的神韵, 随着天气的冷暖变化, 衣服可以动态的穿上脱下。

let wear = function() {
  console.log('穿上第一件衣服')
}

const _wear1 = wear

wear = function() {
  _wear1()
  console.log('穿上第二件衣服')
}

const _wear2 = wear

wear = function() {
  _wear2()
  console.log('穿上第三件衣服')
}

wear()

// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服

这种方式有以下缺点:

  • 临时变量会变得越来越多;
  • this 指向有时会出错;

利用apply实现装饰函数

Function.prototype.after = function(fn) {
  const self = this
  return function() {
    self.apply(new(self), arguments)
    return fn.apply(new(self), arguments)
  }
}

用后置代码来实验下上面穿衣服的 demo,

const wear1 = function() {
  console.log('穿上第一件衣服')
}

const wear2 = function() {
  console.log('穿上第二件衣服')
}

const wear3 = function() {
  console.log('穿上第三件衣服')
}

const wear = wear1.after(wear2).after(wear3)
wear()

// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服

利用高阶函数的**简化代码

const after = function(fn, afterFn) {
  return function() {
    fn.apply(this, arguments)
    afterFn.apply(this, arguments)
  }
}

const wear = after(after(wear1, wear2), wear3)
wear()

操作系统专题之操作系统基础(一)

操作系统基础

定义:是控制和管理计算机系统的硬件和软件资源,合理的组织计算机工作流程以及方便用户使用,是一种系统软件

管理硬件主要包括:CPU管理、内存管理、终端管理、磁盘管理、文件管理等。

操作系统作用:设置操作系统的目的就是提高计算机系统的效率,增强系统的处理能力,充分发挥系统的利用率,方便用户使用

取指执行:计算机每执行一条指令都可分为三个阶段进行。即取指令-----分析指令-----执行指令。

中断:指CPU 在收到外部中断信号后,停止原来工作,转去处理该中断事件,完毕后回到原来断点继续工作。例如:进程间切换。

中断过程:中断请求,中断响应,中断点(暂停当前任务并保存现场),中断处理例程,中断返回(恢复中断点的现场并继续原有任务)。

TCB(Trusted Computing Base):指的是计算机内保护装置的总体,包括硬件、固件、软件和负责执行安全策略的组合体。

中断分类:

1. 外中断

由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

2. 异常

由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

3.陷入

在用户程序中使用系统调用。

现代操作系统最基本的特征:

1. 并发

并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。

并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。

操作系统通过引入进程和线程,使得程序能够并发运行。

2. 共享

共享是指系统中的资源可以被多个并发进程共同使用。

有两种共享方式:互斥共享和同时共享。

互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。

3. 虚拟

虚拟技术把一个物理实体转换为多个逻辑实体。

主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。

多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。

虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

4.异步

异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。

操作系统的五大功能:

  • CPU管理 如:进程间的切换
  • 内存管理 如:内存换入换出
  • 文件管理 如:文件读写
  • 设备管理 如:外部设备的驱动
  • 提供用户接口 如:系统调用

Linux 的系统调用主要有以下:

Task Commands
进程控制 fork(); exit(); wait();
进程通信 pipe(); shmget(); mmap();
文件操作 open(); read(); write();
设备操作 ioctl(); read(); write();
信息维护 getpid(); alarm(); sleep();
安全 chmod(); umask(); chown();

并行性:两个或多个事件在同一时间同时发生。如:多CPU每个CPU执行一个进程,不互相抢占资源,

并发性:两个或多个事件在同一时间间隔发生,交替进行。如:单CPU进程间交替执行任务。

操作系统分类:批处理、分时、实时、嵌入式、网络、集群、分布式。

用户态:用户程序执行时机器所处的状态 ,权限小,只能执行特定指令。

核心态:操作系统管理程序执行时机器所处的状态,权限大,能执行特权指令。

特权指令:I/O指令、设置中断屏蔽指令、清理内存指令、设置时钟指令。核心态只向用户提供接口,使得用户态能执行特定的指令和中断等。如系统调用 printf

JavaScript基础专题之参数传递(五)

按值传递

什么是按值传递呢?
把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

举个简单的例子:

示例一:js代码按值传递
如果按引用传递,那么函数里面的num会变成类似全局变量,最后输出60

function box(num){ // 按值传递
    num+=10;
    return num;
}
var num=50;

console.log(box(num));  // 60
console.log(num);        // 50


当传递 num 到函数 box 中,相当于拷贝了一份 num,假设拷贝的这份叫 num,函数中修改的都是 num 的值,而不会影响原来的 num 值。

示例二:php代码传递一个参数:
function box(&$num){ 
    //加上&符号将num变成全局变量
    $num+=10;
    return $num;
}
$num = 50;
echo box($num);    // 60
echo $num;    // 60

在 php 中的引用传递,会改变外部的 num 值,最后 num 也会输出60。

引用传递

拷贝虽然很好理解,但是当值是一个是对象这种复杂的数据结构的时候,拷贝就会产生性能问题。

所以还有另一种传递方式叫做按引用传递。

所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该原对象的值,因为两者引用的是同一个对象。

举个例子:

function box(o){ // 按对象传递
        o.num = 10;
	return obj.num;
}
var obj = { num: 50 };

console.log(box(obj));  // 10
console.log(obj.num);    // 10

ECMAScript标准中所有函数的参数都是按值传递的,会出现引用传递,而这究竟是不是引用传递呢?

让我们再看个例子:

function box(o){ // 按对象传递
        o = 10;
	return o;
}
var obj = { num: 50 };

console.log(box(obj));  // 10
console.log(obj.num);    // 50

如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗?

这就要讲到其实还有第三种传递方式,叫共享传递

而共享传递是指,在传递对象的时候,传递对象的引用的副本。

所以修改 o.num,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递

总结

我们可以理解,参数如果是基本类型是按值传递,如果是引用类型,也就是传递的是一个对象的引用,按共享传递。

HTTP专题之网络概述(一)

互联网

网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网把网络串联起来。

通信方式

  • 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。

  • 对等(P2P):不区分客户和服务器。

时延(delay)

时延是指一个报文或分组从一个网络的一端传送到另一个端所需要的时间。它包括了发送时延,传播时延,处理时延,排队时延。

总时延 = 排队时延 + 处理时延 + 传输时延 + 传播时延

5层网络模型介绍

互联网的实现分为好几层,每层都有自己的功能,向城市里的高楼一样,每层都需要依赖下一层,对于用户接触到的,只是上面最高一层,当然,如果要了解互联网,就必须从最下层开始自下而上理解每一层的功能。

1. 应用层

构建于TCP协议之上,为应用软件提供服务,应用层也是最高的一层直接面向用户。

  • www万维网
  • FTP文件传输协议
  • DNS协议: 域名与IP的转换
  • 邮件传输
  • DCHP协议

2. 传输层

传输层向用户提供可靠的端到端(End-to-End)服务,主要有两个协议分别是TCP、 UDP协议, 大多数情况下我们使用的是TCP协议,它是一个更可靠的数据传输协议。

  • 协议对比TCP
    • 面向链接: 需要对方主机在线,并建立链接。
    • 面向字节流: 你给我一堆字节流的数据,我给你发送出去,但是每次发送多少是我说了算,每次选出一段字节发送的时候,都会带上一个序号,这个序号就是发送的这段字节中编号最小的字节的编号。
    • 可靠: 保证数据有序的到达对方主机,每发送一个数据就会期待收到对方的回复,如果在指定时间内收到了对方的回复,就确认为数据到达,如果超过一定时间没收到对方回复,就认为对方没收到,在重新发送一遍。
  • 协议对比UDP
    • 面向无链接: 发送的时候不关心对方主机在线,可以离线发送。
    • 面向报文: 一次发送一段数据。
    • 不可靠: 只负责发送出去,至于接收方有没有收到就不管了。

3. 网络层

为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。

4. 数据链路层

网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。

5. 物理层

考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。

根据信息在传输线上的传送方向,分为以下三种通信方式:

  • 单工通信:单向传输
  • 半双工通信:双向交替传输
  • 全双工通信:双向同时传输

OSI

其中表示层和会话层用途如下:

  • 表示层 :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。
  • 会话层 :建立及管理会话。

五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。

设计模式专题之代理模式(三)

代理模式(Proxy Pattern)

定义:一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。我们创建具有现有对象的对象,以便向外界提供功能接口。

情景: 小明买火车票坐车回家

  • 非代理模式: 小明 =>火车站买票 => 回家
  • 代理模式: 小明 => 网络代售买票 => 回家

其中网络代售是从火车站买来的票,作为了一个代理专卖,但都有卖票的性质

优点

  • 代理对象和本体对象具有一致的接口, 对使用者友好

代理模式的种类有很多, 在 JavaScript 中最常用的为虚拟代理缓存代理

图片预加载

下面这段代码运用代理模式来实现图片预加载, 可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离, 并且在未来如果不需要预加载, 只要改成请求本体代替请求代理对象就行。

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()

const proxyImage = (function() {
  const img = new Image()
  img.onload = function() { // http 图片加载完毕后才会执行
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      // 代理对象
      // 两者使用同一个元素的src
      myImage.setSrc('loading.jpg') // 先用loading占位
      // 实体对象
      img.src = src// 加载完成后显示原有图片
    }
  }
})()

proxyImage.setSrc('http://loaded.jpg')

乘积计算

const mult = function() {
  let a = 1
  for (let i = 0, l; l = arguments[i++];) {
    a = a * l
  }
  return a
}

const proxyMult = (function() {
  //  记忆函数,第一次计算出参数和记录下来,第二次从缓存中拿,不再进行计算
  const cache = {}
  return function() {
    const tag = Array.prototype.join.call(arguments, ',')
    if (cache[tag]) {
      return cache[tag]
    }
    cache[tag] = mult.apply(this, arguments)
    return cache[tag]
  }
})()

proxyMult(1, 2, 3, 4) // 24

事件委托

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>
<script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });
</script>

JavaScript基础专题之深入执行上下文(三)

对于ES3每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

这篇我们来聊聊这三个重要属性

变量对象

变量对象作为执行上下文的一种属性,每次创建后,根据执行环境不同上下文下的变量对象也稍有不同,我们比较熟悉的就是全局对象函数对象,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

全局上下文

我们先了解一个概念,什么叫全局对象。在 W3School 中:

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

我们可以根据代码理解

  1. 可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。
console.log(this); //window
  1. 全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);//true
  1. 我们调用的一些方法都在window下。
console.log(Math.random());
console.log(this.Math.random());

4.作为全局变量的宿主。

var a = 1;
console.log(this.a);

5.客户端 JavaScript 中,全局对象有 window 属性指向自身。

var a = 1;
console.log(window.a);//1

this.window.b = 2;
console.log(this.b);//2

我们发现全局上下文中的变量对象就是全局对象

函数上下文

在函数上下文中,不同于全局上下文比较死板,我们用活动对象(activation object, AO)来表示变量对象。

所以活动对象和变量对象其实是一个东西,只是变量对象是规范上或者说是引擎实现上不可在 JavaScript 环境中直接访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以称为activation object,只有在激活状态才会对属性进行访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments属性初始化。arguments 属性值是 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文
  2. 代码执行

进入执行上下文

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

举个例子:

function foo(a) { 
  var b = 2;
  function c() {}
  var d = function() {};
  b =3;

}

foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

例子

function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

第一段会报错:Uncaught ReferenceError: a is not defined

第二段会打印:1

这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

第一段执行 console 的时候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。

当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

但是这个例子在非严格模式下才会成立,因为严格模式并不会主动帮你创建一个变量

再看看另一个例子

console.log(foo);

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

var foo = 1;

会打印函数,而不是 undefined 。

这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

作用域

在讲解作用域链之前,先说说作用域

作用域是指程序源代码中定义变量的区域。

作用域对如何查找变量进行了规定,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

编译原理

我们都知道JavaScript是一门动态语言或是解释性语言,但事实上它是一门编译语言。

程序中一段源码在执行前虎易经理三个步骤,统称为“编译”

  1. 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元,例如:var = 2;。这段代码会分解成var、a、=、2、;。如果词法单元生成器在判断a是一个独立的分词单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就称为词法分析。

  1. 解析/语法分析(Parsing)

这个过程是将词法单元流动(数组)转汉城一个由元素所组成的代表了程序语法结构的书。
这个书称为“抽象语法树(AST)”,var a = 2;的抽象语法树,可能会有一个叫做VariableDeclearation的顶级节点,接下来是一个叫作Identifier(它的值是 a)的子节点,以及一个叫作AssignmentExpresstion的子节点,AssignmentExpresstion节点有一个叫作NumericLiteral(它的值是2)的子节点。

  1. 代码生产

将AST转换为可执行代码的过程为代码生成

简单来说,就是有某种方法将var a = 2; 的AST转换为一组机器指令,用来创建一个叫作a的变量(包括分配内存),并将一个值储存在a中。

赋值操作

JavaScript在引擎中,变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会给它赋值

在编译器中的过程

先引入两个名词

RHS:负责查找某个变量的值

LHS:找到变量的容器本身,从而对其赋值

现在我们以console.log(a)为例,其中对a的引用进行是一个RHS引用,因为这里a并没有赋予任何值。响应地,需要查找并取得a的值,这样值就传递给console.log()。

相比之下,例如:

a = 2;

这里对a的引用则是LHS的引用,因为实际上我们并不关心当前的值是什么,只是想为= 2这个值操作找个一个目标或是容器

一个例子:

function foo(a){
  console.log(a + b)
}
var b = 2
foo(2)

首先会对b进行RHS查询,无法在函数内部获得值,就会在上一级作用域查找,找到b之后再进行RHS查询。就是说,如果该变量如果在该作用域没有找到对应的赋值,就会向上查找,直到找到对应的赋值。

静态作用域与动态作用域

我们大多使用的作用域是词法作用域, 而函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

让我们认真看个例子就能明白之间的区别:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

动态作用域

bash 就是动态作用域
例如:

value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar

作用域链

说完了作用域,终于到作用域链了。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

函数创建

函数的作用域在函数定义的时候就决定了。

这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是需要注意:[[scope]] 并不代表完整的作用域链

举个例子:

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

这时候执行上下文的作用域链,我们命名为 Scope

Scope = [AO].concat([[Scope]]);

这样我们就创建了一个作用域链。

重新思考

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

  1. checkscope 函数被创建,保存作用域链到内部属性[[scope]]
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
];
  1. checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    }
    Scope: checkscope.[[scope]],
}
  1. 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];

this

好吧,现在在说说this的问题,总结性的东西,面试题都会刷到,我就不多说了,下面我讲讲面试不考的知识,说说this到底是什么

先看一段代码

function foo() {
  var a = 2;
  this.bar();
}
function bar() {
  console.log( this.a );
}
foo(); 

聪明的同学肯定会发现会发现结果是undefined,在严格模式下会报错,首先,这段代码试图通过 this.bar() 来引用 bar() 函数。但是调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。
此外,我们发现我们试图通过内部调用函数来改变词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的。this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动对象。这个对象会包含函数在哪里被调用、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。也就是说this在函数创建的时候,已经形成了。

这样执行上下文的三个属性就讲完了,大概过程如图所示:

回顾

上面我们把三大属性就讲解了一遍,下面说说以前做过的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

两段代码都会打印'local scope'。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

具体执行分析

我们分析第一段代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

执行过程如下:

  1. 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
    ECStack = [
        globalContext
    ];
  1. 全局上下文初始化
    globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
  1. 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
    checkscope.[[scope]] = [
      globalContext.VO
    ];
  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
    ECStack = [
        checkscopeContext,
        globalContext
    ];
  1. checkscope 函数执行上下文初始化:
  • 复制函数 [[scope]] 属性创建作用域链,
  • 用 arguments 创建活动对象,
  • 初始化活动对象,即加入形参、函数声明、变量声明,
  • 将活动对象压入 checkscope 作用域链顶端。

同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. 执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];
  1. f 函数执行上下文初始化, 以下跟第 4 步相同:
  • 复制函数 [[scope]] 属性创建作用域链
  • 用 arguments 创建活动对象
  • 初始化活动对象,即加入形参、函数声明、变量声明
  • 将活动对象压入 f 作用域链顶端
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
  1. f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

  2. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

    ECStack = [
        checkscopeContext,
        globalContext
    ];
  1. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
    ECStack = [
        globalContext
    ];

ES5标准

ES5中在 我们改进了命名方式

  • 词法环境(lexical environment)
  • 变量环境(variable environment)
  • this (this value)

所以执行上下文在概念上表示如下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

词法环境

官方的 ES5 文档把词法环境定义为

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

环境记录器是存储变量和函数声明的实际位置。
外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:

全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型:

声明式环境记录器存储变量、函数和参数。
对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

简而言之,

在全局环境中,环境记录器是对象环境记录器。
在函数环境中,环境记录器是声明式环境记录器。

对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。
抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Global or outer function environment reference>
  }
}

变量环境

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
我们看点样例代码来理解上面的概念:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);
执行上下文看起来像这样:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

只有遇到调用函数 multiply 时,函数执行上下文才会被创建。
可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。
这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段
这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。
注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。

总结

本篇文章对执行上下文进行了深入的讨论,也对不同的标准进行了大致的分析,意义在于略懂一些底层知识。说了那么多也写不好代码,知道个大概就好了。

设计模式专题之观察者模式(五)

观察者模式(Observer Pattern)

定义:当对象间存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知它的依赖对象。

相比与发布-订阅模式,不再需要消息中心进行转发,而是直接通知依赖对象。

应用

  1. 当观察的数据对象发生变化时, 自动调用相应函数。比如 vue 的双向绑定;
  2. 每当调用对象里的某个方法时, 就会调用相应'访问'逻辑。比如给测试框架赋能的 spy 函数;

双向绑定

Object.defineProperty

使用 Object.defineProperty实现观察者模式, 示例如下(当改变 obj 中的 value 的时候, 自动调用相应相关函数):

var obj = {
  value: 0
}

Object.defineProperty(obj, 'list', {
  get() {
    return this.value
  },
  set(val) {
    console.log('值被更改了')
    this.value = val
  }
})
obj.value = 1 //  console.log('值被更改了')

Proxy

Proxy/Reflect 是 ES6 引入的新特性, 也可以使用其完成观察者模式, 示例如下(效果同上):

var obj = {
  value: 0
}

var proxy = new Proxy(obj, {
  set: function(target, key, value, receiver) { // {value: 0}  "value"  1  Proxy {value: 0}
    console.log('调用相应函数')
    Reflect.set(target, key, value, receiver)
  }
})

proxy.value = 1 // 调用相应函数

场景二

下面来实现 sinon 框架的 spy 函数:

const sinon = {
  analyze: {},
  spy: function(obj, fnName) {
    const that = this
    const oldFn = Object.getOwnPropertyDescriptor(obj, fnName).value
    Object.defineProperty(obj, fnName, {
      value: function() {
        oldFn()
        if (that.analyze[fnName]) {
          that.analyze[fnName].count = ++that.analyze[fnName].count
        } else {
          that.analyze[fnName] = {}
          that.analyze[fnName].count = 1
        }
        console.log(`${fnName} 被调用了 ${that.analyze[fnName].count} 次`)
      }
    })
  }
}

const obj = {
  someFn: function() {
    console.log('my name is someFn')
  }
}

sinon.spy(obj, 'someFn')

obj.someFn()
// my name is someFn
// someFn 被调用了 1 次
obj.someFn()
// my name is someFn
// someFn 被调用了 2 次

Proxy VS Object.defineProperty

Object.defineProperty 的存在的缺点:

  1. 监测不到数组的变化(比如 push/pop 等);
  2. 只能监测到外层对象的属性的改变, 即如果有深层嵌套的对象则需要再对之绑定 Object.defineProperty();

关于 Proxy 的优点:

  1. 可以劫持数组与更多属性的改变,
  2. defineProperty 是对属性的劫持, Proxy 是对对象的劫持;

设计模式专题之中介者模式(七)

中介者模式(Mediator Pattern)

定义:中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。

  • 减低多个对象或的通信复杂性
  • 需要建立一个类或对象作为中介者进行通信

实例

一场测试结束后, 公布结果: 告知解答出题目的人挑战成功, 否则挑战失败。

const player = function(name) {
  this.name = name
  playerMiddle.add(name)
}

player.prototype.win = function() {
  playerMiddle.win(this.name)
}

player.prototype.lose = function() {
  playerMiddle.lose(this.name)
}

const playerMiddle = (function() { // 将就用下这个 demo, 这个函数当成中介者
  const players = []
  const winArr = []
  const loseArr = []
  return {
    add: function(name) {
      players.push(name)
    },
    win: function(name) {
      winArr.push(name)
      if (winArr.length + loseArr.length === players.length) {
        this.show()
      }
    },
    lose: function(name) {
      loseArr.push(name)
      if (winArr.length + loseArr.length === players.length) {
        this.show()
      }
    },
    show: function() {
      for (let winner of winArr) {
        console.log(winner + '挑战成功;')
      }
      for (let loser of loseArr) {
        console.log(loser + '挑战失败;')
      }
    },
  }
}())

const a = new player('A 选手')
const b = new player('B 选手')
const c = new player('C 选手')

a.win()
b.win()
c.lose()

// A 选手挑战成功;
// B 选手挑战成功;
// C 选手挑战失败;

在这段代码中 A、B、C 之间没有直接发生关系, 而是通过另外的 playerMiddle 对象建立链接, 姑且将之当成是中介者模式了。

设计模式专题之发布-订阅模式(四)

发布订阅模式(Pub-Sub)

定义: 发布者的消息发送者不会将消息直接发送给订阅者的特定接收者,需要有一个中转站来转发消息。

在异步编程中帮助我们完成更松的解耦, 甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。

与其相近的还有观察者模式

场景: 小明取快递

  • 发布订阅模式: 快递小哥 => 快递放再门口超市(还有其他人的快递) => 小明取快递
  • 观察者模式: 快递小哥 => 拿着小明的快递送货上门(只对小明的快递负责) => 小明拿到快递

如图对比:

优点: 在异步编程中进行解耦

缺点: 如果过多的使用发布订阅模式, 维护会成为问题

实现一个发布订阅模式

class EventHub{
  constructor(){
    this.obj = {}
  }
  on(eventType,fn){
    if(!this.obj[eventType]){
      this.obj[eventType] = []
    }
    this.obj[eventType].push(fn)
  }
  emit(eventType,...arguments){
    let arr = this.obj[eventType]
    for (let i = 0; i < arr.length; i++) {
      arr[i].apply(arr[i], arguments)
    }
  }
}

let hub = new EventHub()

hub.on('click', (a) => { // 订阅函数
  console.log(a) // 1
})

hub.emit('click', 1)  

发布早于订阅

我们需要实现这样的逻辑:

var hub = new Event()
hub.emit('click', 1)

hub.on('click', function(a) {
  console.log(a) // 1
})

是利用记忆函数:

class EventHub {
  constructor() {
    this.obj = {}
    this.cacheList = []
  }
  on(eventType, fn) {
    if (!this.obj[eventType]) {
      this.obj[eventType] = []
    }
    this.obj[eventType].push(fn)
    // 先调用缓存队列的方法
    for (let i = 0; i < this.cacheList.length; i++) {
      this.cacheList[i]()
    }
  }
  emit(eventType, ...arguments) {
    const that = this
      cache = () => {
      let arr = that.obj[eventType]
      for (let i = 0; i < arr.length; i++) {
        arr[i].apply(arr[i], arguments)
      }
    }
    // 订阅之前将发布函数都存在缓存队列中
    this.cacheList.push(cache)
  }
}

以上代码实现思路就是把原本在 emit 里触发的函数存到 cacheList, 再转交到 on 中触发。从而实现了发布函数先于订阅函数执行。

HTTP专题之HTTP应用(三)

连接管理

1. 短连接与长连接

当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。

长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。

  • 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close
  • 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 Connection : Keep-Alive

2. 流水线

默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。

流水线是在同一条长连接上连续发出请求,而不用等待响应返回,这样可以减少延迟。

Cookie

HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。

Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。

1. 用途

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

2. 创建过程

服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

[page content]

客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

3. 分类

  • 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
  • 持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

4. 作用域

Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。

Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:

  • /docs
  • /docs/Web/
  • /docs/Web/HTTP

5. JavaScript获取

浏览器通过 document.cookie 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。

document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);

6. HttpOnly

标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 document.cookie API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

7. Secure

标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。

8. Session

除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。

Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。

使用 Session 维护用户登录状态的过程如下:

  • 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
  • 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
  • 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
  • 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。

应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。

9. 浏览器禁用 Cookie

此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。

10. Cookie 与 Session 选择

  • Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session;
  • Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
  • 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。

缓存

1. 优点

  • 缓解服务器压力;
  • 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存服务器在地理位置上也有可能比源服务器来得近,例如浏览器缓存。

2. 实现方法

  • 让代理服务器进行缓存;
  • 让客户端浏览器进行缓存。

3. 缓存头Cache-Control的含义和使用

可缓存性

  • public http经过的任何地方都可以进行缓存
  • private 只有发起请求的这个浏览器才可以进行缓存,如果设置了代理缓存,那么代理缓存是不会生效的
  • no-cache 任何一个节点都不可以缓存

到期

  • max-age= 设置缓存到多少秒过期
  • s-maxage= 会代替max-age,只有在代理服务器(nginx代理服务器)才会生效
  • max-stale= 是发起请求方主动带起的一个头,是代表即便缓存过期,但是在max-stale这个时间内还可以使用过期的缓存,而不需要愿服务器请求新的内容

重新验证

  • must-revalidate 如果max-age设置的内容过期,必须要向服务器请求重新获取数据验证内容是否过期
  • proxy-revalidate 主要用在缓存服务器,指定缓存服务器在过期后重新从原服务器获取,不能从本地获取

其它

  • no-store 本地和代理服务器都不可以存储这个缓存,永远都要从服务器拿body新的内容使用
  • no-transform 主要用于proxy服务器,告诉代理服务器不要随意改动返回的内容

缓存cache-control示例

  • 先思考两个问题?
    1. 在页面中引入静态资源文件,为什么静态资源文件改变后,再次发起请求还是之前的内容,没有变化呢?
    2. 在使用webpack等一些打包工具时,为什么要加上一串hash码?
  • cache-control.html
<html>
    <head>
        <meta charset="utf-8" />
        <title>cache-control</title>
    </head>
    <body>
        <script src="/script.js"></script>
    </body>
</html>
  • cache-control.js
const http = require('http');
const fs = require('fs');
const port = 3010;

http.createServer((request, response) => {
    console.log('request url: ', request.url);

    if (request.url === '/') {
        const html = fs.readFileSync('./example/cache/cache-control.html', 'utf-8');
    
        response.writeHead(200, {
            'Content-Type': 'text/html',
        });

        response.end(html);
    } else if (request.url === '/script.js') {
        response.writeHead(200, {
            'Content-Type': 'text/javascript',
            'Cache-Control': 'max-age=200'
        });

        response.end("console.log('script load')");
    }

}).listen(port);

console.log('server listening on port ', port);
  • 第一次运行

浏览器运行结果,没有什么问题,正常响应

img

控制台运行结果

img

  • 修改cache-control.js返回值
...
response.writeHead(200, {
    'Content-Type': 'text/javascript',
    'Cache-Control': 'max-age=200'
});

response.end("console.log('script load !!!')");
...
  • 中断上次程序,第二次运行

浏览器运营结果

第二次运行,从memory cahce读取,浏览器控制台并没有打印修改过的内容

img

控制台运营结果

指请求了/ 并没有请求 /script.js

img

以上结果浏览器并没有返回给我们服务端修改的结果,这是为什么呢?是因为我们请求的url/script.js没有变,那么浏览器就不会经过服务端的验证,会直接从客户端缓存去读,就会导致一个问题,我们的js静态资源更新之后,不会立即更新到我们的客户端,这也是前端开发中常见的一个问题,我们是希望浏览器去缓存我们的静态资源文件(js、css、img等)我们也不希望服务端内容更新了之后客户端还是请求的缓存的资源, 解决办法也就是我们在做js构建流程时,把打包完成的js文件名上根据它内容hash值加上一串hash码,这样你的js文件或者css文件等内容不变,这样生成的hash码就不会变,反映到页面上就是你的url没有变,如果你的文件内容有变化那么嵌入到页面的文件url就会发生变化,这样就可以达到一个更新缓存的目的,这也是目前前端来说比较常见的一个静态资源方案。

资源验证

如果使用cahce-control浏览器发起一个请求到缓存查找的一个过程流程图

img

验证头
  • Last-Modified 上次修改时间,配合If-Modified-Since或者If-Unmo dified-Since使用,对比上次修改时间以验证资源是否可用
  • Etag 数据签名,配合If-Match或者If-Non-Match使用,对比资源的签名判断是否使用缓存

内容协商

通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。

1. 类型

1.1 服务端驱动型

客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。

它存在以下问题:

  • 服务器很难知道客户端浏览器的全部信息;
  • 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术);
  • 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。

1.2 代理驱动型

服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。

2. Vary

Vary: Accept-Language

在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。

例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 Vary: Accept-Language 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。

内容编码

内容编码将实体主体进行压缩,从而减少传输的数据量。

常用的内容编码有:gzip、compress、deflate、identity。

浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,响应报文的 Vary 首部字段至少要包含 Content-Encoding。

范围请求

如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。

1. Range

在请求报文中添加 Range 首部字段指定请求的范围。

GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023

请求成功的话服务器返回的响应包含 206 Partial Content 状态码。

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)

2. Accept-Ranges

响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。

Accept-Ranges: bytes

3. 响应状态码

  • 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
  • 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
  • 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。

分块传输编码

Chunked Transfer Encoding,可以把数据分割成多块,让浏览器逐步显示页面。

通信数据转发

1. 代理

代理服务器接受客户端的请求,并且转发给其它服务器。

使用代理的主要目的是:

  • 缓存
  • 负载均衡
  • 网络访问控制
  • 访问日志记录

代理服务器分为正向代理和反向代理两种:

  • 用户察觉得到正向代理的存在。

img

  • 而反向代理一般位于内部网络中,用户察觉不到。

img

2. 网关

与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。

3. 隧道

使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。

六、HTTPS

HTTP 有以下安全性问题:

  • 使用明文进行通信,内容可能会被窃听;
  • 不验证通信方的身份,通信方的身份有可能遭遇伪装;
  • 无法证明报文的完整性,报文有可能遭篡改。

HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。

通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。

img

加密

1. 对称密钥加密

对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。

  • 优点:运算速度快;
  • 缺点:无法安全地将密钥传输给通信方。

img

2.非对称密钥加密

非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。

公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。

非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。

  • 优点:可以更安全地将公开密钥传输给通信发送方;
  • 缺点:运算速度慢。

img

3. HTTPS 采用的加密方式

HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)

img

认证

通过使用 证书 来对通信方进行认证。

数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。

服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。

进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。

img

完整性保护

SSL 提供报文摘要功能来进行完整性保护。

HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。

HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。

HTTPS 的缺点

  • 因为需要进行加密解密等过程,因此速度会更慢;
  • 需要支付证书授权的高额费用。

七、HTTP/2.0

HTTP/1.x 缺陷

HTTP/1.x 实现简单是以牺牲性能为代价的:

  • 客户端需要使用多个连接才能实现并发和缩短延迟;
  • 不会压缩请求和响应首部,从而导致不必要的网络流量;
  • 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。

二进制分帧层

HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。

img

在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。

  • 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。
  • 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。
  • 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

img

服务端推送

HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。

img

首部压缩

HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。

HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。

不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。

img

八、HTTP/1.1 新特性

详细内容请见上文

  • 默认是长连接
  • 支持流水线
  • 支持同时打开多个 TCP 连接
  • 支持虚拟主机
  • 新增状态码 100
  • 支持分块传输编码
  • 新增缓存处理指令 max-age

九、GET 和 POST 比较

作用

GET 用于获取资源,而 POST 用于传输实体主体。

参数

GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。

因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 中文 会转换为 %E4%B8%AD%E6%96%87,而空格会转换为 %20。POST 参数支持标准字符集。

GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2

安全

安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。

GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。

安全的方法除了 GET 之外还有:HEAD、OPTIONS。

不安全的方法除了 POST 之外还有 PUT、DELETE。

幂等性

幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。

所有的安全方法也都是幂等的。

在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。

GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:

GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1

POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:

POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row

DELETE /idX/delete HTTP/1.1 是幂等的,即使不同的请求接收到的状态码不一样:

DELETE /idX/delete HTTP/1.1   -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1   -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1   -> Returns 404

可缓存

如果要对响应进行缓存,需要满足以下条件:

  • 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
  • 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
  • 响应报文的 Cache-Control 首部字段没有指定不进行缓存。

XMLHttpRequest

为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:

XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。

  • 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
  • 而 GET 方法 Header 和 Data 会一起发送。

JavaScript基础专题之类数组对象(七)

类数组对象

简单概括:

拥有一个 length 属性和若干索引属性的对象

举个例子:

//数组
var array = ['name', 'age', 'sex'];

//类数组
var arrayLike = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 3
}
Aarry.isAarry(arrLike) //false

两个对象都是可读写可获取长度遍历

读取

console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';

长度

console.log(array.length); // 3
console.log(arrayLike.length); // 3

遍历

for(var i = 0, len = array.length; i < len; i++) {
   ……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    ……
}

但是他并不是真实的数组,所以并不能用数组原型上的方法。

比如:

arrayLike.push('4');// arrayLike.push is not a function

所以知识长得像数组。

一些应用

如果我们要调用数组上的方法怎么办呢?

别忘了我们可以用 Function.call 进行继承的方式,进行调用:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }

Array.prototype.join.call(arrayLike, '&'); // name&age&sex

Array.prototype.map.call(arrayLike, function(item){
    return item.toUpperCase();
}); 
// ["NAME", "AGE", "SEX"]

类数组转数组

常用的奇技淫巧:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

Arguments对象

接下来说说 Arguments 对象。

Arguments 对象只定义在函数体中,包括了函数的参数和一些自身的属性。

举个例子:

function foo(name, age, sex) {
    console.log(arguments);
}

foo('name', 'age', 'sex')

打印结果如下:

arguments

我们可以看到除了类数组的索引属性和length属性之外,还有一个 callee 属性,接下看看这些属性的定义。

length属性

length属性表示实参的长度,举个例子:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1

callee属性

callee 它可以调用函数自身。

讲个闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2

接下来讲讲 arguments 对象的几个注意要点:

arguments 和对应参数的关系

function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

所以我们会发现形参会对应argument的索引,其实在传入形参过程中进行了一步我们隐式操作。

举个例子:

function foo(name) {
	name = arguments[0]
}

以上是在非严格模式下,如果是在严格模式下,由于变量未声明的原因,实参和 arguments 是不会共享的。

继承参数

我们常用的继承:

//ES5
// 使用 apply 将 foo 的参数传递给 bar\
function bar(a, b, c) {
   console.log(a, b, c);
}

function foo() {
    bar.apply(this, arguments);//1,2,3
}

foo(1, 2, 3)

//ES6
class bar{
	constructor(a,b,c){
		this.a = a
		this.b = b
		this.c = c
	}
}

class foo extends bar{
	constructor(d){
		super(...arguments)//继承参数a,b,c
		this.d = d
	}
}

JavaScript基础专题之执行上下文和执行栈(二)

执行顺序

一般执行顺序很显然按照创建顺序执行,对大对数开发者来说并不陌生。

像是这样:

foo();  // 报错
var foo = function () {

    console.log('foo1');

}

foo();  // foo1

var foo = function () {

    console.log('foo2');

}

foo(); // foo2

然而有的时候会是这样:

foo();  // foo2

function foo() {

    console.log('foo1');

}

foo();  // foo2

function foo() {

    console.log('foo2');

}

foo(); // foo2

我们可以看到,作为函数调用的时候,会出现三个foo2。其实 JavaScript 引擎并非一行一行地分析和执行程序,在执行之前会对一些对结构进行分析执行。比如第一个例子中的变量提升,和第二个例子中的函数提升。
那么JS又是如何进行结构化的解析的呢?执行过程中又做了什么动作呢?

栈又是什么?

图片来自MDN

这张图分别展示了栈、堆和队列,其中栈就是我们所说的执行上下文栈;堆是用于存储复杂类型比如:对象,数组等等。队列就是异步队列,用于事件循环(event loop)的执行。
JS代码在引擎中是以代码片段的方式来分析执行的,而并非一行一行来分析执行。

简单的例子

//入栈过程
Stack.push("chris")

Stack.push("james")

Stack.push("kobe")

//出栈过程

Stack.pop() //["chirs","james"]

Strack.pop() //["chirs"]

Stack.pop() //[]

可以看出,栈是执行过程是一个先入后出的过程。

可执行代码

而这些代码片段的可执行代码无非为三种:全局代码(Global code)函数代码(Function Code)eval代码(Eval code)
这些可执行代码在执行的时候又会创建一个一个的执行上下文(Execution context)。例如,当执行到一个
函数的时候,JS引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文
那么随着我们的执行上下文数量的增加,JS引擎又如何去管理这些执行上下文呢?这时便有了执行上下文栈

执行上下文栈

问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?

所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

Stack= [];

试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执

行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,

Stack才会被清空,所以程序结束之前, Stack最底部永远有个 globalContext

Stack = [
    globalContext// 一开始只有全局上下文
];

一个简单的例子:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

每当一个函数执行,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函

数的执行上下文从栈中弹出。

// fun1()
Stack .push(<fun1> functionContext); 
//[globalContext,<fun1> functionContext]

// fun1中有fun2,还要创建fun2的执行上下文
Stack .push(<fun2> functionCofuntext);
//[globalContext,<fun1> functionContext,<fun2> functionCofuntext],

// fun2还调用了fun3
Stack .push(<fun3> functionContext);
//[globalContext,<fun1> functionContext,<fun2> functionCofuntext,<fun3> functionContext]      

// fun3执行完毕 
Stack .pop();
//[globalContext,<fun1> functionContext,<fun2> functionCofuntext]

// fun2执行完毕
Stack .pop();
//[globalContext,<fun1> functionContext]

// fun1执行完毕
Stack .pop();
//[globalContext]

// javascript接着执行下面的代码,但是Stack 底层永远有个globalContext

复杂的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

答案就是执行上下文栈的变化不一样。

先模拟第一段代码:

Stack.push(<checkscope> functionContext);
Stack.push(<f> functionContext);
Stack.pop();
Stack.pop();

再模拟第二段代码:

Stack.push(<checkscope> functionContext);
Stack.pop();
Stack.push(<f> functionContext);
Stack.pop();

我们可以发现二三部分出栈和入栈是不同的。

总结

  1. JS代码在引擎中是以一段一段的方式来分析执行的,而并非一行一行来分析执行
  2. 可执行代码分为三种:全局代码(Global code)函数代码(Function Code)eval代码(Eval code),其中全局代码函数代码比较常见,关于eval代码可参考JavaScript 为什么不推荐使用 eval?
  3. 每遇到函数执行的时候,就会创建一个执行上下文执行上下文会进入执行上下文栈
  4. 程序开始最先入栈和程序结束最后出栈的都是全局执行上下文

设计模式专题之责任链模式(十四)

责任链模式(Chain of Responsibility Pattern)

定义: 责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推 , 直到返回结果为止 。

  • 创建请求条件接收者链
  • 一个接收者内包含着另一个接收者
  • 连续调用接收者,直到结果返回

责任链模式是 JS 常用的设计模式

场景:

测试A提bug给前端组,前端A说不是他的责任,去询问前端B,前端B说也不是他的责任,前端B去询问前端C,前端C说我得锅,解决BUG。

实例

某电商针对已付过定金的用户有优惠政策, 在正式购买后, 已经支付过 500 元定金的用户会收到 100 元的优惠券, 200 元定金的用户可以收到 50 元优惠券, 没有支付过定金的用户只能正常购买。

// orderType: 表示订单类型, 1: 500 元定金用户;2: 200 元定金用户;3: 普通购买用户
// pay: 表示用户是否已经支付定金, true: 已支付;false: 未支付
// stock: 表示当前用于普通购买的手机库存数量, 已支付过定金的用户不受此限制

const order = function( orderType, pay, stock ) {
  if ( orderType === 1 ) {
    if ( pay === true ) {
      console.log('500 元定金预购, 得到 100 元优惠券')
    } else {
      if (stock > 0) {
        console.log('普通购买, 无优惠券')
      } else {
        console.log('库存不够, 无法购买')
      }
    }
  } else if ( orderType === 2 ) {
    if ( pay === true ) {
      console.log('200 元定金预购, 得到 50 元优惠券')
    } else {
      if (stock > 0) {
        console.log('普通购买, 无优惠券')
      } else {
        console.log('库存不够, 无法购买')
      }
    }
  } else if ( orderType === 3 ) {
    if (stock > 0) {
        console.log('普通购买, 无优惠券')
    } else {
      console.log('库存不够, 无法购买')
    }
  }
}

order( 3, true, 500 ) // 普通购买, 无优惠券

下面用职责链模式改造代码:

const order500 = function(orderType, pay, stock) {
  if ( orderType === 1 && pay === true ) {
    console.log('500 元定金预购, 得到 100 元优惠券')
  } else {
    order200(orderType, pay, stock)
  }
}

const order200 = function(orderType, pay, stock) {
  if ( orderType === 2 && pay === true ) {
    console.log('200 元定金预购, 得到 50 元优惠券')
  } else {
    orderCommon(orderType, pay, stock)
  }
}

const orderCommon = function(orderType, pay, stock) {
  if (orderType === 3 && stock > 0) {
    console.log('普通购买, 无优惠券')
  } else {
    console.log('库存不够, 无法购买')
  }
}

order500( 3, true, 500 ) // 普通购买, 无优惠券

改造后可以发现代码相对清晰了, 但是链式调用和业务代码依然耦合在一起, 进一步优化:

// 业务代码
const order500 = function(orderType, pay, stock) {
  if ( orderType === 1 && pay === true ) {
    console.log('500 元定金预购, 得到 100 元优惠券')
  } else {
    return 'nextSuccess'
  }
}

const order200 = function(orderType, pay, stock) {
  if ( orderType === 2 && pay === true ) {
    console.log('200 元定金预购, 得到 50 元优惠券')
  } else {
    return 'nextSuccess'
  }
}

const orderCommon = function(orderType, pay, stock) {
  if (orderType === 3 && stock > 0) {
    console.log('普通购买, 无优惠券')
  } else {
    console.log('库存不够, 无法购买')
  }
}

// 链式调用
const chain = function(fn) {
  this.fn = fn
  this.sucessor = null
}

chain.prototype.setNext = function(sucessor) {
  this.sucessor = sucessor
}

chain.prototype.init = function() {
  const result = this.fn.apply(this, arguments)
  if (result === 'nextSuccess') {
    this.sucessor.init.apply(this.sucessor, arguments)
  }
}

const order500New = new chain(order500)
const order200New = new chain(order200)
const orderCommonNew = new chain(orderCommon)

order500New.setNext(order200New)
order200New.setNext(orderCommonNew)

order500New.init( 3, true, 500 ) // 普通购买, 无优惠券

重构后, 链式调用和业务代码彻底地分离。假如未来需要新增 order300, 那只需新增与其相关的函数而不必改动原有业务代码。

另外结合 AOP 还能简化上述链路代码:

// 业务代码
const order500 = function(orderType, pay, stock) {
  if ( orderType === 1 && pay === true ) {
    console.log('500 元定金预购, 得到 100 元优惠券')
  } else {
    return 'nextSuccess'
  }
}

const order200 = function(orderType, pay, stock) {
  if ( orderType === 2 && pay === true ) {
    console.log('200 元定金预购, 得到 50 元优惠券')
  } else {
    return 'nextSuccess'
  }
}

const orderCommon = function(orderType, pay, stock) {
  if (orderType === 3 && stock > 0) {
    console.log('普通购买, 无优惠券')
  } else {
    console.log('库存不够, 无法购买')
  }
}

// 链路代码
Function.prototype.after = function(fn) {
  const self = this
  return function() {
    const result = self.apply(self, arguments)
    if (result === 'nextSuccess') {
      return fn.apply(self, arguments) // 这里 return 别忘记了~
    }
  }
}

const order = order500.after(order200).after(orderCommon)

order( 3, true, 500 ) // 普通购买, 无优惠券

职责链模式比较重要, 项目中能用到它的地方会有很多, 用上它能解耦 1 个请求对象和 n 个目标对象的关系。

操作系统专题之设备管理(四)

磁盘结构

  • 盘面(Platter):一个磁盘有多个盘面;
  • 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
  • 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
  • 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
  • 制动手臂(Actuator arm):用于在磁道之间移动磁头;
  • 主轴(Spindle):使整个盘面转动。

1567228097(1).jpg

读取过程如图:

1567228132(1).jpg

磁盘调度算法

读写一个磁盘块的时间的影响因素有:

  • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
  • 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
  • 实际的数据传输时间

其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。

1. 先来先服务

FCFS, First Come First Served

按照磁盘请求的顺序进行调度。

优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。

2. 最短寻道时间优先

SSTF, Shortest Seek Time First

优先调度与当前磁头所在磁道距离最近的磁道。

虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。

img

3. 电梯算法

SCAN

电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。

电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。

因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。

设计模式专题之组合模式(十二)

组合模式(Composite Pattern)

组合模式,又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。

  • 组合模式在对象间形成树形结构;
  • 组合模式中基本对象和组合对象被一致对待;
  • 无须关心对象有多少层, 调用时只需在根部进行调用;

实例

命令执行

我们需要按照计划执行一下任务:

1. 煮咖啡
2. 打开电视、打开音响
3. 打开空调、打开电脑

结合了命令模式组合模式的具体实现:

const MacroCommand = function() {
  return {
    lists: [],
    // 消息队列
    add: function(task) {
      this.lists.push(task)
    },
    // 执行队列
    excute: function() { // 组合对象调用这里的 excute,
      for (let i = 0; i < this.lists.length; i++) {
        this.lists[i].excute()
      }
    },
  }
}

const command1 = MacroCommand() // 基本对象

command1.add({
  excute: () => console.log('煮咖啡') // 基本对象调用这里的 excute,
})

const command2 = MacroCommand() // 组合对象

command2.add({
  excute: () => console.log('打开电视')
})

command2.add({
  excute: () => console.log('打开音响')
})

const command3 = MacroCommand()

command3.add({
  excute: () => console.log('打开空调')
})

command3.add({
  excute: () => console.log('打开电脑')
})

const macroCommand = MacroCommand()
macroCommand.add(command1)
macroCommand.add(command2)
macroCommand.add(command3)

macroCommand.excute()

// 煮咖啡

// 打开电视
// 打开音响

// 打开空调
// 打开电脑

可以看出在组合模式中基本对象和组合对象被一致对待, 所以要保证基本对象(节点对象)和组合对象具有一致方法。

扫描文件夹

扫描文件夹时, 文件夹下面可以为另一个文件夹也可以为文件, 我们希望统一对待这些文件夹和文件, 这种情形适合使用组合模式。

const Folder = function(folder) {
  this.folder = folder
  this.lists = []
}

Folder.prototype.add = function(resource) {
  this.lists.push(resource)
}

Folder.prototype.scan = function() {
  console.log('开始扫描文件夹: ', this.folder)
  for (let i = 0, folder; folder = this.lists[i++];) {
    folder.scan()
  }
}

const File = function(file) {
  this.file = file
}

File.prototype.add = function() {
  throw Error('文件下不能添加其它文件夹或文件')
}

File.prototype.scan = function() {
  console.log('开始扫描文件: ', this.file)
}

const folder = new Folder('根文件夹')
const folder1 = new Folder('JS')
const folder2 = new Folder('life')

const file1 = new File('深入React技术栈.pdf')
const file2 = new File('JavaScript权威指南.pdf')
const file3 = new File('小王子.pdf')

folder1.add(file1)
folder1.add(file2)

folder2.add(file3)

folder.add(folder1)
folder.add(folder2)

folder.scan()

// 开始扫描文件夹:  根文件夹
// 开始扫描文件夹:  JS

// 开始扫描文件:  深入React技术栈.pdf
// 开始扫描文件:  JavaScript权威指南.pdf
// 开始扫描文件夹:  life
// 开始扫描文件:  小王子.pdf

JavaScript基础专题之闭包(四)

定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

什么又是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

举个例子:

var a = 0; //自由变量

function foo() {
    console.log(a);//访问自由变量,此时这个变量并不是函数参数或者函数的局部变量
}

foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以我们说 a 就是自由变量,那么函数 foo 就形成了一个闭包。

所以在《 JavaScript权威指南 》中讲到:从技术的角度讲,所有的 JavaScript 函数都是闭包。

在ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

接下来就来讲讲实践上的闭包。

常见的闭包问题

以下代码为什么与预想的输出不符?

// 代码1
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出5次5
    }, 0)
}

假设A:因为 setTimeout 这块的任务直接进入了事件队列中,所以 i 循环之后i先变成了5,再执行 setTimeoutsetTimeout 中的箭头函数会保存对i的引用,所以会打印5个5.

// 代码2
for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出 0,1,2,3,4
    }, 0)
}

假设结论 A 成立,那么上式应该也是输出5次5,但是很明显不是,所以结论A并不完全正确。

那我们去掉循环,先写成最简单的异步代码:

function test(a){
    setTimeout(function timer(){
        console.log(a)
    },0)
}
test('hello')

复制代码执行 testsetTimeouttimer 函数放入了事件队列,timer 保留着 test 函数的作用域(在函数定义时创建的),test 执行完毕,主线程上没有其他任务了,timer 从事件队列中出队,执行 timer,执行 console.log ( a ) ,由于闭包的原因,a 依然会保留着之前的引用,输出 'hello'

那我们在回到题目中,因为两段代码中的不同只有声明语句,所以我们提出假设 B :因为在代码1中,匿名函数保留着外部词法作用域,i 都是在全局作用域上,代码2中由于存在块作用域,所以它保留着每次循环时i的引用。

// 代码3
for (var i = 0; i < 5; i++) {
    ((i) => {
        setTimeout(function timer() {
            console.log(i) // 输出 0,1,2,3,4
        }, 0)
    })(i)
}

复制代码使用 IIFE 传递了变量i给匿名函数,IIFE 产生了一个新作用域,timer 中保留对匿名函数中的i的引用,所以会依次输出。

// 代码4
for (var i = 0; i < 5; i++) {
    (() => {
        setTimeout(function timer() {
            console.log(i) // 输出 5个5
        }, 0)
    })()
}

代码3的区别为 IIFE 没有给匿名函数传递 i,timer 保留的作用域链中对i的引用还是在全局作用域上。

经过以上两个变体的验证,所以假设B 成立,即:由于作用域链的变化,闭包中保留的参数引用也发生了变化,输出的参数也发生了变化。

下例,循环中的每个迭代器在运行时都会给自己捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的五个函数分别是在各个迭代器中分别定义的,但是它们都会被封闭在一个共享的全局作用域中,实际上只有一个i,换句话说,i的值在传入内部函数之前,已经为 6 了,所以结果每次都会输出 6 。

for(var i=1; i <= 5; i++){
    setTimeout(function(){
        console.log(i);//6
    },0)
}

解决上面的问题,在每个循环迭代中都需要一个闭包作用域,下面示例,循环中的每个迭代器都会生成一个新的作用域。

for(var i=1; i <= 5; i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        })
    },0)(i)
}

也可以使用let解决,let声明,可以用来劫持块作用域,并且在这个块作用域中生明一个变量。

for(let i=1; i <= 5; i++){
    setTimeout(function(){
        console.log(i);
    },0)
}

总结

简单的说:函数 + 自由变量就形成了闭包。其实并不是特别复杂,只是我们需要在引用自由变量的时候小心作用域的变化。

数据结构专题之数组与字符串(一)

前言

数组是数据结构中的基本模块之一。因为字符串是由字符数组形成的,所以二者是相似的。大多数面试问题都属于这个范畴。

数组

数组是一种基本的数据结构,用于按顺序存储元素的集合,占用联系的储存空间,适合查找方法。根据数组下标索引来查找时间复杂度为O(1),。

数组可以有一个或多个维度。这里我们从一维数组开始,它也被称为线性数组。这里有一个例子:

img

在上面的例子中,数组 A 中有 6 个元素。也就是说,A 的长度是 6 。我们可以使用 A[0] 来表示数组中的第一个元素。因此,A[0] = 6 。类似地,A[1] = 3,A[2] = 8,依此类推。

题目:寻找中心索引

输入: 
nums = [1, 7, 3, 6, 5, 6]
输出: 3
解释: 
索引3 (nums[3] = 6) 的左侧数之和(1 + 7 + 3 = 11),与右侧数之和(5 + 6 = 11)相等。
同时, 3 也是第一个符合要求的中心索引。

/**
 * @param {number[]} nums
 * @return {number}
 */
var pivotIndex = function(nums) {
    if(!nums) return -1
    let sum = 0
    let sumLeft = 0
    let sumRight = 0
    for(let j = 0;j<nums.length;j++){
        sum = sum + nums[j]
    }
    for(let i =0;i<nums.length;i++){
        if(i == 0){
            sumLeft = 0
        }else{
            sumLeft = sumLeft + nums[i-1]
        }
        sumRight = sum - sumLeft - nums[i]
        if(sumLeft == sumRight){
            return i
        }
    }
    return -1
    
};

动态数组

正如我们在上一篇文章中提到的,数组具有固定的容量,我们需要在初始化时指定数组的大小。有时它会非常不方便并可能造成浪费。

因此,大多数编程语言都提供内置的动态数组,它仍然是一个随机存取的列表数据结构。

题目 加一:

输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function(digits) {
    var i;
    for(i=digits.length-1;i>=0;i--){
        if(digits[i]==9){
            digits[i]=0;
        }else{
            digits[i]++;
            return digits;
        }
    }
    if(i == -1){
        digits.unshift(1);
    }
    return digits;
};

二维数组

类似于一维数组,二维数组也是由元素的序列组成。但是这些元素可以排列在矩形网格中而不是直线上。

对角线遍历

输入:
[
 [ 1, 2, 3 ],
 [ 4, 5, 6 ],
 [ 7, 8, 9 ]
]

输出:  [1,2,4,7,5,3,6,8,9]
/**
 * @param {number[][]} matrix
 * @return {number[]}
 */
var findDiagonalOrder = function(matrix) {
    if (matrix.length == 0) return [];
    let col = matrix.length // 列数
    let row = matrix[0].length // 行数
    let res = []
    let r = 0
    let c= 0
    for(let i = 0;i<lens*row;i++){
        res[i] = matrix[r][c];
            if ((r + c) % 2 == 0) {
                if (c == row - 1) { // 元素在最后一列,往下走
                    r++;
                } else if (r == 0) { // 元素在第一行,往右走
                    c++;
                } else { // 其他情况,往右上走
                    r--;
                    c++;
                }
            } else {
                if (r == col - 1) { //元素在最后一行,往右走
                    c++;
                } else if (c == 0) { // //元素在第一列,往下走
                    r++;
                } else { //其他情况,往左下走
                    r++;
                    c--;
                }
            }
    }
    return res;
    
};

字符串

字符串实际上是一个 unicode 字符数组。你可以执行几乎所有我们在数组中使用的操作。

最长公共前缀

输入: ["flower","flow","flight"]
输出: "fl"
/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    var len = strs.length
    if(len===0){
        return ""
    }
    if(len===1){
        return strs[0]
    }
    var index = 0
    for(var j = 0;j < strs[0].length;j++){
        for(var i = 0;i<len;i++){
            if(j >= strs[i].length || strs[0][j] != strs[i][j]){
				return strs[0].substring(0,index);
			}
        }
        index++
    }
    return strs[0].substring(0 ,index);
};

设计模式之迭代器模式(十五)

迭代器模式(Iterator Pattern)

定义: 能访问到聚合对象的顺序与元素,不需要知道集合对象的底层表示。

实现一个内部迭代器

function each(arr, fn) {
  for (let i = 0; i < arr.length; i++) {
    fn(i, arr[i])
  }
}

each([1, 2, 3], function(i, n) {
  console.log(i) // 0 1 2
  console.log(n) // 1 2 3
})

可以看出内部迭代器在调用的时候非常简单, 使用者不用关心迭代器内部实现的细节, 但这也是内部迭代器的缺点。比如要比较两数组是否相等, 只能在其回调函数中作文章了, 代码如下:

const compare = function(arr1, arr2) {
  each(arr1, function(i, n) {
    if (arr2[i] !== n) {
      console.log('两数组不等')
      return
    }
  })
  console.log('两数组相等')
}

const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
compare(arr1, arr2) // true

实现一个外部迭代器

相较于内部迭代器, 外部迭代器将遍历的权利转移到外部, 因此在调用的时候拥有了更多的自由性, 不过缺点是调用方式较复杂。

const iterator = function(arr) {
  let current = 0
  const next = function() {
    current = current + 1
  }
  const done = function() {
    return current >= arr.length
  }
  const value = function() {
    return arr[current]
  }
  return {
    next,
    done,
    value,
  }
}

const arr1 = [1, 2 ,3]
const arr2 = [1, 2, 3]
const iterator1 = iterator(arr1)
const iterator2 = iterator(arr2)

const compare = function(iterator1, iterator2) {
  while (!iterator1.done() && !iterator2.done()) {
    if (iterator1.value() !== iterator2.value()) {
      console.log('两数组不等')
      return
    }
    iterator1.next() // 外部迭代器将遍历的权利转移到外部
    iterator2.next()
  }
  console.log('两数组相等')
}

compare(iterator1, iterator2)

这个模式在ES6中应用比较广泛,比如:与生成器 generator结合,实现异步变成。在 koa 框架中实现洋葱圈模型。 大多数场景式应用式异步操作队列。

操作系统专题之进程和线程(二)

进程

进程是资源分配的基本单位。

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。

线程

线程是独立调度的基本单位。

一个进程中可以有多个线程,它们共享进程资源。

QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。

区别

  • 拥有资源

进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

  • 调度

线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。

  • 系统开销

由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

  • 通信方面

线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

进程状态的切换

  • 就绪状态(ready):等待被调度
  • 运行状态(running)
  • 阻塞状态(waiting):等待资源

应该注意以下内容:

  • 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
  • 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。

进程调度算法

不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。

1. 批处理系统

批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

1.1 先来先服务 first-come first-serverd(FCFS)

非抢占式的调度算法,按照请求的顺序进行调度。

有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。

1.2 短作业优先 shortest job first(SJF)

非抢占式的调度算法,按估计运行时间最短的顺序进行调度。

长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

1.3 最短剩余时间优先 shortest remaining time next(SRTN)

最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

2. 交互式系统

交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。

2.1 时间片轮转

将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系:

  • 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
  • 而如果时间片过长,那么实时性就不能得到保证。

2.2 优先级调度

为每个进程分配一个优先级,按优先级进行调度。

为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

2.3 多级反馈队列

一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

3. 实时系统

实时系统要求一个请求在一个确定时间内得到响应。

分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

进程同步

1. 临界区

对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

// entry section
// critical section;
// exit section

2. 同步与互斥

  • 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

3. 信号量

信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。

如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

void P2() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer() {
    while(TRUE) {
        down(&full);
        down(&mutex);
        int item = remove_item();
        consume_item(item);
        up(&mutex);
        up(&empty);
    }
}

4. 管程

使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。

管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。

管程引入了 条件变量 以及相关的操作:wait()signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

经典同步问题

生产者和消费者问题前面已经讨论过了。

1. 读者-写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

2. 哲学家进餐问题

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。

为了防止死锁的发生,可以设置两个条件:

  • 必须同时拿起左右两根筷子;
  • 只有在两个邻居都没有进餐的情况下才允许进餐。

进程通信

进程同步与进程通信很容易混淆,它们的区别在于:

  • 进程同步:控制多个进程按一定顺序执行;
  • 进程通信:进程间传输信息。

进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。

1. 管道

管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。

#include <unistd.h>
int pipe(int fd[2]);

它具有以下限制:

  • 只支持半双工通信(单向交替传输);
  • 只能在父子进程或者兄弟进程中使用。

2. FIFO

也称为命名管道,去除了管道只能在父子进程中使用的限制。

3. 消息队列

相比于 FIFO,消息队列具有以下优点:

  • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
  • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
  • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。

4. 信号量

它是一个计数器,用于为多个进程提供对共享数据对象的访问。

5. 共享存储

允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。

需要使用信号量用来同步对共享存储的访问。

多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。

6. 套接字

与其它通信机制不同的是,它可用于不同机器间的进程通信。

死锁

必要条件

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

处理方法

主要有以下四种方法:

  • 鸵鸟策略
  • 死锁检测与死锁恢复
  • 死锁预防
  • 死锁避免

鸵鸟策略

把头埋在沙子里,假装根本没发生问题。

因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。

当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。

大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

死锁检测与死锁恢复

不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。

1. 每种类型一个资源的死锁检测

每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。

2. 每种类型多个资源的死锁检测

每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。

  1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。
  2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
  3. 如果没有这样一个进程,算法终止。

3. 死锁恢复

  • 利用抢占恢复
  • 利用回滚恢复
  • 通过杀死进程恢复

死锁预防

在程序运行之前预防发生死锁。

  • 破坏互斥条件
  • 破坏占有和等待条件
  • 破坏不可抢占条件
  • 破坏环路等待

死锁避免

在程序运行时避免发生死锁。

1. 安全状态

定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。

安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。

2. 单个资源的银行家算法

一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。

3. 多个资源的银行家算法

JavaScript基础专题之类型转换(十二)

什么是类型转换?

我们都知道 JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。

var x = y ? 1 : 'a';

上面代码中,变量x到底是数值还是字符串,取决于另一个变量y的值。y 为 true 时,x 是一个数值;y为 false 时,x是一个字符串。这意味着,x 的类型没法在编译阶段就知道,必须等到运行时才能知道。

虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。

'4' - '3' // 1

上面代码中,虽然是两个字符串相减,但是依然会得到结果数值 1,原因就在于 JavaScript 将运算子自动转为了数值。

1. 强制转换

强制转换主要指使用 NumberStringBoolean 三个函数,手动将各种类型的值,分布转换成数字、字符串或者布尔值。

1.1 Number()

使用 Number 函数,可以将任意类型的值转化成数值。

下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。

(1)原始类型值

原始类型值的转换规则如下:

// 数值:转换后还是原来的值
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

Number函数将字符串转为数值,要比 parseInt 函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。

parseInt('42 cats') // 42
Number('42 cats') // NaN

上面代码中,parseInt逐个解析字符,而Number函数整体转换字符串的类型。

另外,parseInt和Number函数都会自动过滤一个字符串前导和后缀的空格。

parseInt('\t\v\r12.34\n') // 12
Number('\t\v\r12.34\n') // 12.34

(2)对象
简单的规则是,Number 方法的参数是对象时,将返回 NaN,除非是包含单个数值的数组

Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

之所以会这样,是因为 Number 背后的转换规则比较复杂。

第一步,调用对象自身的 valueOf 方法。如果返回原始类型的值,则直接对该值使用 Number 函数,不再进行后续步骤。

第二步,如果 valueOf 方法返回的还是对象,则改为调用对象自身的 toString 方法。如果 toString 方法返回原始类型的值,则对该值使用 Number 函数,不再进行后续步骤。

第三步,如果 toString 方法返回的是对象,就报错。

举个例子。

var obj = {x: 1};
Number(obj) // NaN

// 等同于
if (typeof obj.valueOf() === 'object') {
	Number(obj.toString());
  } else {
	Number(obj.valueOf());
}

上面代码中,Number 函数将 obj 对象转为数值。背后发生了一连串的操作,首先调用 obj.valueOf 方法, 结果返回对象本身;于是,继续调用 obj.toString 方法,这时返回字符串 [object Object],对这个字符串使用 Number 函数,得到 NaN

默认情况下,对象的 valueOf 方法返回对象本身,所以一般总是会调用 toString 方法,而 toString 方法返回对象的类型字符串(比如 [object Object])。所以,会有下面的结果。

Number({}) // NaN

如果 toString 方法返回的不是原始类型的值,结果就会报错。

var obj = {
valueOf: function () {
	return {};
	},
toString: function () {
	return {};
	}
};
Number(obj)// TypeError: Cannot convert object to primitive value

上面代码的 valueOftoString 方法,返回的都是对象,所以转成数值时会报错。

从上例还可以看到, valueOftoString 方法,都是可以自定义的。

Number({
	valueOf: function () {
		return 2;
	}
})// 2

Number({
	toString: function () {
		return 3;
	}
})// 3

Number({
	valueOf: function () {
		return 2;
	},
	toString: function () {
		return 3;
	}
})// 2

上面代码对三个对象使用 Number 函数。第一个对象返回 valueOf 方法的值,第二个对象返回 toString 方法的值,第三个对象表示 valueOf 方法先于 toString 方法执行。

1.2 String()

String 函数可以将任意类型的值转化成字符串,转化规则如下:
(1)原始类型值

  • 数值:转为相应的字符串
  • 字符串:转换称还是原来的值。
  • 布尔值:true转为字符串"true",false转为字符串"false"。
  • undefied:转为字符串"undefined"
  • null:转为字符串"null"
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"

(2)对象
String 方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

String 方法背后的转换规则,与 Number 方法基本相同,只是互换了 valueOf 方法和 toString 方法的执行顺序。

1.3 Boolean()

Boolean 函数可以将任意类型的值转为布尔值。
它的转换规则相对简单:除了一下五个值的转换结果为 false ,其他的值全部为 true

  • undefined
  • null
  • 0
  • NaN
  • "" 或者''(空字符串)
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

所有对象,包括空对象的转换结果都是 true

Boolean({}) // true
Boolean([]) // true

所有对象的布尔值都是true,这是因为 JavaScript 语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于 obj1 && obj2 这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为 true

2.隐式转换

遇到以下三种情况时,JavaScript 会进行隐式转换。

第一种情况,不同类型的数据互相运算。

123+'abc'  //"123abc"

第二种情况,对非布尔值类型的数据求布尔值

if ('abc') { console.log('hello') }  // "hello"

第三种情况,对非数值类型的值使用一元运算符(即+和-)。

+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String 函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。

由于隐式转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用 BooleanNumberString 函数进行显式转换。

下面这个例子中,条件部分的每个值都相当于false,使用否定运算符后,就变成了true。

if (!undefined&& !null&& !0&& !NaN&& !'') {	console.log('true')	} // true

下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是 Boolean 函数。

// 写法一
expression ? true : false

// 写法二
!! expression

2.1 转换为字符串

JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

2.2 转换为数值

JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调 Number函数。

除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

上面代码中,运算符两侧的运算子,都被转成了数值。

注意:null转为数值时为0,而undefined转为数值时为NaN。

一元运算符也会把运算子转成数值。

+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0

2.3 布尔值

布尔值代表“真”和“假”两个状态。“真”用关键字 true 表示,“假”用关键字 false 表示。布尔值只有这两个值。

下列运算符会返回布尔值:

  • 两元逻辑运算符: && (And),|| (Or)
  • 前置逻辑运算符: ! (Not)
  • 相等运算符:===,!==,==,!=
  • 比较运算符:>,>=,<,<=

举个例子:

if ( a && b || c) {console.log('1') } // a和b同时存在或者只在c返回true
if ( !a ) { consoel.log('1')} // 不存在a的话返回true
if ( a === b) { console.log('1') } // a和比相等返回true
if ( a > b ) { console.log('1') } // a大于b返回true

如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为 false,其他值都视为 true

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""或''(空字符串)

举个例子:

if ('') { console.log('true'); }// 没有任何输出

上面代码中,if命令后面的判断条件,预期应该是一个布尔值,所以 JavaScript 自动将空字符串,转为布尔值false,导致程序不会进入代码块,所以没有任何输出。

注意,空数组([])和空对象({})对应的布尔值,都是true。

if ( [] ) { console.log('true'); }// true
if ( {} ) { console.log('true'); }// true

设计模式专题之解释器模式(十八)

解释器模式(Interpreter Pattern)

定义:解释器模式是指,对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器,来解释语言中定义的句子。

案例:解析 dom 树。

// xPath解释器
var Interpreter = (function() {
  // 获取兄弟元素名称
  function getSulingName(node) {
    if (node.previousSibling) {
      var name = '',
        count = 1,
        nodeName = node.nodeName,
        sibling = node.previousSibling;
      while (sibling) {
        if (
          sibling.nodeType == 1 &&
          sibling.nodeType === node.nodeType &&
          sibling.nodeName
        ) {
          // 如果节点名称和前一个兄弟元素名称相同
          if (nodeName == sibling.nodeName) {
            name += ++count;
          } else {
            count = 1;
            name += '|' + sibling.nodeName.toUpperCase();
          }
        }
        sibling = sibling.previousSibling;
      }
      return name;
    } else {
      return '';
    }
  }
  return function(node, wrap) {
    var path = [],
      wrap = wrap || document;
    if (node == wrap) {
      if (wrap.nodeType == 1) {
        path.push(wrap.nodeName.toUpperCase());
      }
      return path;
    }
    if (node.parentNode !== wrap) {
      path = arguments.callee(node.parentNode, wrap);
    } else {
      if (wrap.nodeType == 1) {
        path.push(wrap.nodeName.toUpperCase());
      }
    }
    var sublingsNames = getSulingName(node);
    if (node.nodeType == 1) {
      path.push(node.nodeName.toUpperCase() + sublingsNames);
    }
    return path;
  };
})();
var path = Interpreter(document.getElementsByTagName('img')[0]);
// ["HTML", "BODY|HEAD", "DIV", "SECTION", "HEADER", "DIV", "DIV", "DIV", "A", "IMG"]

babel 中的 AST 解析器也是一个比较好的案例

HTTP专题之前端跨域的九种方式(四)

跨域

在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,从而克服Ajax只能同源使用的限制。

同源策略

同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

url���

同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM和JS对象无法获得
  • AJAX 请求不能发送

常见的跨域场景

img

9种跨域解决方案

1、jsonp

jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
1)原生JS实现:

 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

服务端返回如下(返回时即执行全局函数):

handleCallback({"success": true, "user": "admin"})

2)jquery Ajax实现:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",  // 自定义回调函数名
    data: {}
});

3)Vue axios实现:

this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

后端node.js代码:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

jsonp的缺点:只能发送 get请求。

2、跨域资源共享(CORS)

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器将CORS跨域请求分为简单请求和非简单请求。
只要同时满足一下两个条件,就属于简单请求
使用下列方法之一:

  • head
  • get
  • post

请求的Heder是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件,就属于非简单请求。浏览器对这两种的处理,是不一样的。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

CORS请求设置的响应头字段,都以 Access-Control-开头:

1)Access-Control-Allow-Origin:必选
它的值可以能是多个域名,或者设置为 * 允许任意域名的访问。
2)Access-Control-Allow-Credentials:可选
它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
3)Access-Control-Expose-Headers:可选
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

预检请求

预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

1)Access-Control-Request-Method:必选
用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
2)Access-Control-Request-Headers:可选
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的回应

服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
HTTP回应中,除了关键的是Access-Control-Allow-Origin字段,其他CORS相关字段如下:
1)Access-Control-Allow-Methods:必选
它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
3)Access-Control-Allow-Credentials:可选
该字段与简单请求时的含义相同。
4)Access-Control-Max-Age:可选
用来指定本次预检请求的有效期,单位为秒。

CORS跨域示例

1)前端设置

  • 原生ajax:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
复制代码
  • jquery ajax:
$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});


2)服务端设置

  • nodejs代码
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');


3、nginx代理跨域

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin...等字段。

  1. nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}


  1. nginx反向代理接口跨域

跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。

实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。
nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}


4、nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

  1. 非vue框架的跨域
    使用node + express + http-proxy-middleware搭建一个proxy服务器。
  • 前端代码:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();


  • 中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');



  1. vue框架的跨域
    node + vue + webpack + webpack-dev-server搭建的项目,跨域请求接口,直接修改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。
    webpack.config.js部分配置:
module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}


5、document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>


<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    console.log('get js data from parent ---> ' + window.parent.user);
</script>


6、location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
  具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>


<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>


<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>


7、window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});


<script>
    window.name = 'This is domain2 data!';
</script>


  通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

8、postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数:

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>


2)b.html:(www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>


9、WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。 原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1)前端代码:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>


2)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});
复制代码

小结

CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案

JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。

日常工作中,用得比较多的跨域方案是 CORS nginx 反向代理

JavaScript基础专题之创建对象几种方式及优缺点(九)

前言

根据《JavaScript高级程序设计》(红宝书)来总结一下创建对象的几种方式及优缺点

1. 工厂模式

function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;
}

var person1 = createPerson('chris');

缺点:对象无法识别,因为所有的实例都指向一个原型

2. 构造函数模式

function Person(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    };
}

var person1 = new Person('chris');


优点:实例可以识别为一个特定的类型

缺点:每次创建实例时,每个方法都要被创建一次

3. 原型模式

function Person(name) {

}

Person.prototype.name = 'chris';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();


优点:方法不会重新创建

缺点:1. 所有的属性和方法都共享 2. 不能初始化参数

3.1 原型模式优化

function Person(name) {

}

Person.prototype = {
    name: 'chris',
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();


优点:封装性好了一点

缺点:重写了原型,丢失了constructor属性

3.2 原型模式优化

function Person(name) {

}

Person.prototype = {
    constructor: Person,
    name: 'chris',
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

优点:实例可以通过constructor属性找到所属构造函数

缺点:还是需要重写原型

4. 组合模式

构造函数模式与原型模式一起上

function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

优点:该共享的共享,该私有的私有,使用最广泛的方式

缺点:有的人就是希望全部都写在一起,即更好的封装性

4.1 动态原型模式

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype.getName = function () {
            console.log(this.name);
        }
    }
}

var person1 = new Person();

注意:使用动态原型模式时,不能用对象字面量重写原型

解释下为什么:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('chris');
var person2 = new Person('daisy');

// 报错 并没有该方法
person1.getName();

// 注释掉上面的代码,这句是可以执行的。
person2.getName();

为了解释这个问题,假设开始执行var person1 = new Person('chris')

我们回想一下 new 的实现步骤:

  1. 首先新建一个对象
  2. 然后将对象的原型指向 Person.prototype
  3. 然后 Person.apply(obj)
  4. 返回这个对象

注意这个时候,回顾下 apply 的实现步骤,会执行 obj.Person 方法,这个时候就会执行 if 语句里的内容,注意构造函数的 prototype 属性指向了实例的原型,使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了!


如果你就是想用字面量方式写代码,可以尝试下这种:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
        //将实例直接返回
        return new Person(name);
    }
}

var person1 = new Person('chris');
var person2 = new Person('daisy');

person1.getName(); // chris
person2.getName();  // daisy

5.1 寄生 - 构造函数模式

function Person(name) {
    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;

}

var person1 = new Person('chris');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object)  // true

这个模式创建的实例使用 instanceof 都无法指向构造函数

这样方法可以在特殊情况下使用。比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,我们可以这样写:

function SpecialArray() {
    var values = new Array();

    for (var i = 0, len = arguments.length; i < len; i++) {
        values.push(arguments[i]);
    }

    values.toPipedString = function () {
        return this.join("|");
    };
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');


console.log(colors);
console.log(colors.toPipedString()); // red|blue|green

console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2

你会发现,其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new,实际上两者的结果是一样的。

但是作者可能是希望能像使用普通 Array 一样使用 SpecialArray,虽然把 SpecialArray 当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。

在可以使用其他模式的情况下,不要使用这种模式。

但是值得一提的是,上面例子中的循环:

for (var i = 0, len = arguments.length; i < len; i++) {
    values.push(arguments[i]);
}

完成可以替换成:

values.push.apply(values, arguments);

5.2 稳妥构造函数模式

function person(name){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    };
    return o;
}

var person1 = person('chris');

person1.sayName(); // chris

person1.name = "daisy";

person1.sayName(); // chris

console.log(person1.name); // daisy

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。

与寄生构造函数模式有两点不同:

  1. 新创建的实例方法不引用 this
  2. 不使用 new 操作符调用构造函数

稳妥对象最适合在一些安全的环境中。

稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。

补充

ES6创建对象

class person{
    constructor(name){
        this.name = name
    }
    sayName(){
        console.log(this.name)
    }
}

person.prototpye.hi = function(){console.log('2')}

new person('chris')

使用class定义类跟上面的构造函数+原型组合模式有一些相似之处,但又有所区别。

class定义的类上有个 constructor方法,这就是构造方法,该方法会返回一个实例对象,

this代表的就是实例对象,这跟上边的构造函数模式很类似。

es5与es6定义对象的区别:

  1. class的构造函数必须使用new进行调用,普通构造函数不用new也可执行。

  2. class不存在变量提升,es5中的function存在变量提升。

  3. class内部定义的方法不可枚举,es5在prototype上定义的方法可以枚举。

我们可以用 Babel 转义一下:

'use strict'; // es6中class使用的是严格模式

// 处理class中的方法
var _createClass = function () { 
   function defineProperties(target, props) { 
      for (var i = 0; i < props.length; i++) { 
         var descriptor = props[i]; 
         // 默认不可枚举
         descriptor.enumerable = descriptor.enumerable || false; 
         descriptor.configurable = true; 
         if ("value" in descriptor) descriptor.writable = true; 
         Object.defineProperty(target, descriptor.key, descriptor);
      } 
   } 
   return function (Constructor, protoProps, staticProps) { 
      if (protoProps) defineProperties(Constructor.prototype, protoProps); 
      if (staticProps) defineProperties(Constructor, staticProps); 
      return Constructor; 
   }; 
}();

// 对构造函数进行判定
function _classCallCheck(instance, Constructor) { 
   if (!(instance instanceof Constructor)) { 
      throw new TypeError("Cannot call a class as a function"); 
   }
}

// class Person转换为 es5的function
var Person = function () {
    function Person(age, name) {
        // 调用了_classCallCheck检查Person是否为构造函数
        _classCallCheck(this, Person); 

        
        this.name = name;
    }

    // 调用_createClass处理定义在class中的方法。
    _createClass(Person, [{
        key: 'sayName',
        value: function sayName() {
            console.log(this.name);
        }
    }]);

    return Person;
}();

var person1 = new Person('chris');

设计模式专题之建造者模式(二十)

建造者模式(Builder Pattern)

定义:建造者模式是指将一个复杂的对象分解成多个简单的对象来进行构建,将复杂的构建层与表示层分离,使得相同的构建过程可以创建不同的表示。

案例:制造一辆汽车。

// 产品类:car 目前需要构建一辆车。
function car() {
  this.wheel = '';
  this.engine = '';
}

// 建造者类,里面有专门负责各个部分的工人
function carBuilder() {
  this.wheelBuilder = function() {
    this.wheel = '轮子';
  };
  this.engineBuilder = function() {
    this.engine = '发动机';
  };
  this.getCar = function() {
    var Car = new car();
    Car.wheel = this.wheel;
    Car.engine = this.engine;
    return Car;
  };
}

// 指挥者类,指挥各个部分的工人工作
function director() {
  this.action = function(builder) {
    builder.wheelBuilder();
    builder.engineBuilder();
  };
}

// 开始创建
var builder = new carBuilder();
var director = new director();
director.action(builder);
var Car = builder.getCar();
console.log(Car);

JavaScript基础系列总结

WHY?

最近很迷茫,苦于如何自我提升,也听了很多的知乎live去寻找自己的方向,最近听到最多的声音是抛开一些框架去探究这门的本质。

记得在58面试的时候,前端主管问了我一些基础的问题,并没有在意你会多少框架和工具。可能自己的方向走的不太对,依然不知道自己的基础到底怎么样,这也是自学的一个痛点,并不像在学校那样,会有老师给你打分,有同学可以讨论,了解对方的水平。

所以自己着手解决这个痛点,重新"探索"JS这门语言,忘掉DOM、浏览器、 node环境。更多的是对语言本身的研究。

基础系列只是自己学习后复盘,进行总结的一些知识点,并没有写的过于深入,所以以后随着自己工作经验的增加,写一些更加深入的文章。

How?

主要是看一些博客,和一些经典的书籍,7月份利用上下班的通勤时间,把《你不会到的JavaScript》的上卷、中卷看完了,并把一些重点内容记录在了博客里。一些弄的不是特别清楚的知识点也会结合《红宝书》和《犀牛书》去理解,当然,还有一些大牛的博客。后面我会将链接分享出来。

what?

本系列一共十三篇,由于自己是第一次以总结的方式去写博客,会有一些书写或表述不到位的地方,我也在系列的后期去不断的复查。可能这些内容并不是很全,但是已经覆盖了一些 JS 常用的基础知识,本打算写一篇正则的文章,但思考了一下,正则也并不算 JS 基础,算是比较通用的知识。我收藏了一篇很全的正则小书,最后也会分享出来。

最近要做什么?

最近在参考一下 lodash , underscore 源码,把一些高阶函数去自己动手实现了一遍,正在准备写进阶系列,总的来说比较吃力,但是会坚持下去的,毕竟还要写好多系列。最近比较对 node 后台和 ptyhon , golang感兴趣。周末除了造轮子和写博客,还会学习一些其他语言。

学习后端语言的同时,也发现自己的短板,作为非科班的开发者,说到线程和进程,CPU的时候,自己就傻掉了。所以周末也会学习科班的基础课,比如《操作系统》,《计算机组成原理》,《计算机网络》。在慕课网已经选好了课程,最后会分享出来。

参考链接

《JavaScript高级程序设计》(第三版)(红宝书)

《JavaScript权威指南》(犀牛书)

《你不知道的JavaScript(上卷)》

《你不知道的JavaScript(中卷)》

冴羽的博客

深入理解 JavaScript 异步

分享链接

操作系统

计算机组成原理(上)

计算机组成原理(下)

计算机网络

JS 正则迷你书

JavaScript进阶系列之数组去重(一)

前言

进阶第一篇,先用老生常谈,面试中常问的数组去重来练练手吧

循环套循环

比较暴力的解法

let array = [1,2,3,3,4,5,4]

function unique(array) {
    // res用来存储结果
    let res = []
    let lens = arrray.length
    let resLens = res.length
    for (let i = 0, i < lens; i++) {
        for (let j = 0,  j < resLens; j++ ) {
            if (array[i] === res[j]) {
                break
            }
        }
        // 如果array[i]是唯一的,那么执行完循环,j等于resLens
        if (j === resLens) {
            res.push(array[i])
        }
    }
    return res
}

console.log(unique(array)); // [1,2,3,4,5]

在这个方法中,我们使用循环嵌套,最外层循环 array,里面循环 res,如果 array[i] 的值跟 res[j] 的值相等,就跳出循环,如果都不等于,说明元素是唯一的,这时候 j 的值就会等于 res 的长度,根据这个特点进行判断,将值添加进 res。

优点就是兼容性好。

Hash

Hash结构用来记录次数,比较好理解

let array = [1,2,3,3,4,5,4]
let unique = (arr) => {
    let hash = {}
    let res = []
    for (key in arr) {
    	//表内如果没有这个元素,就记录为1
        if (!hash[arr[key]]) {
            hash[arr[key]] = 1
            res.push(arr[key])
        }
    }
    return res
}
console.log(unique(array)); // [1,2,3,4,5]

利用哈希表,如果 Hash 表中不存在则记录为1,最后形成一个{1:1,2:1,3:1,4:1,5:1}的结构,之后将 key 推入新数组中。

indexOf

利用 indexOf 查找代替循环

let array = [1,2,3,3,4,5,4]

function unique(array) {
    let res = []
    let len = array.length
    for (let i = 0,  i < len; i++) {
        let current = array[i]
        if (res.indexOf(current) === -1) {
            res.push(current)
        }
    }
    return res
}

console.log(unique(array)) // [1,2,3,4,5]

利用 indexOf 查找元素第一次出现的位置的特性与 filter 的配合

let unique = function(){
    let arr = [...array]
    arr.filter((item,index,array)=>{
        return array.indexOf(item) === index
    })
    return arr
}

复杂类型去重

利用 Hash 、 Reduce 与深拷贝

let array = [{value: 1}, {value: 1}, {value: 2}];

function unique(array) {
    let obj = {};
    return array.filter(function(item){
        return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? 
        false : (obj[typeof item + JSON.stringify(item)] = true)
    })
}
console.log(unique(array)); // [{value: 1}, {value: 2}]

多维去重

参考 lodash 利用 reduce 和 hash 实现多维去重

let hash = {};

function uniqueBy(arr, key){
    return arr.reduce(function(previousValue, currentValue){
        //存在的则存入hash,
        hash[currentValue[key]] ? '' : hash[currentValue[key]] = true && previousValue.push(currentValue);
        return previousValue
    }, []);
}
//去除name相同的对象
const uniqueArr = uniqueBy([{name: 'zs', age: 15}, {name: 'lisi'}, {name: 'zs'}], 'name');

console.log(uniqueArr); //[{name: 'zs', age: 15}, {name: 'lisi'}]

ES6

利用 Set 数据结构不存在重复属性

let array = [1,2,3,3,4,5,4]
let unique = arr => [...new Set(arr)]
console.log(unique(array)); // [1,2,3,4,5]

利用 Map 数据结构得到不存在 map 中并将其赋值 value 为 1 的值

let array = [1,2,3,3,4,5,4]
let unique = arr => {
    const map = new Map() 
    return arr.filter(item => !map.has(item) && map.set(item, 1))
}
console.log(unique(array)); // [1,2,3,4,5]

特殊类型比较

去重的方法就到此结束了,然而要去重的元素类型可能是多种多样,除了例子中简单的 1 和 '1' 之外,其实还有 null、undefined、NaN、对象等,那么对于这些元素,之前的这些方法的去重结果又是怎样呢?

在此之前,先让我们先看几个例子:

var str1 = '1';
var str2 = new String('1');

console.log(str1 == str2); // true
console.log(str1 === str2); // false

console.log(null == null); // true
console.log(null === null); // true

console.log(undefined == undefined); // true
console.log(undefined === undefined); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(/a/ == /a/); // false
console.log(/a/ === /a/); // false

console.log({} == {}); // false
console.log({} === {}); // false

console.log([] == []); //false
console.log([] === []); //false

那么,对于这样一个数组

var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN,[],[]];

以上各种方法去重的结果到底是什么样的呢?

我特地整理了一个列表,我们重点关注下对象和 NaN 的去重情况:

方法 结果 说明
for循环 [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 对象、数组和 NaN 不去重
indexOf [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 对象、数组和 NaN 不去重
hash结构 [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] 全部去重
filter + indexOf [1, "1", null, undefined, String, String, /a/, /a/] 对象、数组不去重, NaN 会被忽略掉
Set [1, "1", null, undefined, String, String, /a/, /a/, NaN] 对象不去重 NaN 去重

想了解为什么会出现以上的结果,看两个 demo 便能明白:

// demo1
var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1

indexOf 底层还是使用 === 进行判断,因为 NaN === NaN的结果为 false,所以使用 indexOf 查找不到 NaN 元素

// demo2
function unique(array) {
   return Array.from(new Set(array));
}
console.log(unique([NaN, NaN])) // [NaN]

Set 认为尽管 NaN === NaN 为 false,但是这两个元素是重复的。

var unique = arr => [...new Set(arr)]
console.log(unique([NaN,NaN,null,null,null])) //[NaN,null]

设计模式专题之访问者模式(十七)

访问者模式(Visitor Pattern)

定义:针对于对象结构中的元素,定义在不改变对象的前提下访问结构中元素的方法。

在访问者模式中,主要包括下面几个角色

1、抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是 visit 方法中的参数定义哪些对象是可以被访问的。

2、访问者:实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。

3、抽象元素类:接口或者抽象类,声明接受哪一类访问者访问,程序上是通过 accept 方法中的参数来定义的。抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接收哪类访问者来访问。

4、元素类:实现抽象元素类所声明的 accept 方法,通常都是 visitor.visit(this),基本上已经形成一种定式了。

5、结构对象:一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如 List、Set、Map 等,在项目中一般很少抽象出这个角色。

// 访问者
function Visitor() {
  this.visit = function(concreteElement) {
    concreteElement.doSomething();
  };
}
// 元素类
function ConceteElement() {
  this.doSomething = function() {
    console.log('这是一个具体元素');
  };
  this.accept = function(visitor) {
    visitor.visit(this);
  };
}
// Client
var ele = new ConceteElement();
var v = new Visitor();
ele.accept(v);

数据结构专题之链表(三)

前言

单链表中的每个结点不仅包含值,还包含链接到下一个结点内存地址。在C语言中我们称它为指针,在JS中我们称他为引用。通过这种方式,单链表将所有结点按顺序组织起来。数组是占用内存的连续地址,而链表恰恰相反,而是占用非连续的内存地址。

链表更适合添加和删除操作,时间复杂度为O(1)。

下面是一个单链表的例子:

img

蓝色箭头显示单个链接列表中的结点是如何组合在一起的。

链表通过指针连接, 如果需要插入或删除只需改变相应指针指向的目标就行。这也是链表相比较于数组最大的优点, 不用移动元素就能很轻松地添加删除元素。如果有大量的数据要插入或删除可以考虑使用链表这种数据结构。

var LinkedList = function() {
  const Node = function(element) {
    this.element = element
    this.next = null
  }

  let head = null
  let current
  let length = 0

  // 在链表末尾加入元素
  this.append = function(element) {
    const node = new Node(element)
    if (head === null) {       // 插入第一个链表
      head = node
    } else {
      current = head
      while (current.next) {     // 找到最后一个节点
        current = current.next
      }
      current.next = node
    }
    length++
  }

  // 移除指定位置元素
  this.removeAt = function(position) {
    if (position > -1 && position < length) {
      let previous
      let index = 0
      if (position === 0) {         // 如果是第一个链表的话, 特殊对待
        head = head.next
      } else {
        current = head
        while (index < position) {  // 循环找到当前要删除元素的位置
          previous = current
          current = current.next
          index++
        }
        previous.next = current.next
      }
      length--
    }
  }

  // 在指定位置加入元素
  this.insert = function(position, element) {
    const node = new Node(element)
    let index = 0
    let current, previous
    if (position > -1 && position < length + 1) {
      if (position === 0) { // 在链表最前插入元素
        current = head
        head = node
        head.next = current
      } else {
        current = head
        while (index < position) { // 同 removeAt 逻辑, 找到目标位置
          previous = current
          current = current.next
          index++
        }
        previous.next = node       // 在目标位置插入相应元素
        node.next = current
      }
      length++
    }
  }

  // 链表中是否含有某个元素, 如果有的话返回相应位置, 无的话返回 -1
  this.indexOf = function(element) {
    let index = 0
    current = head
    while (index < length) {
      if (current.element === element) {
        return index
      }
      current = current.next
      index++
    }
    return -1
  }

  // 移除某元素
  this.remove = function(element) {
    const position = this.indexOf(element)
    this.removeAt(position)
  }

  // 获取大小
  this.size = function() {
    return length
  }

  // 获取最开头的链表
  this.getHead = function() {
    return head
  }

  // 是否为空
  this.isEmpty = function() {
    return length === 0
  }

  // 打印链表元素
  this.log = function() {
    current = head
    let str = current.element
    while (current.next) {
      current = current.next
      str = str + ' ' + current.element
    }
    return str
  }
}

// 测试用例
var linkedList = new LinkedList()
linkedList.append(5)
linkedList.append(10)
linkedList.append(15)
linkedList.append(20)
linkedList.log()         // '5 10 15 20'
linkedList.removeAt(1)
linkedList.log()         // '5 15 20'
linkedList.insert(1, 10)
linkedList.log()

双向链表

单向链表如果错过了某次查询就得重头开始重新查找, 双向链表进行了升级, 除了可以向后查找, 同时也支持向前查找

img

var DbLinkedList = function() {
  const Node = function(element) {
    this.element = element
    this.next = null
    this.prev = null
  }

  let head = null
  let tail = null
  let current, previous
  let length = 0

  // 指定任意位置插入元素
  this.insert = function(position, element) {
    let index = 0
    const node = new Node(element)
    if (position > -1 && position < length + 1) {
      if (position === 0) {             // ① 在开头插入元素
        if (head === null) {  // 链表内元素为空
          head = node
          tail = node
        } else {              // 链表内存在元素
          current = head
          head = node
          head.next = current
          current.prev = head
        }
      } else if (position === length) { // ② 在末尾插入元素
        current = tail
        tail = node
        current.next = tail
        tail.prev = current
      } else {                          // ③ 在链表中插入元素
        current = head
        while (index < position) { // 找到需插入节点的位置
          previous = current
          current = current.next
          index++
        }
        previous.next = node
        node.next = current

        current.prev = node
        node.prev = previous
      }
      length++
    }
  }

  // 删除指定位置的元素
  this.removeAt = function(position) {
    let index = 0
    if (position > -1 && position < length) {
      if (position === 0) {                  // 删除链表最开头的元素
        if (length === 1) {
          head = null
          tail = null
        } else {
          current = head
          head = current.next
          head.prev = current.prev
        }
      } else if (position === length - 1) {  // 删除链表最末尾的元素
        current = tail
        tail = current.prev
        tail.next = current.next
      } else {                               // 删除链表中的元素
        current = head
        while (index < position) {
          previous = current
          current = current.next
          index++
        }
        previous.next = current.next
        current.next.prev = previous
      }
      length--
    }
  }

  this.log = function() {
    current = head
    let str = current.element
    while (current.next) {
      current = current.next
      str = str + ' ' + current.element
    }
    return str
  }
}

var dbLinkedList = new DbLinkedList()
dbLinkedList.insert(0, 5)
dbLinkedList.insert(1, 10)
dbLinkedList.insert(2, 15)
dbLinkedList.insert(3, 20)
dbLinkedList.insert(4, 25)
dbLinkedList.log()         // "5 10 15 20 25"
dbLinkedList.removeAt(4)
dbLinkedList.removeAt(0)
dbLinkedList.removeAt(1)
dbLinkedList.log()         // "10 20"

JavaScript基础专题之异步(十三)

什么是异步?

简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

我们以 JqueryAJAX 发送请求为例。

console.log('1')
var ajax = $.ajax({
    url: '/data/data1.json',
    success: function (res) {
        console.log('success')
    }
})
console.log('2')
//1 2 success

上面代码中 $.ajax() 需要传入两个参数进去,urlsuccess,其中url是请求的路由,success是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。

上面的代码中,任务的第一段是向 url 发送请求。然后,程序执行其他任务,等到数据返回,我们再执行后续的操作。

这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做 ``同步

为何会有异步?

一句话, JS 是单线程的语言。所谓“单线程”就是一根筋,会对程序一行一行的进行执行,上一行代码的执行未完成,就会一直等着上一行执行结束后执行。例如

var i, 
	t = Date.now()
for (i = 0; i < 100000000; i++) {
}
console.log(Date.now() - t)  // 230 (chrome浏览器)

上面的程序花费 230ms 的时间执行完成,执行过程中必须要执行完循环,之后才会下面的代码。

对于 JS 的浏览器环境下,可能会有大量的网络请求,而一个网络资源什么时候返回,这个时间是不可预估的。那么这种情况也要傻傻的等着,啥都不做吗?

那肯定不行。

因此,JS 对于这种场景就设计了异步。即发起一个网络请求,就先不管这边了,先干其他事儿,网络请求什么时候返回结果,到时候再说。这样就能保证一个网页的流畅的运行了。

异步场景与解决方案

我们常用的异步场景有大致几点:

  • 网络请求,如ajax
  • IO 操作,如 node 中的readFile writeFile `
  • 定时函数,如setTimeout setInterval
  • 事件监听,如 addEventListener

在 ES6 一直前,我们异步编程的解决方案最多的是运用 回调函数,自 ES6 出现之后,将 JavaScript 异步编程带入了一个全新的阶段。

解决方案大概分为以下几种:

  • 回调函数
  • Promise 对象
  • Generator对象
  • Async/Await语法

下面先说说回调函数。

回调函数

JavaScript 语言最初对异步编程的实现,就是运用 回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。**它的英语名字 callback,直译过来就是"重新调用"。

Node.js 中读取文件为例

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

上面代码中,readFile 函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了 /etc/passwd 这个文件以后,回调函数才会执行。

一个有趣的问题是,为什么 Node.js 约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是 null)?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。

Promise

回调函数本身可以很好的解决异步问题,但是又出现新的问题,就是多层嵌套。假定读取A文件之后,再读取B文件,代码如下。

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
})

可想而知,如果依次读取多个文件,就会出现多重嵌套。这样我们的代码可读性和可维护性就会降低。这种情况就称为"回调地狱"(callback hell)。

Promise 就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许使用链式调用,逻辑更加清晰。

采用Promise,连续读取多个文件,写法如下。

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function(data){
  console.log(data.toString());
})
.then(function(){
  return readFile(fileB);
})
.then(function(data){
  console.log(data.toString());
})
.catch(function(err) {
  console.log(err);
});

上面代码中,使用了 fs-readfile-promise 模块,它的作用就是返回一个 Promise 版本的 readFile 函数。Promise 提供 then 方法加载回调函数,catch方法捕捉执行过程中抛出的错误。

可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。

Promise 的最大问题是代码冗余严重,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,可读性也变的很差。

那么,有没有更好的写法呢?

Generator

在 ES6 出现之前,基本都是各式各样类似Promise的解决方案来处理异步操作的代码逻辑,但是 ES6 的Generator却给异步操作又提供了新的思路,马上就有人给出了如何用Generator来更加优雅的处理异步操作。

先来一段最基础的Generator代码

function* Hello() {
    yield 100
    yield (function () {return 200})()
    return 300
}

var h = Hello()
console.log(typeof h)  // object

console.log(h.next())  // { value: 100, done: false }
console.log(h.next())  // { value: 200, done: false }
console.log(h.next())  // { value: 300, done: true }
console.log(h.next())  // { value: undefined, done: true }

nodejs 环境执行这段代码,打印出来的数据都在代码注释中了,也可以自己去试试。将这段代码简单分析一下吧

  • 定义Generator时,需要使用function*,其他的和定义函数一样。内部使用yield,至于yield的用处以后再说
  • 执行var h = Hello()生成一个Generator对象,经验验证typeof h发现不是普通的函数
  • 执行Hello()之后,Hello内部的代码不会立即执行,而是出于一个暂停状态
  • 执行第一个 h.next() 时,会激活刚才的暂停状态,开始执行Hello内部的语句,但是,直到遇到 yield 语句。一旦遇到 yield 语句时,它就会将 yield 后面的表达式执行,并返回执行的结果,然后又立即进入暂停状态。
  • 因此第一个 console.log(h.next()) 打印出来的是 { value: 100, done: false }value 是第一个yield返回的值, done: false 表示目前处于暂停状态,尚未执行结束,还可以再继续往下执行。
  • 执行第二个 h.next() 和第一个一样,不在赘述。此时会执行完第二个 yield 后面的表达式并返回结果,然后再次进入暂停状态
  • 执行第三个 h.next() 时,程序会打破暂停状态,继续往下执行,但是遇到的不是yield而是return。这就预示着,即将执行结束了。因此最后返回的是 { value: 300, done: true }done: true 表示执行结束,无法再继续往下执行了。
  • 再去执行第四次 h.next() 时,就只能得到 { value: undefined, done: true } ,因为已经结束,没有返回值了。

一口气分析下来,发现并不是那么简单,可见Generator的学习成本多高,但是一旦学会,那将受用无穷!别着急,跟着我的节奏慢慢来,一行一行代码看,你会很快深入了解Genarator

但是,你要详细看一下上面的所有步骤,争取把我写的每一步都搞明白。如果搞不明白细节,至少要明白以下几个要点:

  • Generator不是函数
  • Hello()不会立即出发执行,而是一上来就暂停
  • 每次h.next()都会打破暂停状态去执行,直到遇到下一个yield或者return
  • 遇到yield时,会执行yeild后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时done: false
  • 遇到return时,会返回值,执行结束,即done: true
  • 每次h.next()的返回值永远都是{value: ... , done: ...}的形式

之前讲解Promise时候,主要是使用then做链式操作。

举个例子:

readFilePromise('some1.json').then(data => {
    console.log(data)  // 打印第 1 个文件内容
    return readFilePromise('some2.json')
}).then(data => {
    console.log(data)  // 打印第 2 个文件内容
    return readFilePromise('some3.json')
}).then(data => {
    console.log(data)  // 打印第 3 个文件内容
    return readFilePromise('some4.json')
}).then(data=> {
    console.log(data)  // 打印第 4 个文件内容
})

而如果学会Generator那么读取多个文件就是如下这样写。先不要管如何实现的,光看一看代码,你就能比较出哪个更加简洁、更加易读、更加所谓的优雅!

//借助 co 函数库
co(function* () {
    const r1 = yield readFilePromise('some1.json')
    console.log(r1)  // 打印第 1 个文件内容
    const r2 = yield readFilePromise('some2.json')
    console.log(r2)  // 打印第 2 个文件内容
    const r3 = yield readFilePromise('some3.json')
    console.log(r3)  // 打印第 3 个文件内容
    const r4 = yield readFilePromise('some4.json')
    console.log(r4)  // 打印第 4 个文件内容
})

是不是更像是同步了?

我们看到了 Generator 已经实现了异步代码同步书写,那我们看看 Async/Await是什么?

Async/Await

Async/Await 作为异步编程的终极解决方案,根本不用关心它是不是异步。

Async/Await 又是怎么实现的呢?

一句话概括,Async/Await 就是 Generator 的语法糖。

举个例子:

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

写成 Async/Await,就是下面这样。

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,Async/Await 就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

HTTP专题之HTTP基础(二)

HTTP协议发展历史

http是基于TCP/IP之上的应用层协议,也是互联网的基础协议,最新http2协议基于信道复用,分帧传输在传输效率上也有了大幅度的提升

阶段一

http/0.9

只有一个命令GET,对应我们现在的请求GET、POST,没有header等描述数据的信息,服务器发送完毕数据就关闭TCP链接,每个http请求都要经历一次dns域名解析、传输和四次挥手,这样反复创建和断开tcp链接的开销是巨大的,在现在看来这种方式很糟糕。

阶段二

http/1.0

  • 增加了很多命令POST、GET、HEAD
  • 等增status code和header

status code描述服务端处理某一个请求之后它的状态, header是不管发送还是请求一个数据它的描述。

  • 多字符集支持、多部分发送、权限、缓存等。

阶段三

http/1.1

  • 持久链接
  • 管道机制(pipeline)

可以在同一个链接里发送多个请求,但是在服务端对于进来的请求都是要按照顺序进行内容的返回,如果前一个请求很慢,后一个请求很多,它也需要第一个请求发送之后,后一个请求才可以发送,这块在http2里面进行了优化

  • 增加host和其他功能

增加host可以在同一台物理服务器上跑多个web服务,例如一个nodejs的web服务,一个java的web服务

阶段四

http/2

  • 所有数据以二进制传输
  • 同一个链接里面发送多个请求,不在需要按照顺序来
  • 头信息压缩以及推送等提高效率的功能

HTTP三次握手

先清楚一个概念http请求与tcp链接之间的关系,在客户端向服务端请求和返回的过程中,是需要去创建一个TCP connection,因为http是不存在链接这样一个概念的,它只有请求和响应这样一个概念,请求和响应都是一个数据包,中间要通过一个传输通道,这个传输通道就是在TCP里面创建了一个从客户端发起和服务端接收的一个链接,TCP链接在创建的时候是有一个三次握手(三次网络传输)这样一个消耗在的。

客户端与服务器端的一次请求

img

三次握手时序图

img

第一次握手: 建立连接,客户端A发送SYN=1、随机产生Seq=client_isn的数据包到服务器B,等待服务器确认。

第二次握手: 服务器B收到请求后确认联机(可以接受数据),发起第二次握手请求,ACK=(A的Seq+1)、SYN=1,随机产生Seq=client_isn的数据包到A。

第三次握手: A收到后检查ACK是否正确,若正确,A会在发送确认包ACK=服务器B的Seq+1、ACK=1,服务器B收到后确认Seq值与ACK值,若正确,则建立连接。

TCP标示:

  1. SYN(synchronous建立联机)
  2. ACK(acknowledgement 确认)
  3. Sequence number(顺序号码)

三次握手数据包详细内容分析

这里采用的是wireshark 官网地址 https://www.wireshark.org/,是一个很好的网络数据包抓取和分析软件。

示例采用的网址http://news.baidu.com/,windows下打开cmd、Mac下打开终端ping下得到ip可以利用wireshark工具进行一次ip地址过滤,只分析指定的数据。

  • 第一次握手,客户端发送一个TCP,标志位为SYN,Seq(序列号)=0,代表客户端请求建立链接,如下图所示

img

  • 第二次握手,服务器发回数据包,标志位为[SYN, ACK],ACK设置为客户端第一次握手请求的Seq+1,即ACK=0+1=1,在随机产生一个Seq的数据包到客户端。

img

  • 第三次握手请求,客户端在次发送确认数据包,标识位为ACK,把服务器发来的Seq+1,即ACK=0+1,发送给服务器,服务器成功收到ACK报文段之后,连接就建立成功了。

img

总结

至于为什么要经过三次握手呢,是为了防止服务端开启一些无用的链接,网络传输是有延时的,中间可能隔着非常远的距离,通过光纤或者中间代理服务器等,客户端发送一个请求,服务端收到之后如果直接创建一个链接,返回内容给到客户端,因为网络传输原因,这个数据包丢失了,客户端就一直接收不到服务器返回的这个数据,超过了客户端设置的时间就关闭了,那么这时候服务端是不知道的,它的端口就会开着等待客户端发送实际的请求数据,服务这个开销也就浪费掉了。

URI

URI

Uniform Resource Identifier/统一资源标志符,用来标示互联网上唯一的信息资源,包括URL和URN。

URL

Uniform Resource Locator/统一资源定位器

URN

Uniform Resource Name/统一资源名称,例如资源被移动后如果是URL则会返回404,在URN中资源被移动之后还能被找到,当前还没有什么成熟的使用方案

请求和响应报文

1. 请求报文

img

2. 响应报文

img

HTTP 方法

客户端发送的 请求报文 第一行为请求行,包含了方法字段。

GET

获取资源

当前网络请求中,绝大部分使用的是 GET 方法。

HEAD

获取报文首部

和 GET 方法类似,但是不返回报文实体主体部分。

主要用于确认 URL 的有效性以及资源更新的日期时间等。

POST

传输实体主体

POST 主要用来传输数据,而 GET 主要用来获取资源。

更多 POST 与 GET 的比较请见第九章。

上传文件

由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。

PUT /new.html HTTP/1.1
Host: example.com
Content-type: text/html
Content-length: 16

<p>New File</p>

PATCH

对资源进行部分修改

PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100

[description of changes]

DELETE

删除文件

与 PUT 功能相反,并且同样不带验证机制。

DELETE /file.html HTTP/1.1

OPTIONS

查询支持的方法

查询指定的 URL 能够支持的方法。

会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。

CONNECT

要求在与代理服务器通信时建立隧道

使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。

CONNECT www.example.com:443 HTTP/1.1

HTTP 状态码

服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。

状态码 类别 含义
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出错

1XX 信息

  • 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

2XX 成功

  • 200 OK
  • 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
  • 206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。

3XX 重定向

  • 301 Moved Permanently :永久性重定向
  • 302 Found :临时性重定向
  • 303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
  • 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
  • 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
  • 307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。

4XX 客户端错误

  • 400 Bad Request :请求报文中存在语法错误。
  • 401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
  • 403 Forbidden :请求被拒绝。
  • 404 Not Found

5XX 服务器错误

  • 500 Internal Server Error :服务器正在执行请求时发生错误。
  • 503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

四、HTTP 首部

有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。

各种首部字段及其含义如下(不需要全记,仅供查阅):

通用首部字段

首部字段名 说明
Cache-Control 控制缓存的行为
Connection 控制不再转发给代理的首部字段、管理持久连接
Date 创建报文的日期时间
Pragma 报文指令
Trailer 报文末端的首部一览
Transfer-Encoding 指定报文主体的传输编码方式
Upgrade 升级为其他协议
Via 代理服务器的相关信息
Warning 错误通知

请求首部字段

首部字段名 说明
Accept 用户代理可处理的媒体类型
Accept-Charset 优先的字符集
Accept-Encoding 优先的内容编码
Accept-Language 优先的语言(自然语言)
Authorization Web 认证信息
Expect 期待服务器的特定行为
From 用户的电子邮箱地址
Host 请求资源所在服务器
If-Match 比较实体标记(ETag)
If-Modified-Since 比较资源的更新时间
If-None-Match 比较实体标记(与 If-Match 相反)
If-Range 资源未更新时发送实体 Byte 的范围请求
If-Unmodified-Since 比较资源的更新时间(与 If-Modified-Since 相反)
Max-Forwards 最大传输逐跳数
Proxy-Authorization 代理服务器要求客户端的认证信息
Range 实体的字节范围请求
Referer 对请求中 URI 的原始获取方
TE 传输编码的优先级
User-Agent HTTP 客户端程序的信息

响应首部字段

首部字段名 说明
Accept-Ranges 是否接受字节范围请求
Age 推算资源创建经过时间
ETag 资源的匹配信息
Location 令客户端重定向至指定 URI
Proxy-Authenticate 代理服务器对客户端的认证信息
Retry-After 对再次发起请求的时机要求
Server HTTP 服务器的安装信息
Vary 代理服务器缓存的管理信息
WWW-Authenticate 服务器对客户端的认证信息

实体首部字段

首部字段名 说明
Allow 资源可支持的 HTTP 方法
Content-Encoding 实体主体适用的编码方式
Content-Language 实体主体的自然语言
Content-Length 实体主体的大小
Content-Location 替代对应资源的 URI
Content-MD5 实体主体的报文摘要
Content-Range 实体主体的位置范围
Content-Type 实体主体的媒体类型
Expires 实体主体过期的日期时间
Last-Modified 资源的最后修改日期时间

设计模式专题之享元模式(七)

享元模式(Flyweight Pattern)

定义:享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。

以下情况可以使用享元模式:

  1. 有大量相似的对象, 占用了大量内存
  2. 对象中大部分状态可以抽离为外部状态

实例

某商家有 50 种男款内衣和 50 种款女款内衣, 要展示它们

方案一: 造 50 个塑料男模和 50 个塑料女模, 让他们穿上展示, 代码如下:

const Model = function(gender, underwear) {
  this.gender = gender
  this.underwear = underwear
}

Model.prototype.takephoto = function() {
  console.log(`${this.gender}穿着${this.underwear}`)
}

for (let i = 1; i < 51; i++) {
  const maleModel = new Model('male', `第${i}款衣服`)
  maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
  const female = new Model('female', `第${i}款衣服`)
  female.takephoto()
}

方案二: 造 1 个塑料男模特 1 个塑料女模特, 分别试穿 50 款内衣

const Model = function(gender) {
  this.gender = gender
}

Model.prototype.takephoto = function() {
  console.log(`${this.sex}穿着${this.underwear}`)
}

const maleModel = new Model('male')
const femaleModel = new Model('female')

for (let i = 1; i < 51; i++) {
  maleModel.underwear = `第${i}款衣服`
  maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
  femaleModel.underwear = `第${i}款衣服`
  femaleModel.takephoto()
}

对比发现: 方案一创建了 100 个对象, 方案二只创建了 2 个对象, 在该 demo 中, gender(性别) 是内部对象, underwear(穿着) 是外部对象。

当然在方案二的 demo 中, 还可以进一步改善:

  1. 一开始就通过构造函数显示地创建实例, 可用工场模式将其升级成可控生成
  2. 在实例上手动添加 underwear 不是很优雅, 可以在外部单独在写个 manager 函数
const Model = function(gender) {
  this.gender = gender
}

Model.prototype.takephoto = function() {
  console.log(`${this.gender}穿着${this.underwear}`)
}

const modelFactory = (function() { // 优化第一点
  const modelGender = {}
  return {
    createModel: function(gender) {
      if (modelGender[gender]) {
        return modelGender[gender]
      }
      return modelGender[gender] = new Model(gender)
    }
  }
}())

const modelManager = (function() {
  const modelObj = {}
  return {
    add: function(gender, i) {
      modelObj[i] = {
        underwear: `第${i}款衣服`
      }
      return modelFactory.createModel(gender)
    },
    copy: function(model, i) { // 优化第二点
      model.underwear = modelObj[i].underwear
    }
  }
}())

for (let i = 1; i < 51; i++) {
  const maleModel = modelManager.add('male', i)
  modelManager.copy(maleModel, i)
  maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
  const femaleModel = modelManager.add('female', i)
  modelManager.copy(femaleModel, i)
  femaleModel.takephoto()
}

Git总结

集中式与分布式

Git 属于分布式版本控制系统,而 SVN 属于集中式。

集中式:

central-repo

分布式:

distributed-repo

优缺点:

  • 集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。

  • 集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。

  • 集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。

  • 分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。

中心服务器

中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。

工作流

新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。

Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库,History 存储所有分支信息,使用一个 HEAD 指针指向当前分支。

img

  • git add files 把文件的修改添加到暂存区
  • git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
  • git reset -- files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files
  • git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改

img

可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。

  • git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
  • git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作

img

冲突

当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。

Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。

<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

Fast forward

"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。

可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。

$ git merge --no-ff -m "merge with no-ff" dev

分支管理策略

master 分支应该是非常稳定的,只用来发布新版本;

日常开发在开发分支 dev 上进行。

SSH 实现远程登陆

Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。

如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:

$ ssh-keygen -t rsa -C "[email protected]"

然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。

.gitignore 文件

忽略以下文件:

  • node_modules文件夹
  • 不需要上传到服务器的本地测试文件
  • 自己的敏感信息,比如存放口令的配置文件

不需要全部自己编写,可以到 https://github.com/github/gitignore 中进行查询。

git常用命令总结

三板斧

# 添加缓存区
git add . 
# 从缓存区加入本地当前分支
git commit -m "xxxx"
# 加入远程当前分支
git push

初始化及状态查询

# 初始化一个git项目
git init
# 查看状态
git status
git status -sb
# 查看修改
git diff
# 查看提交信息
git log

撤销指定 add

# 撤销指定文件的add
git rm --cached [file-name]
git reset HEAD  [file-name]
# 撤销全部add
git reset HEAD .

修改 commit 描述

# 修改commit描述
git commit -m --amend

版本回退

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致 commitid可以通过git log查看
git reset --hard [commitid]

远程合并

# 从远程库获取最新代码
git fetch 
# 与当前本地代码进行合拼
git merge
# 前两者的合拼操作,出现冲突使用前两者比较多
git pull

分支操作

# 创建分支
git branch b
# 切换分支
git checkout b
# 切换到主分支
git checkout master

JavaScript基础专题之原型与原型链(一)

构造函数创建对象

function Person(){

}
let person1 = new Person()
let person2 = new Person()
person1.name = 'james'
person2.name = 'kobe'


我们通过new来创建一个person实例,我们可以看到不同的实例拥有自己的属性。

proto

我们可以看到每个对象下都会有__proto__的属性,这个属性会指向该对象的原型

function Person(){

}
Person.prototype.name = 'chris'
let person = new Person()
let person1 = new Person()
let person2 = new Person()

person1.name = 'james'
person2.name = 'kobe'


我们看到__proto__下会出现prototype的name属性,那么__proto__prototype关系又是什么呢?

prototype

每个函数都有具有 prototype 属性,就是我们经常在用到的prototype

Person.prototype.name = 'chris'

那么问题来了,那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。

那原型是什么呢?可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型所谓的继承属性。

如图:

通过实例的__proto__和构造函数的prototype的对比,我们不难发现person 和 Person.prototype 的关系

person.__proto__ === Person.prototype  //true

如图:

既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

constructor

不难发现,每个构造函数都有 constructor这个属性, 通过控制台我们会发现constructor 属性指向关联的构造函数

constructor

指向自己实例.png

这样我们就了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系:

实例下的原型

我们知道如果读取不到实例的属性时,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

function Person() {

}

Person.prototype.name = 'chris';

var person = new Person();

person.name = 'james';
console.log(person.name) // james 拿到实例的name属性

delete person.name;
console.log(person.name) // chris 拿到原型的name属性

但是万一还没有读取到呢?原型下的原型又是什么呢?

原型下的原型

通过上面的知识我们知道person.__proto__ Person.protype相等,那么Person.prototype.__proto__下又是什么呢?很显然就是对象实例的__proto__

function Person(){}
let person = new Person()
let obj = new Object()
Person.prototype.__proto__ === obj.__proto__//true
Person.prototype.__proto__ === Object.prototype//true
obj.__proto__.__proto__ //null

let obj = new Object();
obj.__proto__.name = 'chris'
obj.name = 'Kevin'
console.log(obj.name) // Kevin
delete obj.name
console.log(obj.name) // chris

原型链

既然我们知道Object的原型,那 Object.prototype 的原型呢?

我们可以看到返回一个null,表达的就是已经没有原型了。

最终就是原型和原型链的结构

一些补充

  1. 关于Funtion中的原型
    我们可以会发现Function.prototype有些特殊
Function.prototype === Function.__proto__  //true

这样看上去实例的原型和原型的原型是相等的,即是鸡也是蛋。
我们可以参考MDN关于__proto__的解释:

__proto__的读取器(getter)暴露了一个对象的内部 [[Prototype]] 。对于使用对象字面量创建的对象,这个值是 Object.prototype。对于使用数组字面量创建的对象,这个值是 Array.prototype。对于functions,这个值是Function.prototype。对于使用 new fun 创建的对象,其中fun是由js提供的内建构造器函数之一(Array, Boolean, Date, Number, Object, String 等等),这个值总是fun.prototype。对于用js定义的其他js构造器函数创建的对象,这个值就是该构造器函数的prototype属性。

例子

Object.__proto__ === Function.prototype//true
Object.__proto__ === Function.__proto__ //true

引用冴羽的理解

至于为什么Function.__proto__ === Function.prototype,我认为有两种可能:一是为了保持与其他函数一致,二是就是表明一种关系而已。
简单的说,就是先有的Function,然后实现上把原型指向了Function.prototype,但是我们不能倒过来推测因为Function.__proto__ === Function.prototype,所以Function调用了自己生成了自己。

总结

  1. 实例对象的__proto__始终指向构造函数的prototype
  2. 只有构造函数才拥有prototype属性,对象(除了null)都拥有__proto__属性
  3. 每一个原型对象都有一个constructor属性指向它们的构造函数
  4. 要读取属性时,先读取实例上的属性,读取不到会在原型链上寻找相应属性
  5. 原型链按照__proto__的指向下一级对象
  6. 原型链的尽头始终是null
  7. 构造函数实例化以后,既是构造函数函数,也是对象
function Foo() {

}

const obj = new Foo()


Foo.prototype === obj.__proto__ //true
obj.constructor === Foo //true
Foo.prototype.__proto__ === Object.prototype //true
Object.prototype.__proto__ === null //true
Object.constructor === Function  //true

相关题目

实现一个 instanceof 方法

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
  var O = R.prototype;
  L = L.__proto__;
  while (true) { 
    if (L === null) 
      return false; 
    if (O === L)  // 这里重点:当 O 严格等于 L 时,返回 true 
      return true; 
    L = L.__proto__; 
 } 
}
console.log(instance_of([], Array)) // true
console.log(instance_of({}, Object)) // true

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.