Giter Club home page Giter Club logo

myblog's People

Contributors

2018212632 avatar

Stargazers

 avatar

Watchers

 avatar

myblog's Issues

高级树、AVL树和红黑树

高级树、AVL树和红黑树

通过next指针连接子结点的树形储存结构

链表是特殊化的树(树可以看作有很多next指针的链表)

树是特殊化的图(没有形成环的图)

树的中序遍历模板:

let res = []
const inOrder = (root) => {
    while(root) {
        root.left && inOrder(root.left)
        res.push(root.val)
        root.right && inOrder(root.right)
    }
}

二叉树

子结点只有两个的特殊的树

二叉搜索树

左结点小于根节点,右结点大于根节点,依次类推。

查询、删除和插入操作Time:O(log(n))

查询:按照左右子树的排序逻辑查找即可

插入:在查询的基础上,按左右子树的逻辑插入,如果相等根据需求来插入

删除:如果是叶子结点直接删除;如果是根节点,在右子树中找和该结点最接近的结点来替换这个根结点。

二叉搜索树极端情况,当所有结点往左边插入,树成了一个棍子,退化成了链表,为了保证性能即出现平衡二叉树,如:AVL、红黑树、2-3树...

平衡二叉树

关键点:

  1. 保证二维维度,
  2. 左右子树结点的平衡

AVL树

  • 保证每个结点的平衡因为在规定范围内,balance factor={0,-1,1}
  • 平衡因子:它的左子树的高度减去右子树的高度(有时候相反)
  • 通过旋转操作来进行平衡(四种):
    a.左旋:右右子树时,3个结点成一个棍子状
    b.右旋:左左子树时,3个结点成一个棍子状
    c.左右旋:左右子树,先左旋,然后右旋
    d.右左旋:右左子树,先右旋,然后左旋
    e.如果有子树的情况,需要考虑相应转换:可以看下图
    

AVL旋转情况

不足点:需要额外存储信息,且调整次数频繁

红黑树

红黑树是一种近似平衡二叉搜索树,它能够保证左右子树的高度的高度差小于两倍(大的高度最多比小的两倍)。满足以下特点:

  1. 每个结点要么是红色,要么是黑色。
  2. 根节点是黑色
  3. 每个叶子结点(NULL、空结点)是黑色
  4. 不能有两个相邻的红色结点
  5. 从任何一个结点到其每一个叶子结点的所有路径都包含相同数目的黑色结点。

总结(AVL与红黑树对比)

  1. 如果查询操作比较插入、删除多,那么使用AVL;因为AVL相对红黑树更加平衡。
  2. 如果插入、删除操作比查询多,那么使用红黑树;因为插入删除操作时,AVL需要更多的平衡操作,使用内存更多。
  3. 如果插入删除与查询操作55开,那么使用红黑树,因为红黑树实现相对简单。

js创建对象的多种模式以及优缺点

js创建对象的多种模式以及优缺点

工厂模式

虽然Object构造函数或对象字面量都可以用来创建单个对象;但是当使用同一个接口创建很多对象,会产生大量的重复代码。工厂模式是一种用函数来封装以特定接口创建对象的细节

function createPerson(name, age, job) {
    var o = new Object()
    o.name = name
    o.age = age
    o.job = job
    o.sayName = function() {
        console.log(this.a)
    }
    return o
}

var person1 = new createPerson('Tom', 20, 'software Engineer')

缺点:对象无法识别,所有实例都指向一个原型

构造函数模式

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

var person1 = new Person('Tom')

在构造函数中,将属性和方法赋值给了this对象

优点:实例可以识别为一个特定的类型

缺点:每次创建实例时,每个方法都要被创建一次

构造函数模式优化

function Person(name) {
    this.name = name
    this.getNmae = getName
}
function getName = function() {
    console.log(this.name)
}

var person1 = new Person('Tom')

优点:解决了同一个方法重复创建的缺点

缺点:将方法声明在全局,污染全局环境,对于封装性来说很不好

原型模式

function Person(name) {

}

Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();

优点:方法不会被重新创建
缺点:所有属性和方法共享;且不能初始化参数

原型模式优化

function Person(name) {

}

Person.prototype = {
    constructor: Person,
    name: 'kevin',
    getName: function () {
        console.log(this.name);
    }
}
var person1 = new Person()

优点:实例通过constructor找到构造函数
缺点:原型模拟的缺点依然存在

组合模式

构造函数模式和原型模式相结合,即现在new运算符运行的机制。

function Person(name) {
    this.name = name
}
Person.prototype = {
    constructor: Person,
    getName: function() {
        console.log(this.name)
    }
}

var person1 = new Person()

优点:该私有的私有,该共有的共有
缺点:有的人希望全部写在一起,封装性更好

动态原型

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);
            }
        }

        return new Person(name);
    }
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

person1.getName(); // kevin
person2.getName();  // daisy

寄生构造函数&稳妥构造函数模式

寄生构造函数

寄生构造函数模式,创建一个函数,该函数的作用仅仅是封装创建对象的代码。然后返回新创建的对象。

function Person(name) {

    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;

}

var person1 = new Person('kevin');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object)  // true

寄生构造函数其实本质上和工厂函数模式类似,所有实例扔指向同一个原型。

稳妥构造函数模式

function person(name){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    };
    return o;
}

var person1 = person('kevin');

person1.sayName(); // kevin

person1.name = "daisy";

person1.sayName(); // kevin

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

稳妥的意思在于没有公共属性,其方法也没有用this对象。即只能通过sayName来获取属性,因此这种模式非常适合在某些安全执行环境。

它于工厂模式的不同:

  1. 创建方法是不使用this
  2. 不使用new操作符调用构造函数

缺点:仍然无法识别对象所属类型

ES6之箭头函数

ES6之箭头函数

基础用法

let func = (arg1, arg2) => {
    return arg1 + arg2
}
// 上面写法等价于:
let func = function(arg1, arg2) {
    retrun arg1 + arg2
}

不同

箭头函数与普通函数的不同:

没有 this

箭头函数中没有this,所以需要查找作用域链来确定this的值;这就意味如果箭头函数被非箭头函数包含,箭头函数就绑定是最近一层非箭头函数的this值。

一个实际开发场景:需求是点击一个按钮,改变它的背景颜色

// HTML代码
<button id="button">点击变色</button>

// javascript代码
function Button(id) {
    this.element = document.querySelector("#" + id);
    this.bindEvent()
}

Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", this.setBgColor, false);
}

Button.prototype.setBgColor = function() {
    this.element.style.backgroundColor = '#1abc9c'
};

var button = new Button("button");

创建实例的时候会报错,说找不到style属性;的确,由于调用addEventListener事件监听函数,因此在setBgColor函数中this指向具体的元素 < div id='button'> < /div>;this.element 就是undefined了

解决方法:绑定传入事件函数的this,让this绑定到Button

  1. 使用ES5中bind函数:
Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", this.setBgColor.bind(this), false);
};
  1. 使用箭头函数:
Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", (event)=>{this.setBgColor(event)}, false)
}

由于箭头函数本身没有this,因此会指向外层非箭头函数,即指向了Button

注意:如果将this传递给call、bind、或者apply来调用箭头函数,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为null。

没有 arguments

在箭头函数内部直接访问arguments属性会报错,如果箭头函数嵌套在普通函数中,可以访问外层函数的arguments属性

function constant() {
    const h = ()=>{console.log(arguments[0])}
    h()
    return () => arguments[0]
}

var result = constant(1);
console.log(result());
// 1
// 1

可以通过命名参数或rest参数来获取箭头函数的参数:

const help = (...rest) => {console.log(rest)}

不能通过 new 关键字调用

因为箭头函数对于call方法,传参时会设置为null,因此new背后调用call方法,就无法通过new来调用箭头函数。

没有 new.target

由于无法使用new调用,也就没有new.target属性

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

没有原型和继承

由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

使用箭头函数立即执行

(function() {
    console.log('do somethings')
})()
// 用箭头函数转换
(
    ()=>{
        console.log('do somethings')
    }
)()
(function(){
    console.log('do somethings')
}())
// 当使用箭头函数替换时,会报错
(
    ()=>{
        console.log('do somethings')
    }()
)

箭头属于AssignmentExpressions,而第二种立即执行函数写法只支持MemberExpression或者CallExpression。

总结

箭头函数本身没有this,也没有arguments对象,不能通过new来调用;所以箭头函数不能作为构造函数使用,且适用于非方法函数。

方法函数:

A method is a function which is a property of an object.
对象属性中的函数,对象属性常用到this对象,而箭头函数又没有本身的this,因此箭头函数适合非方法函数。

js之函数柯里化

js之函数柯里化

什么是函数柯里化

柯里化:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

例子:

function add(a, b) {
    return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化

var addCurry = curry(add);
addCurry(1)(2) // 3

// 其中addCurry等效于下面
function addCurry(a) {
    return function(b) {
        return a+b
    }
}

函数柯里化的用途

  1. 降低一个函数参数通用性,提高适用性,即把相同的参数传入返回一个共同的方法,也就提高了适用性
// 示意而已
function ajax(type, url, data) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")

// 利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");

// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");
  1. 提高函数的复用性,我们可以把一个被柯里化的函数传给map,对于特定的功能,多次使用的,起到精简代码的作用,也能提高代码的可读性

例子:

// 假设我们有一个数据
var person = [{name: 'kevin'}, {name: 'daisy'}]

// 需求:我们要遍历对象的name属性

// 一般情况下
var name = person.map(function (item) {
    return item.name;
})

// 当使用柯里化之后,假定curry函数
var prop = curry(function (key, obj) {
    return obj[key]
});

var name = person.map(prop('name'))

可以发现,当函数经过柯里化之后,如果我们对遍历对象有三行代码简化到了一行;且prop函数可以复用。

柯里化函数的实现

实现功能:把一个普通函数柯里化,即让所需参数可以分步传入,当所需参数长度足够时,执行函数。

实现思路:根据形参的长度和实参的长度进行比较,如果实参小于形参的长度,说明参数还没有传完;反之,说明实参和形参相等,即执行那个普通函数即可

function curry(fn,args) {
    var length = fn.length
    var args = args || []

    return function() {
        var _args = args.slice(0)

        var arg,i
        for(i=0; i<arguments.length; i++) {
            arg = arguments[i]
            _args.push(arg)
        }

        if(_args.length < length) {
            return curry.call(this, fn, _args)
        } else{
            return fn.apply(this, _args)
        }
    }
}

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

ES6之Generator

ES6之Generator

Generator是什么

创建一个Generator函数 :

function* gen() {
    console.log('hello')
    let a = 1
    yield a
    a+=1
    yield a
    a+=1
    return a
}

let g = gen()
console.log(g) // gen {<suspended>}

let y1 = g.next()
console.log(y1) // 2 {value: 1, done: false}

let y2 = g.next()
console.log(y2) // 4 {value: 2, done: false}

let r = g.next()
console.log(r) // 7 {value: 3, done: true}

let f = g.next()
console.log(f) // 10 {value: undefined, done: true}

Generator与普通函数的区别:

  1. 创建时,在关键字function后加 * 号
  2. 创建实例后,函数被没有立即执行,返回的是一个指向函数内部的指针对象,一个遍历器对象(Iterator Object)。
  3. 每次调用next方法,会执行到yield语句,并纪录执行到的位置,返回的对象,有done属性,表示函数内部是否执行结束;vaule值是yield或者return语句返回的值
  4. 如果done为ture,继续执行next方法,那么一直返回的value是undefined。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

Generator的特性

yield的特点

yield与return的不同:

  1. yield只能写在Generator函数中,否则报错;next运行的逻辑,yield在Generator就是暂停标志,遇见yield就暂停后面的操作,并返回yield后面的表达式的值,作为对象的value值。再次执行next时,往下找,直到return,表示函数结束,如果函数没有return值,则返回的对象的value时undefined。
  2. 一个函数中可以有个yield,但不能有多个return
  3. yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,
function* gen() {
  yield  123 + 456;
}

上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。

next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
console.log(a.next()) // Object{value:6, done:false}
console.log(a.next()) // Object{value:NaN, done:false}
console.log(a.next()) // Object{value:NaN, done:true}

var b = foo(5);
console.log(b.next()) // { value:6, done:false }
console.log(b.next(12)) // { value:8, done:false }
console.log(b.next(13)) // { value:42, done:true }

Generator函数第一个next只是作为函数启动来用的,如果带参数,是被v8引擎忽略。因此通过next()的参数,我们是可以在外部向Generator输入值的。

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。这样很麻烦。ES6 提供了yield*表达式来解决这个问题:

function* gen() {
    yield '1'
    yield* foo()
    yield '2'
}

function* foo() {
    yield 'a'
    yield 'b'
    return 'c'
}

for(const i of gen()) {
    console.log(i)
}

// 等价于下面写法

function* gen() {
    yield '1'
    for(const i of foo()) {
        console.log(i)
    }
    yield '2'
}

function* foo() {
    yield 'a'
    yield 'b'
}

for(const i of gen()) {
    console.log(i)
}

// 1 -> a -> b -> 2

如果有return返回值,需要用值来接收这个值 :

function* gen() {
    yield '1'
    let c = yield* foo()
    yield c
}

function* foo() {
    yield 'a'
    yield 'b'
    return 'c'
}

for(const i of gen()) {
    console.log(i)
}

// 1 -> a -> b -> b

Generator的this

由于ES6规定Generator返回的是可遍历对象,因此Generator自然不能作为构造函数使用,也不能使用new操作符。

如果要让Generator函数返回一个对象的实例,既可以用next,又可以获得正常的this:

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

通过将gen.prototype的this指向gen,当gen的实例next方法执行后,实例也有了this的属性值;如果f没有执行next,那么f.a仍然是undefined。

Generator应用场景

将异步操作同步化表达

Generator 可以暂停函数执行,返回任意表达式的值。因此我们可以把异步操作放在 yiled 的后面作为表达式,而异步函数之后的回调函数放在 yiled 的下面:

function showLoadingScreen(){
    console.log('showLoadingScreen')
}

function loadUIDataAsynchronously(){
    setTimeout(()=>{console.log('loadUIDataAsynchronously')},1000)
}

function hideLoadingScreen(){
    console.log('hideLoadingScreen')
}

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();

loader.next()
//  showLoadingScreen
//  loadUIDataAsynchronously

loader.next()
// hideLoadingScreen

可以发现loadUI函数初始化的时候并没有执行,当执行第一个next时,执行 showLoadingScreen, 遇到 yield 执行后面的表达式 loadUIDataAsynchronously;暂停函数,当执行第二个 next 时,执行 hideLoadingScreen 这个回调。

控制复杂回调

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

如果用promise来改写:

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。(针对的是一种同步的写法)

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

Generator的自动执行

当Genreator中yield表达式中有异步操作,我们是不能直接next来调用的,因为这样无法保证异步完成再进行下一步的操作。

由此可以看到 Generator 函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权。

而两种方法可以做到这一点:

  1. 回调函数。将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权。

  2. Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

实现Generator的逻辑:

  1. 返回一个promise对象,来捕获异常,或者获取成功值
  2. 对于回调函数的异步操作,将其转换成promise对象
  3. 初始化g指向Generator,返回一个promise对象,执行next函数,对于结果进行捕获,如果异常,那么promise状态设为reject;如果成功把状态设置为resloved;递归调用next,直到结果的done属性为ture。并对每次一的value进行promise转换,确保递归可以用then来交回执行权。
// Generator处理多个异步操作部分
var fetch = require('node-fetch');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var r2 = yield fetch('https://api.github.com/users/github/followers');
    var r3 = yield fetch('https://api.github.com/users/github/repos');

    console.log([r1.bio, r2[0].login, r3[0].full_name].join('\n'));
}

// Generator自动运行函数 run
function run(gen) {
    var gen = gen();

    return new Promise(function(resolve, reject) {

        function next(data) {
            try {
                var result = gen.next(data);
            } catch (e) {
                return reject(e);
            }

            if (result.done) {
                return resolve(result.value)
            };

            var value = toPromise(result.value);

            value.then(function(data) {
                next(data);
            }, function(e) {
                reject(e)
            });
        }

        next()
    })

}

function isPromise(obj) {
    return 'function' == typeof obj.then;
}

function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if ('function' == typeof obj) return thunkToPromise(obj);
    return obj;
}

function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
        fn(function(err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

run(gen)

而 co 是什么? co 是大神 TJ Holowaychuk 于 2013 年 6 月发布的一个小模块,用于 Generator 函数的自动执行。

// yield 后是一个 Promise
var fetch = require('node-fetch');
var co = require('co');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var json1 = yield r1.json();
    var r2 = yield fetch('https://api.github.com/users/github/followers');
    var json2 = yield r2.json();
    var r3 = yield fetch('https://api.github.com/users/github/repos');
    var json3 = yield r3.json();

    console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n'));
}

co(gen);
// yield 后是一个回调函数
var co = require('co');

function fetchData(url) {
    return function(cb) {
        setTimeout(function() {
            cb(null, { status: 200, data: url })
        }, 1000)
    }
}

function* gen() {
    var r1 = yield fetchData('https://api.github.com/users/github');
    var r2 = yield fetchData('https://api.github.com/users/github/followers');

    console.log([r1.data, r2.data].join('\n'));
}

co(gen);

js中new的模拟实现

js中new的模拟实现

new

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

function animal() {
    this.name = 'cat'
    this.age = 3
}

animal.prototype.speed = 'fast'

animal.prototype.say = function() {
    console.log(this.name, ':miao miao')
}

var xiaoHua = new animal()
console.log(xiaoHua.age) // 3
console.log(xiaoHua.speed) // 'fast'
xiaoHua.say() // cat:miao miao

可以发现,xiaoHua这个实例继承了构造函数的属性,也继承了构造函数原型的属性。

模拟实现new

步骤:

  1. 创建一个空对象obj
  2. 拿到创建的构造函数constructor,并让obj.__proto__ == constructor.prototype
  3. 要让obj能访问constructor的属性,即constructor.apply(obj, arguments)

注意点:

如果构造函数有返回值,返回的是对象,那么实例只能访问这个对象的属性;如果返回的基本类型,那么访问的仍然是构造函数的属性

function imiate_new() {
    var obj = new Object()
    // arguments参数会因为shift去掉第一个,shift会改变原数组
    Constructor = Array.prototype.shift.call(arguments)
    obj.__proto__ = Constructor.prototype

    var ret = Constructor.apply(obj, arguments)
    return typeof obj === 'object' ? ret : obj
}

总结

new运算符创建的实例,可以访问构造函数的属性,也可以访问构造函数原型的属性;如果构造函数有返回值,当返回值为对象时,实例访问的这个对象的属性,如果为基本类型,实例访问的仍是构造函数的属性。

React15源码解析

React15源码解析

说明

本文对于组件的是什么,以及组件的挂载,组件的初始化部分源码来源于React17.0.1版本;对于生命周期参考资料是React15版本.

React是什么

React主要用于构建UI,很多人认为 React 是 MVC 中的 V(视图);React本质上只处理DOM的更新和响应事件;

React以渲染函数为基础。这些函数读入当前的状态,将其转换为目标页面上的一个虚拟表现。只要React被告知状态有变化,他就会重新运行这些函数,计算出页面的一个新的虚拟表现,接着自动把结果转换成必要的DOM更新来反映新的表现。

React怎么支持jsx的

用babel来编译即可:

{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

组件是什么

React中组件的本质就是对象。从源码中发现Component的代码:

function Component(props, context, updater) {
  this.props = props;
  this.context = context; // If a component has string refs, we will assign a different object later.

  this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the
  // renderer.

  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype上有两个方法,setState和forceUpdate(被弃用)


Component.prototype.setState = function (partialState, callback) {
  if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
    {
      throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
    }
  }

  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

从注释代码中,可以看出:

  • 参数:未来要改变的state ,state被更新后的回调
  • setState并不保证它是同步的,因为它们最终可能被分批放在一起,可以提供回调让它在setState真正完成后执行。

为什么setState是异步呢?合成事件会先执行onClick中的setState,但是并不会马上进行渲染,所以新的state只存在于Fiber节点的updateQueue中,并不会马上赋值到组件的state中。

组件的初始化

声明A后,我们可以在其内部自定义方法,也可以使用生命周期的方法,如ComponentDidMount等等,这些和我们在写"类"的时候是完全一样的。唯一不同的是组件类必须拥有render方法输出类似<\div>{this.state.name}</div>的结构并挂载到真实DOM上,才能触发组件的生命周期并成为DOM树的一部分。

我们有一个组件App:

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'tom'}
  }

  componentDidMount() {
    
  }

  render() {
    return (
      <div>{this.state.name}</div>
    )
  }
}

class再bable转换之前(es2015-loose)类的写法:

var App = /*#__PURE__*/function (_React$Component) {
  _inheritsLoose(App, _React$Component);

  function App(props) {
    var _this;

    _this = _React$Component.call(this, props) || this;
    _this.state = {
      name: 'tom'
    };
    return _this;
  }

  var _proto = App.prototype;

  _proto.componentDidMount = function componentDidMount() {};

  _proto.render = function render() {
    return /*#__PURE__*/React.createElement("div", null, this.state.name);
  };

  return App;
}(React.Component);

构造函数本质上是一个普通函数,创建了_this,然后让ReactComponent调用call方法指向当前App函数返回给_this,_this会保留ReactCOmponent返回element对象的属性,之后让App的原型指向render函数,如果有生命周期函数,就绑定生命周期的函数。

怎么让setState马上执行,并不需要经过合成事件去处理呢?加个setTImeout试试!但是不建议,React这么做是有原因的,因为防止多次setState触发多次的render导致性能减低,所以我们的setState都应该保持在生命周期内或者合成事件内

可以发现,render方法本质上调用了createElement这个方法:

function createElement(type, config, children) {
 // ...
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

在看到RectElement这个方法:

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,
    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,
    // Record the component responsible for creating this element.
    _owner: owner
  };

  return element;
};

可以发现一个React组件有一下属性的对象:

  • type:组件的标识类型
  • key:DOM结构标识,提升update性能
  • props:子结构相关信息和组件属性
  • ref:真实DOM的引用

组件的挂载

我们知道可以通过ReactDOM.render(component,mountNode)的形式对自定义组件/原生DOM/字符串进行挂载,那么挂载的过程又是如何实现的呢?

function render(element, container, callback) {
    ...
  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
    ...
  var root = container._reactRootContainer;
  var fiberRoot;

  if (!root) {
    // Initial mount
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    fiberRoot = root._internalRoot;

    if (typeof callback === 'function') {
      var originalCallback = callback;

      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    } // Initial mount should not be batched.


    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;

    if (typeof callback === 'function') {
      var _originalCallback = callback;

      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);

        _originalCallback.call(instance);
      };
    } // Update


    updateContainer(children, fiberRoot, parentComponent, callback);
  }

  return getPublicRootInstance(fiberRoot);
}

在初次渲染时,采用的是unbatchedUpdates并且,parentElement给的是null,因为首次渲染需要尽快完成。

在react-dom中不论是setstate还是render都会造成React更新,都会生成一个update对象,并且会赋值给Fiber.updateQueue,然后调用scheduleUpdateOnFiber。不论怎样,创建的component对象都会挂载到真实的dom上。

组件的生命周期

不论是setstate还是render函数,都会造成React更新;

首次render是直接渲染,并不会比对虚拟树也进行批处理;可以根据Component.prototype.isReactComponent来判断是类组件还是函数组件,如果是函数组件,会执ReactDOMComponent类型的组件渲染,并不会有生命周期,而是根据节点信息直接解析出html并挂载到真实dom上。如果是类组件,那么会执行ReactCompositeComponent的组件渲染,随后处理props和state并执行componentWillMount,然后执行render,解析出HTML并挂载到真实dom,执行componentDidMount。

setstate函数在对当前组件进行判断是否在队列中,如果不存在,那么创建空队列并将state放入队列,如果存在,那么将state放入之前这个对象创建的队列中;然后判断batchingStrategy.isBatchingUpdates是否true,初始值为false,如果为true把组件加入dirtyComponents,随后调用遍历dirtyComponents数组又通过事务的形式调用runBatchedUpdates方法,该方法执行的是一是通过执行updateComponent方法来更新组件,二是若setState方法传入了回调函数则将回调函数存入callbackQueue队列。

在updateComponent前会先执行componentWillReceiveProps,在执行_processPendingState 该函数主要对state进行处理:

  1. 如果更新队列为null,那么返回原来的state;
  2. 如果更新队列有一个更新,那么返回更新值;
  3. 如果更新队列有多个更新,那么通过for循环将它们合并;

之后执行componentShouldUpdate。这说明在 componentShouldUpdate 所有的state会被合并处理。

最后如果 componentShouldUpdate 为true,执行_performComponentUpdate方法,该方法中执行componentWillUpdate生命周期方法,更新完成后执行componentDidUpdate方法。

整个流程:

  1. 获取旧的组件信息
  2. 获取新的组件信息
  3. shouldUpdateReactComponent 是一个方法(下面简称should函数),根据传入的新旧组件信息判断是否进行更新。
  4. should 函数返回true,执行旧组件的更新。
  5. should 函数返回false,执行旧组件的卸载和新组件的挂载。

React事件系统

React实现了SyntheticEvent层处理事件,合成事件会将所有我们在jsx中编写的事件进行拦截,并进行一些封装变成一个React的事件,最终只会绑定一个事件到document元素中,通过事件冒泡的方式传递到绑定到document的统一事件进行分发。

总结:

  • 组件中声明的事件并不会保存起来,不像Fiber架构之前会存起来,而仅仅是将事件类型以及 dispatchEvent / dispatchInteractiveEvent 函数绑定到document元素上,实现事件委派;其中如果有同类事件,会将同类事件纪录在 alreadyListeningTo 中,避免重复绑定。

需要注意的是,绑定事件之前会通过 isInteractiveTopLevelEventType 函数检测当前事件类型是否React支持的事件类型,如果当前的事件并不是React配置中所处理的事件,那么将会直接绑定 dispatchEvent ,否则绑定的将会是 dispatchInteractiveEvent 。区别在于dispatchEvent 不会异步setState,而 dispatchInteractiveEvent 会异步 setState。

  • 在触发阶段,通过事件的触发 dispatchEvent/dispatchInteractiveEvent(前者不会异步setState),找到事件源对象上的对应事件的回调函数,并组合成一个"react-事件名"的自定义事件,通过创建一个 react 元素,通过 element.dispatchEvent 函数自主触发事件。

  • 在触发阶段,如果父级元素绑定了同样事件名的函数,那么会冒泡一层一层触发。可以在子组件中使用 event.stopPropagation() 来阻止父组件的触发。

virtual dom

在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,结果仅仅是轻量级的 JavaScript 对象,我们称之为 virtual DOM。

虚拟 DOM 是 React 的一大亮点,具有 batching(批处理) 和高效的 Diff 算法。

Diff 算法:

传统的diff算法通过循环递归的方式对比新老树来更新dom树,需要的时间复杂度是O(n^3)

ReactV16.8中利用其特殊的diff算法做到了O(n^3)到O(n)提升,依赖于下面这三条的diff策略:

  • Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。(tree diff)
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。(component diff)
  • 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。(element diff)

tree diff

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

component diff

  • 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树即可。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,可通过props的浅比较shouldComponentUpdate()来判断是否变了。但是如果调用了forceUpdate方法,shouldComponentUpdate则失效。

因此当我们对于一些修改比较小的同类型组件,可以使用pureComponent来提高性能优化,pureComponent本质上使用了shuldComponentUpdate来对前后的state和props进行浅比较,如果没有变化那么返回false,即不更新;如果发生变化即返回true,按原策略继续比较虚拟DOM树。

element diff:

当节点处于同一层级,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP (插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)。

如果两颗树的同一层仅仅是位置不对,但diff的时候是对比的相同位置的节点,就会认为节点不同而选择删除并创建新的。

React针对这一现象提出了一种优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分。

通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。

为什么传统的diff时间复杂度是O(n^3),现在是怎么降到O(n)的?

传统Diff算法需要找到两个树的最小更新方式,所以需要[两两]对比每个叶子节点是否相同,对比就需要O(n^2)次了,再加上更新(移动、创建、删除)时需要遍历一次,所以是O(n^3)。

现在diff算法,大体上采用的是同层级比较,不同层级删除或创建,即同一个父节点下的dom进行比较,如果节点不存在,会将其节点和其所有子节点删除;对于一个节点如果其有许多children,采用component diff ,如果这个节点为同一类型的组件,可以通过shouldComponentUpdate来对前后的state和props进行浅比较,如果false则不更新,如果为true那么采用原策略递归比较;对同一层级的多个children采用 element diff,通过加key的操作,让react知道哪些节点是新增的,这样这一层只需要采用移动或删除或插入即可。整体上对整个树只遍历了一次,因此采用新策略的diff算法时间复杂度为O(n)。

setstate的batching(批处理)

首先我们可以使用一个队列存更新函数,另一个队列存组件,存组件的队列判断一下,不能重复就行了。 我们需要确定一个合适的时间来吧队列的更新函数执行了,这个时间可以等到本次事件循环的宏任务执行完成的时候,也就是代码基本跑了一遍,该setState的地方都setState了,于是再微任务阶段调用就满足这个条件,于是可以使用Promise.resolve().then(fn) fn可以用一个循环去取所有的setState函数出来合并计算出最终的state,然后去render

一个例子:

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      anything: '初始化'
    }  
  }
  
  render() {
    console.log('outer', this.state.anything)
    return (
      <div>
        <button
          onClick={() => {
            this.setState({
              anything:'同步1'
            })
            this.setState({
              anything:'同步2'
            })
            Promise.resolve().then(()=>{
              this.setState({
                anything:'微任务1'
              })
            })
            Promise.resolve().then(()=>{
              this.setState({
                anything:'微任务2'
              })
            })
            setTimeout(()=>{
              this.setState({
                anything: 'after 宏任务1',
              })
            },1000)
            setTimeout(()=>{
              this.setState({
                anything: 'after 宏任务2',
              })
            },1000)
          }}
        >
          {this.state.anything}
        </button>
      </div>
    )
  }
}

页面上显示的UI效果,可以在codesanbox.io在线演示。可以发现,起初this.state.anything在UI界面是初始化的文字,当我们点击按钮之后,同步操作到微任务promise.then这段时间的setState函数被批量处理,界面UI先显示 '微任务2',表示真实的dom只是更新了这段时间的state的一个融合。之后微任务到宏任务setTimeout这段时间的setState的函数被融合处理。

总结

在React15版本,也就是没有fiber的版本React操作dom的大致流程为:

  • 首次加载组件,即使用 createReactElement 创建携带 DOM 节点的信息,但本质上是js对象;通过 ReactDom.render 之后,会触发组件的挂载,根据组件的类型,来执行生命周期函数;首次加载时,并没有进行虚拟dom的diff和batch批处理,而是直接渲染虚拟dom,得到html渲染到真实的节点上。
  • render 和 setstate 会更新React 元素,之后是调和阶段(Reconciler)即通过虚拟DOM对比,找到需要更新的元素(patch),放入队列采取批处理机制;随之渲染阶段(Renderer),实际更新渲染对应元素

参考链接:

leetcdoe剑指 Offer 10- I. 斐波那契数列

leetcdoe剑指 Offer 10- I. 斐波那契数列

题目链接

思路

  1. 傻递归,根据Fibonacci数列的规律,f(n) = f(n-1) + f(n-2).time:O(n^2) space:O(n)
  2. 循环,从正向模拟整个递归过程,这样可以优化时间复杂度,通过公式发现只有三个变量使用,可以通过滑动的方式优化空间复杂度。time:O(n) space:O(1)
// 循环的方式
var fib = function(n) {
    if(n==0) return 0
    if(n==1) return 1

    // loop
    let fn1 = 0
    let fn2 = 1
    let fn3 
    for(let i=2; i<=n; i++) {
        fn3 = (fn1 + fn2) % 1000000007
        fn1 = fn2
        fn2 = fn3
    }
    return fn2 
};

总结

正常的傻递归时间复杂度通常是O(2^n),因为有很多重复计算的结点,因此可以通过记忆化优化,通过另外的数组存在已经算过的结点,如果发现计算了直接使用。大多数递归都可以都过自顶向上的循环来改写。

leetcode剑指 Offer 11. 旋转数组的最小数字

leetcode剑指 Offer 11. 旋转数组的最小数字

题目链接

思路

  1. 暴力法,先将数组排序,返回第一个元素。time:O(n^2)
  2. 双下标法,i,j,j表示当前最小元素的下标,如果num[i] < num[j] 替换j。time:O(n)
  3. 二分法,旋转数组,有两个有序子区间,必然有一个区间所有元素大于另外一个区间所有元素。time:O(log(n))
  • left,right,mid分别表示左,右,中间,下标;
  • 如果num[mid] > num[right],最小元素肯定在mid右边,left=mid+1;
  • 如果num[mid] == num[right],则无法区分确定最小在那个子区间,因此right= right - 1;
  • 如果num[mid] < num[right],最小元素肯定在mid的左边,right = mid(因为要找最小元素,因此right不能等于mid-1)
var minArray = function(numbers) {

    let left = 0
    let right = numbers.length - 1
    let mid
    while(left < right) {
        mid = ~~((left + right) / 2)
        if(numbers[mid] > numbers[right]) {
            left = mid + 1
        } else if(numbers[mid] == numbers[left]) {
            right = right - 1
        } else {
            right = mid 
        }

    }
    // 因为mid没有及时更新,最后left==right,返回最小也就时返回numbers[left]
    return numbers[left] 
};

总结

测试结果基于leetcdoe,JavaScript语言环境

  1. praseInt()取整速度没有 ~~ (双取反)快
  2. js中,(left + right) >> 1 也没有(left + right) / 2快,由于js做位运算时,会类型转换导致位运算效率并没有太大提升。
  3. 在一些有序,或者有规律的数组,常常可以用二分查找进行优化

Leetcode剑指Offer03-数组中重复的数字

要求:找出数组任意重复的数字

思路

  1. 暴力法:两层循环 time:O(n^2) space:O(n)
  2. hash表:一次循环,判断 map 中是否存在nums[i],存在即返回nums[i],否则存进去。time:O(n) space:O(n)

总结

  1. 数组类题目,要能熟练手写 for 循环
 for(let i=0; i<nums.length; i++) { 
     for(let j=0; j<nums.length; j++) {
   }
}

  1. 时间复杂度优化大都通过空间换时间来实现

leetcode链接

LRUCache的实现及应用

LRUCache的实现及应用

特点

Last Recently Use Cache:

  • 两个要素,大小(缓存本身容量)、替换策略(最近使用更新,不常使用的往后面调)
  • Hash Table + Double LinkedList
  • 查询:O(1);修改,更新:O(1)

工作原理

)ZW%8B_G WH 2K `B5 8I

js中基于map来实现LRU;map本身有两个特性:

  1. 如果先delete(key),再set(key, value);那么key会添加到map的尾部
  2. map.keys().next().value,会拿到map第一个key;
  3. 如果把map的尾部作为最近使用的元素,满足LRU的原理

LRU.get:

  • 如果不存再key,直接返回-1;如果存在,那么先delete,后set,把位置置后

LRU.put:

  • 如果已经存在key,先delete,再set;如果不存在,直接set;
  • 如果map.size()大于cache的容量,map.delete(map.keys().next().value)

题目链接

var LRUCache = function(capacity) {
    this.cache = new Map()
    this.capacity = capacity
};

LRUCache.prototype.get = function(key) {
    const cache = this.cache
    if(!cache.has(key)) return -1

    const value = cache.get(key)
    cache.delete(key)
    cache.set(key, value)

    return value
};

LRUCache.prototype.put = function(key, value) {
    const cache = this.cache
    if(cache.has(key)) cache.delete(key)
    cache.set(key, value)
    if(cache.size > this.capacity) cache.delete(cache.keys().next().value)
};

总结

LRU缓存常用于优化计算,把最近使用作为可能尝使用作缓存来减少计算量。

js模拟实现bind

js模拟实现bind

bind

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

总结bind两个特点:

  1. 返回一个函数,函数的this为传入的第一个参数
  2. 可以接收多个参数,从第二参数开始的参数会在函数实参之前传入

bind的模拟

思路:

  1. 获取多个调用bind2,传的参数,args;以及调用返回的函数调用的参数bindArgs
  2. 如果函数有返回,因此通过return self.apply 来模拟
  3. 如果返回的函数作为构造函数,来创建实例,那么绑定的this会失效;并不会指向window也不会指向之前的绑定的函数。
Function.prototype.bind2 = function(context) {
    // 如果调用bind2不是函数,抛出错误
    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 tempF = function () {}

    var fBound = function() {
        var bindArgs = Array.prototype.slice.call(arguments)
        // 如果作为构造函数时,this指向实例,将绑定的函数指向该函数,可以让实例获得来自绑定函数的值
        //如果作为普通函数时,this指向window,将指定的函数指向context
        return self.apply(this instanceof tempF ? this : context, args.concat(bindArgs))
    }

    tempF.prototype = this.prototype
    // 修改返回函数的原型为绑定函数的原型,实例可以继承绑定函数原型的值;这里用了tempF来作中转,避免直接修改fBound.prototype时,也会修改绑定函数
    fBound.prototype = new tempF()

    return fBound
}

总结

如果A.bind(foo)返回函数,作为普通函数调用时,this指向绑定的函数(foo)。如果作为构造函数使用时,指向A,并且实例会继承A的原型的属性。

js继承的多种方式及缺点

js继承的多种方式及缺点

原型链继承

function Parent() {
    this.name = 'tom'
}

parent.protoype.sayName = function() {
    console.log(this.name)
}

function child() {

}

child.prototype = new Parent()

var child1 = new child()  

优点:child1这个实例可以继承Parent所有属性和方法

缺点:所有实例共享这个属性,创建child实例时不能向Parent传参

借用构造函数

基本**:在子类型构造函数的内部调用父类型构造函数

function Parent() {
    this.names = ['tom', 'JieNi']
    this.sayName = function() {
        console.log(this.names)
    }
}

function child() {
    Parent.call(this)
}

var child1 = new child()
child1.names.push('Pika')
console.log(child1.names) // ['tom', 'JieNi', 'Pika']

var child2 = new child()
console.log(child2.names) // ['tom', 'JieNi']

优点:解决了实例共享引用类型的属性,且可以向Parent传参

缺点:每次创建实例,构造函数中的方法会被重复创建

组合模式

构造函数模式和原型链模式相结合,使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。

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

Parent.prototype.sayName = 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('Tom', 18)
child1.colors.push('black')
console.log(child1.name) // Tom
console.log(child1.age) // 18
console.log(child1.colors) // ["green", "yellow", "red", "black"]
child1.sayName() // Tom

var child2 = new child('JieNi', 20)
console.log(child2.name) // JieNi
console.log(child2.age) // 20
console.log(child2.colors) // ["green", "yellow", "red"]
child2.sayName() // JieNi

优点:实例既不会共享属性,也不会重复创建方法

原型式继承

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
var person = {
    name: 'kevin',
    friends: ['daisy', 'kelly']
}

var person1 = createObj(person);
var person2 = createObj(person);

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

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

从本质上说,createObj对对象实现的是浅拷贝,复制的引用类型的指针,因此创建的实例和原型链模式有一样的问题,实例共享属性。

寄生式继承

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

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

缺点:和构造函数模式一样,会重复创建方法

寄生组合式继承

组合模式的不足,在于不论什么情况下,都会两次调用父构造函数;一次在子构造函数中,另一次是在创建子类型的原型的时候。

而寄生函数在组合模式基础上避免两次调用父构造函数;把之前 child.prototype = new Parent() 中new背后实现的一步,Parent.call()去掉

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;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


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

console.log(child1);

经过封装之后的代码:

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

function inherit(child, parent) {
    var prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = child
}
// 需要继承时
inherit(child, parent)

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

ES6之WeakMap

ES6之WeakMap

WeakMap是什么

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的

WeakMap的特性

WeakMap只接收对象作为键名

let m = new WeakMap()
m.set({}, 'tom')

m.set(1, 'tom')// Invalid value used as weak map key
m.set(null, 'tom') // Invalid value used as weak map key

WeakMap的键名所引用的对象是弱引用

弱引用:

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

let key = new Array(1024*1024*5)
let wm = new WeakMap()
wm.set(key, 1)

weakMap 实现的效果是,wm对key这个对象是弱引用,而key这个对象对Array(1024*1024*5)是强引用,当我们令key=null时,垃圾回收机制会清除wm对key的引用,释放内存;如果使用的是map的话,map中键名是强引用,从而不会回收,如果先令key=null,此时map.delete(key)并不会清除Array(1024*1024*5),因为删除的是key=null的引用,这部分内存就是因为操作不当而造成内存泄漏。

weakMap帮我们简化了清除内存这部分的操作逻辑,一旦我们不需使用某个对象,wm会自动清除这部分内存。

WeakMap的应用场景

在DOM节点上存储信息

当我们需要在DOM上绑定某个按钮的id信息,如果节点被删除,我们希望相应的信息也被清除:

var element = documnet.getElementById('button')

let wm = new WeakMap()
wm.set(element, 'red button')

wm.get(element) // red button
element.parentNode.removeChild(element);

element = null;

数据缓存

如果我们希望不修改对象的基础上,来绑定一些数据,但是又不想关心数据是否被清除的时候:

const cache = new WeakMap();
function countOwnKeys(obj) {
    if (cache.has(obj)) {
        console.log('Cached');
        return cache.get(obj);
    } else {
        console.log('Computed');
        const count = Object.keys(obj).length;
        cache.set(obj, count);
        return count;
    }
}

这意味,如果缓存的对象本身被清除,那么这部分缓存也就失效,变得无法查询。

私有属性

由于WeakMap本身是不可遍历的,也就可以用来实现私有属性:

const privateData = new WeakMap();

class Person {
    constructor(name, age) {
        privateData.set(this, { name: name, age: age });
    }

    getName() {
        return privateData.get(this).name;
    }

    getAge() {
        return privateData.get(this).age;
    }
}

export default Person;

即通过WeakMap把键名设为class的内部this,那么在class外部只要不能得到this值,也就无法访问class的属性。

leetcode剑指 Offer 12. 矩阵中的路径

leetcode剑指 Offer 12. 矩阵中的路径

题目链接

思路

  1. 确定dfs+剪枝,普通dfs,从某一节点出去,如果找到word就返回true,否则false
  2. 剪枝,首先对于i,j 边界本身的限定;另外通过word单词顺序来,如果一开始就不符合单词顺序那么直接剪掉;对于用过的格子用单词外的符号替换,因此剪纸之后需要恢复之前的状态。
  3. 时间复杂度:O(m*n3^k) 从任意格子开始,m,n是board的长宽;3^k次方表示最坏情况下dfs递归的深度。空间复杂度:O(mn),递归最深不会超过 m*n的深度。
var exist = function(board, word) {

    let m = board.length
    let n = board[0].length
    const directions = [
        [0, 1],
        [0, -1],
        [1, 0],
        [-1, 0]
    ]

    const dfs = (i, j, k) => {
        if(board[i][j] != word[k]) return false
        if(k == word.length-1) return true
        // *表示这个格子已经用过了
        board[i][j] = '*'
        // 左、右、上、下都可以走
        for(const [dx, dy] of directions) {
            const newX = i + dx
            const newY = j + dy
            // 左右上下边界情况
            if(newX>=0 && newX<m && newY>=0 && newY<n) {
                if(dfs(newX, newY, k+1)) return true
            }
        }
        // reserve
        board[i][j] = word[k]
        return false
    }

    for(let p=0; p<m; p++) {
        for(let q=0; q<n; q++) {
            if(dfs(p, q, 0)) {
                return true
            }
        }
    }

    return false
};

总结

路径问题,常用dfs或bfs进行求解,题目本身的约束条件是剪纸重要的依据。

ES6之Promise

ES6之Promise

promise是什么

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise简单的来说是一个对象,里面保存的是一个未来才会结束的事件(通常是一个异步操作)的结果。比如:我们在向服务器发起一个请求的时候,对请求处理的一些逻辑放在promise中,请求结果就是未来才会结束的事件结果。

Promise基本用法

ES6中规定,Promise是一个构造函数,用来创建promise实例。

构造函数接收两个参数resolve和reject。它们是两个内置函数:

  1. resolve:将 promise 对象的状态从 pending 变为 resolved 状态。可以接收一个参数,这个参数会在then方法后的第一个回调函数作为参数获取。
  2. reject:将 promise 对象的状态从 pending 变为 rejected 状态。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve(['arg1','arg2']);
});

promise.then(function(...args) {
  console.log('resolved.',args);
});

如果resolve传递参数是一个promise对象时,会等待这个对象的状态执行:

let p1 = new Promise((resolve, reject) => {
    setTimeout(()=> reject(new Error('some error')), 2000)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(()=> resolve(p1), 1000)
})

p2
.then((result) => {console.log(result)})
.catch((error) => {console.log(error)})
// Error: some error

上面例子说明,p2的状态由p1决定,p2 reslove p1之后,本身状态等待p1,p1在2秒后由pending变为rejected,从而p2也就是成了rejected,就能catch到错误。

如果resolve传递的promise对象是thenable(即带有"then"方法),会“跟随”这个thenable的对象,采用它的最终状态:

let p1 = new Promise((resolve, reject) => {
    setTimeout(()=> reject(new Error('some error')), 2000)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(()=> resolve(p1), 1000)
})

p1.then((value) => {},(error)=>{console.log('p1',error)})

p2
.then((result) => {console.log(result)})
.catch((error) => {console.log(error)})
// p1  Error: some error
//  Error: some error

promise链式调用

约定:

  • 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

链式调用:连续执行两个或者多个异步操作,即在上一个操作的结束之后,带着它的结果执行下一个操作,通过创建Promise链来实现;then()函数会返回一个和原来不同的新promise

let p1 = new Promise((resolve, reject) => {
    resolve('p1')
})

let p2 = p1.then((value)=>{console.log(value)})

console.log(p2 instanceof Promise) // true
console.log(p1 === p2) // false

链式调用解决回调地狱问题:

function addone(x) {
    console.log('addone parmas',x)
    return x+1
}

function addtwo(x) {
    console.log('addtwo parmas',x)
    return x+2
}    


let p1 = new Promise((resolve, reject) => {
    resolve(1)
})

p1
.then(result => addone(result))
.then(newResult => addtwo(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch((error)=>{console.log(error)});
// parmas 1
// addtwo parmas 2
// Got the final result: 4

promise中执行顺序

下面代码输出什么?

setTimeout(function(){console.log('timer1')}, 0)

requestAnimationFrame(function(){
    console.log('requestAnimationFrame')   
})

setInterval(() => {
    console.log('setInterval')
}, 5000);

setTimeout(function(){ console.log('timer2' )}, 0)


new Promise(function executor(resolve) {
    console.log('promise 1')
    resolve()
    console.log('promise2')
}).then(function(){
    console.log('promise then')
})

console.log('end')

结果1 promise1 -> promise2 -> end -> pormise then -> requestAnimationFrame -> timer -> timer2 -> setInterval

结果2 promise1 -> promise2 -> end -> pormise then -> timer -> timer2 -> requestAnimationFrame -> setInterval

分析:

  1. js是单线程的,因此异步操作就很重要,如果没有异步操作,那么光是一个网络请求就会卡死进程
  2. 但是异步当中添加到任务队列也是有先后顺序的,其中微任务执行顺序是先于宏任务的;其中 promise .then就是作为微任务添加的,
    setTimeout是宏任务
  3. 在创建Promise的时候会立即执行,及pending阶段是同步的,resolve(),rejected()是改变promise的状态从pending到fulfilled或rejected,
    promise .then()第一个参数是 resolved 状态下的回调,第二个参数是 rejected 状态下的回调
  4. requestAnimationFrame 浏览器支持的宏任务,告诉浏览器再下一次重绘前执行;对于浏览器的重绘。浏览器的表现是不确定的,
    有时候会先于setTimeout,有时候会在setTimeout之后
  5. 个人理解:尽管同为异步,但根据异步的功能定位,需要消耗的性能,需要持续占用内存,或者 I/O 密集,优先级会低一些。
    比如在点菜之后需要加一杯饮料,那么加饮料可以定义为微任务

promise应用场景

解决回调地狱

回调地狱的导致了什么问题?

  1. 回调函数的逻辑和顺序确定之后,函数是难以复用的
  2. 堆栈信息为被断开,如果回调函数是异步操作,回调函数并没有进入可执行栈,而是进入一个任务队列,等待主线程执行完毕,再进栈。此时栈中只有这一个上下文,如果此时回调报错,是无法获取该异步操作时栈中的信息。
  3. 借助外层变量,当进行多个异步操作时,如果不借助外部变量,是很难处理多个异步操作之间的顺序,如果使用外部变量,这个变量也可能被同一作用域的其它函数访问。

因此,回调函数不仅导致了代码可读性下降,也涉及到其它的问题。

promise怎么来解决的问题:

  1. 通过promise链式调用,很容易能解决了回调嵌套的问题,保证了代码可读性。
  2. 不再需要借助外部变量来控制多个异步操作之间的逻辑,因为promise只有异步完成后,才会进入回调。

控制反转再反转

我们再使用第三方API的时候可能会遇到以下问题:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行,有时异步执行

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

promise反模式

  1. 避免嵌套promise
// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
    DoSomethingOnThem(...[something, another]);
});

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

  1. 断开的 Promise 链
// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}
// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}
  1. catch
// bad
somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});

// good
somethingAsync()
.then(function() {
    return somethingElseAsync();
})
.catch(function(err) {
    handleMyError(err);
});

红绿灯问题

题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)

三个亮灯函数已存在:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

用then和递归来实现:

var light = function(timer, cb) {
    return new Promise((resolve, reject) => {
        setTimeout(function(){
            cb()
            resolve()
        }, timer)
    })
}

var step = function() {
    return Promise.resolve()
                        .then(() => light(3000, red))
                        .then(() => light(2000, green))
                        .then(() => light(1000, yellow))
                        .then(() => { step() })
}

step()

promise的局限性

错误被吃掉

错误被吃掉,即在promise内部的抛出的异常并不会阻断外部代码

let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

promise内部抛出的错误,但是外部代码仍会执行。

而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

单一值

Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。

建议使用ES6的解构赋值:

Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) => {
    console.log(x, y);
});

无法取消

Promise 一旦新建它就会立即执行,无法中途取消。

无法得知 pending 状态

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

工程化规范格式化

工程化规范格式化

单元测试

为什么需要单元测试:

  1. js本身是动态语言,缺少类型检测,编译期间无法定位到错误
  2. 保证代码正确性,自动化测试,不用每一次都console
  3. 如果是API文档类,那么测试样例更有利于API的理解
  4. 保证重构后代码的质量

常用的测试框架:

  1. Mocha:支持node环境和l浏览器环境
  2. Karma:基于node.js,可以与编辑器配合使用,监控文件如果改动即开始检测,在命令行中显示测试结果
  3. Travis.CI:绑定github上代码,如果有新代码,就拉取并提供一个运行环境,执行测试,完成构建,还能部署到服务器。
  4. jest: 配置简单;沙箱和快速,虚化了js的环境模拟浏览器执行;快照测试,Jest能够对React 树进行快照或别的序列化数值快速编写测试,提供快速更新的用户体验;支持异步代码测试:支持promises和async/await。

参考资料:

commit-msg

合作开发时,为了防止一些不规范的代码commit并push到远端;可以在git命令执行前用一些狗子来检测并阻止。目前前端流行的git钩子插件:huskypre-commit

一段husky的配置代码:

"husky": {
    "hooks": {
      "pre-commit": "npm run lint", // 在git commit之前执行 "npm run lint"
      "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS" // 检测提交信息的规范
    }
  }

使用:我们可以在提交前,执行test或者eslint的规范,来让提交的代码规范化。

commit-msg的具体配置

git-cz

用于规范commit说明的工具。

良好的提交信息应该有:

  1. 提供更多的历史信息,方便快速浏览
  2. 可以过滤某些commit,便于筛选代码review
  3. 可以追踪commit生成更新日志
  4. 可以关联issues

git-cz时,一些type的意思参考地址:https://juejin.im/post/6844903831893966856

eslint

ESLint 是一个开源的 JavaScript 代码检查工具,由 Nicholas C. Zakas 于2013年6月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。

有两种主要的方式来配置 ESLint:

  1. Configuration Comments - 使用 JavaScript 注释把配置信息直接嵌入到一个代码源文件中。
  2. Configuration Files - 使用 JavaScript、JSON 或者 YAML 文件为整个目录(处理你的主目录)和它的子目录指定配置信息。可以配置一个独立的 .eslintrc.* 文件,或者直接在 package.json 文件里的 eslintConfig 字段指定配置,ESLint 会查找和自动读取它们,再者,你可以在命令行运行时指定一个任意的配置文件。

配置可参考:

js浮点数精度

js浮点数精度

在JavaScript中0.1+0.2 == 0.3?不是的,0.1 + 0.2 != 0.3 ,原因是浮点数在贮存的过程精度丢失导致的。

浮点数的储存

js中浮点数采用IEEE754双精度储存,用64个bit来存贮浮点数。其中对于浮点先转换成二进制数,对二进制数进行存储。

在这个标准中:

  1. 用1个bit来存储sign,0表示正数,1表示负数
  2. 用11个bit来存储指数部分,E+bias,对bias来说,bias=2^(11-1)-1 = 1023
  3. 用52个bit来存储小数部分,Fraction

比如0.1它的二进制为 1 * 1.1001100110011…… * 2^-4,其中sign为0,E+bias=-4+1023=1019,1019转换成二进制1111111011,Fractin为1.1001100110011……

对应64个bit完整表示为:

0 01111111011 1001100110011001100110011001100110011001100110011010

同理0.2:

0 01111111100 1001100110011001100110011001100110011001100110011010

所以当0.1存储时,就已经发生了精度丢失,当使用浮点运算时,使用的是精度丢失后的数.

浮点数的运算

浮点数的运算:对阶,尾数运算,规格化,舍入处理,溢出判断

01+0.2的过程:首先是对阶,所谓对阶,就是把阶码调整为相同,0.1的二进制为1.1001100110011…… * 2^-4, 0.2的二进制为1.10011001100110...* 2^-3 ,把小阶化大阶0.1 的-4改成-3,即 0.11001100110011…… * 2^-3, 再进行尾数计算:

  0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
 10.0110011001100110011001100110011001100110011001100111

得到结果规格化: 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2

括号中的1为溢出位,要舍入处理,如果为1,就进1,如果为0,就舍弃.结果为:
1.0011001100110011001100110011001100110011001100110100 * 2^-2

最终64bit存储结果为:
0 01111111101 0011001100110011001100110011001100110011001100110100

将它转换为10进制数就得到 0.30000000000000004440892098500626

因此浮点数运算时,对于二进制长度大于52位的小数,不仅存储精度丢失了一次,而且运算也丢失了一次精度,最终导致 0.1 + 0.2 != 0.3

二进制相关方法

// 十进制转二进制
parseFloat(0.1).toString(2);
=> "0.0001100110011001100110011001100110011001100110011001101"

// 二进制转十进制
parseInt(1100100,2)
=> 100

// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=> "0.300000000000000044409"
(0.3).toPrecision(21)
=> "0.299999999999999988898"

leetcode剑指 Offer 15. 二进制中1的个数

leetcode剑指 Offer 15. 二进制中1的个数

题目链接

思路

  1. 暴力法:整数32位bits,循环32次,用1来&n,然后右移,计算1的次数 time:32次
  2. x&(x-1),把x最低位的1清零,有多少个1就执行多少次

代码

// 暴力法
var hammingWeight = function(n) {
    let mask = 1
    let res = 0
    for(let i=0; i<32; i++) {
        // js 进行&运算时,要用(),如果mask & n这么写,结果不正确
        if((mask&n) != 0) {
            res++
        }
        n >>= 1
    }
    return res
};
// 优化版
var hammingWeight = function(n) {
    let res = 0
    while(n!=0) {
        n = n & (n-1)
        res++
    }
    return res
};

总结

MDN对运算符优先级的描述,逻辑非优先级高于逻辑与,因此对于暴力法中mask&n,不加括号,得到的预期不符。

leetcode剑指 Offer 06

leetcode题目

思路

遍历链表,利用栈先进后出的**,模拟存贮进一个数组。time:O(n) space:O(n)

array.unshift

unshift()方法将元素往左添加进数组,并返回该数组的新长度(该方法会修改原有的数组)

代码

var reversePrint = function(head) {
    let a = []
    while(head != null) {
        a.unshift(head.val)
        head = head.next
    }
    return a
};

总结

链表的读取操作复杂度是O(n)的,因为链表只能通过遍历一遍才能找到访问的元素。删除和插入操作是O(1)的复杂度。

位运算

位运算

目录

  • 位运算符
  • 算数移位和逻辑移位
  • 位运算的应用

位运算符

  1. 左移(<<)、右移(>>);把二进制数整体往左(右)移动一位
  2. 按位或 |;按位与 &;
  3. 按位取反 ~ ;按位异或 ^

实战位运算要点

  1. 奇偶判断:

    x%2 == 1 <--> (x&1) == 1

    x%2 == 0 <--> (x&1) == 0

  2. x>>1 <--> x/2;常用于二分查找时,位运算计算更快

  3. x = x&(x-1)将x最低位的1清零

  4. x&(-x)得到最低位的1;负数的二级制以补码形式存在,原码、取反、加1(0001)。

  5. x&(~x) --> 0

js中的位运算

  1. js中并没有整数、浮点数类型,只有Number;因此js进行位运算时,会将操作数转换成32位比特序列,操作完成后,再按照64位浮点数存储。
  2. js做位运算时,小数部分会全部丢弃;因此~~x,双取反也就是取整数操作。
  3. js中非Number类型进行位运算时,所有类型会被转换成二进制0
  4. js进行无符号右移时,不管正、负数都会首先将符号位替换成0,再进行移位;即无符号右移时,永远返回正整数。

不建议在 js 中使用位运算,理由是 js 天生就会进行类型转换,使得效率降低。为了保持可读性,尽量避免使用复杂的位运算。

题目一

思路

  1. 循环,无符号整数32bits,即循环32次,用一个位掩码1来与上n,判断1出现的次数。time:O(1),至少需要执行32次
  2. 位运算,x&(x-1),将最低位清零,每次改变n的值,只要n!=0,继续清零最低位的1,每次将结果加1.time:O(1),有多少个1就执行多少次。
// 1.暴力法:把整数n转换成二进制数,遍历然后统计1的个数 O(32)
// 2.x&(x-1)每次清零最低为的1,判断x是否为0,
var hammingWeight = function(n) {
    let res = 0
    while(n!=0) {
        n = n & (n-1)
        res++
    }
    
    return res
};

补充:判断一个数是否为2的幂次方,即判断该数二进制是否为一个1。

quetion: how to express negative number binary in js ?

题目二

思路

  1. 一个整数可以转换成2^0+2^1+...+2^n,循环32次,每次n右一位,用while来实现颠倒
var reverseBits = function(n) {
    let res = 0
    let count = 32

    while(count--) {
        res *= 2 // 可以用 res << 1 替换
        res += n&1
        n>>=1
    }
    return res
};
  1. 循环32次,每次n右移,result左移,如果n&1为1,那么res+=1.
var reverseBits = function(n) {
    if (n === 0) return 0;

    var result = 0;
    for (var i = 0; i < 32; i ++) {
        result <<= 1;
        if (n & 1 === 1) {
            result += 1;
        }
        
        n >>= 1;
    }
    return result>>>0; // 在js中,用>>>0把结果转换成无符号整数
};

题目三

思路

dfs + bit来实现,在回溯的**基础上,使用位运算来判断皇后放置位置是否合法。

可以通过n=4的情况下,取理解流程,更为理解整个**。

bits的定义,列,撇,捺或再取反,表示竖,撇,捺方向上1就是可以放皇后;1右移n再减1,可以获得n个1(二进制)。

bits&-bits,从最左边拿1放置皇后,注入下一层递归,每次递归列或p,捺或p左移,捺或p右移。

bits&(bits-1),清零最低位的1,每一层放置了一个皇后,因此把最低位置为0.

代码

var totalNQueens = function(n) {
    let res = 0

    // n,row,col,ld,rd 表示n行,当前第几行,列,撇,捺是否有皇后
    const dfs = (n, row, col, ld, rd) => {
        if(row >= n) {
            res++
            return
        }
        // bits代表当前行可以放置皇后的位置,二进制中的1代表可以放置皇后
        let bits = ~(col | ld | rd) & ((1 << n) - 1)
        while(bits > 0) {
            // 拿到bits最低的1
            const p = bits & -bits
            dfs(n, row+1, col | p, (ld | p) << 1, (rd | p) >> 1)
            bits &= bits - 1
        }
    }
    dfs(n, 0, 0, 0, 0)
    return res
};

ES6之 let 和 const

let 和 const

let: 用于定义变量,与var不同点在于,let是块级作用域,不存在变量提升的问题

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

上面代码中,变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

块级作用域

在没有let之前,我们经常会遇见这样一个问题:

var arr = []
for(var i = 0; i<10; i++) {    
    arr[i] = function() {
            console.log(i)    
        }
    }
arr[3]() // 预期值是3,实际上是10

arr[3]() 打印的 3 的原因就是因为var声明的i是全局范围的作用域,以至于i每次循环的时候覆盖了前面的值,最后输出的时候 i 已经变成 10 了。

解决:

  1. 闭包:通过嵌套函数的方式,让函数内可以访问局部变量,且局部变量的内存不被释放,会造成内存泄漏的问题。
var arr = []
for(var i=0; i<10; i++) {
    (function(val) {
        arr[i] = function() {
            console.log(i)
        }
    })(i)
}
arr[3]()
  1. ES6 let: 每次生成的 i 只在本轮循环有效,因此最后输出是3;可以发现 let 不仅优雅、简洁的解决了作用域的问题,也避免了内存泄漏的可能。
var arr = []
for(let i = 0; i<10; i++) {    
    arr[i] = function() {
            console.log(i)    
        }
    }
arr[3]() 

const:const声明一个只读的常量。一旦声明,常量的值就不能改变。const的作用域与let命令相同:只在声明所在的块级作用域内有效。

const

本质:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

babel怎样转换

  1. 全局情况下
// ES6
let a = 1

// after babel
"use strict";

var a = 1; 

  1. 需要块级作用域时,通过改变变量名来实现
// ES6
if (false) {
    let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined

// after babel
"use strict";

if (false) {
  var _value = 1;
}

console.log(value); // Uncaught ReferenceError: value is not defined
  1. 循环的时候,通过函数来划出作用域
// ES6
var arr = []
for(let i=0; i<10; i++) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()

// after babel
"use strict";

var arr = [];

var _loop = function _loop(i) {
  arr[i] = function () {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}

arr[0]();

let和const作用

  • 不会绑定全局作用域

  • 块级作用域(解决之前闭包来写块级作用域的痛点)

  • 暂时性死区(不会被变量提升)

  • 重复声明报错(约束代码规范,避免重复命名差错)

leetcdoe剑指 Offer 05

leetcode题目

剑指 Offer 05. 替换空格

思路

  1. 创建一个 s.length 长度的字符串数组,如果 s[i] 是空格,此处替换为 %20 time:O(n) space:O(n)
  2. 使用str.split 方法,array.join()两个方法 time:O(n) space:O(n)

str.split

split()方法将字符串按指定的分割符将字符串切割成子字符串数组。

array.join

join()方法以指定分隔符将数组成员拼接成以该分隔符连接的字符串。

代码

var replaceSpace = function(s) {
      if (typeof s == "string" && s.length >= 0 && s.length <= 10000) {
        return s.split(' ').join('%20');
      }
      return '';
};

总结

读于字符串的操作,不能直接 for 循环修改 s[i] 的值,通常转换成数组最后转换字符串操作。

js之惰性函数

js之惰性函数

什么是惰性函数以及作用

惰性载入表示函数执行的分支仅会发生一次。

实现惰性函数的方法有两种,一种是在执行分支的时候覆盖函数;另一种使用函数表达式,在声明函数的时候就给每个分支指定适当的函数。

// 方式一
function f() {
    if(typeof XMLHttpRequest != 'undefined') {
        f = function() {
            // do something
        }
    } else if(typeof ActiveXObject != 'undefined') {
        f = function() {
            // do somethins
        }
    } else {
        f = function() {
            throw new Error('some error')
        }
    }
    return f()
}

// 方式二
var f = (function() {
    if(typeof XMLHttpRequest != 'undefined') {
        return function() {
            // do something
        }
    } else if(typeof ActiveXObject != 'undefined') {
        return function() {
            // do somethins
        }
    } else {
        return function() {
            throw new Error('some error')
        }
    }
})()

惰性函数让函数检测浏览器对XMLHttpRequest的支持只需要执行一次。惰性函数的优点在于只在执行分支代码时牺牲一点性能,但是能过避免执行不必要的代码。

js执行上下文栈

js执行上下文栈

可执行代码

js的可执行代码包含:全局代码、函数代码、eval()

  • 全局代码:比如加载外部文件的js代码、本地标签内的代码;不含function内的代码
  • 函数代码:函数体内部的代码
  • eval(): 该函数会将传入的字符串当做 JavaScript 代码进行执行。

可执行上文栈

JavaScript引擎在处理这些可执行代码顺序时,创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

当执行一段js代码时,最先遇见的全局代码,这个执行上下文栈会压入一个全局的上下文,当所有可执行代码结束后,相应的上下文会被释放,因此程序结束之前,栈内最底部永远存在一个全局的上下文。

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

首先,js引擎创建一个ECS,执行fun1()时,会将fun1压进栈,发现执行fun2,会将fun2压进栈,执行fun2,继续压入fun3,fun3执行结束后并从栈中弹出,执行fun2并弹出,执行fun1弹出。

剑指 Offer 07. 重建二叉树

leetcode链接

思路

  1. 二叉树中序(LVR),前序(VLR)根节点的遍历顺序,找出根节点preoder[0]
  2. 找到根节点在中序遍历结果中的下标,得到左右子树个数,递归左右子树

代码

var buildTree = function(preorder, inorder) {
    if(!preorder.length || !inorder.length) return null

    const recursion = (left, right) =>{
        if(left > right) return null

        const val = preorder.shift()
        const root = new TreeNode(val)
        // 只有left>right,那么说明左(右)子树还有相应结点
        root.left = recursion(left, inorder.indexOf(val) - 1)
        root.right = recursion(inorder.indexOf(val) + 1, right)

        return root
    }

    return recursion(0, inorder.length-1)
};

总结

根据二叉树中序、前序遍历顺序找到根节点,左右子树的个数,由递归的**重构二叉树;

ES6之模板字符串

ES6之模板字符串

基础用法

let obj ={name:'jack',age:'20'};
// 字符串拼接
console.log('我的名字'+obj.name+'年龄是'+obj.age);
// ES6的模板字符串
console.log(`es6   我的名字${obj.name},年龄是${obj.age}`);
  1. 使用反斜杠转义 ‘/’
  2. 空格、缩进、换行会被保留,可以使用 trim 函数消除
  3. 变量嵌套使用 ${} ,可以嵌套字符串模板

标签模板

定义

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

let a = 5
let b = 10
tag`five${a} add ten${b}`
tag = function(strArray, value1, value2) {
    console.log(strArray)
    console.log(value1)
    console.log(value2)
}
// console result
strArray -- ["five", " add ten", ""] 
value1 -- 5
value2 -- 10

它第一个参数strArray以模板字符串中除变量的字符串组成的数组;第二个参数是第一个变量,第三个参数是第二个变量,依次类推,可以得到模板字符串所有变量值。

怎么将参数拼合回去

根据参数的含义,将字符串拼接出来:

function tag(literals, ...values) {
    let result = ''
    for(let i=0; i<values.length; i++) {
        result += literals[i]
        result += values[i]
    }
    
    result += literals[literals.length-1]

    return result
}

简化版本:

function tag(literals, ...values) {
    let result = literals.reduce((acc, curr, i) => {
        let value = values[i-1]
        return acc + value + curr
    })
}

数组的 reduce 函数:对数组的每一个成员使用自己定义 reducer(回调函数)

reducer()的参数:

  • accumulator 累计器
  • currentValue 当前值
  • currentIndex 当前索引
  • array 调用的源数组
// no initialValue
arr.reduce(callback(acc, curr, i, sourceArray) => {
    // do some things
    return 
})

// initialValue
arr.reduce(callback(acc, curr, i, sourceArray) => {
    // do some things
    return 
}, 10)

// 当有初始值的时候,累计器初始化是这个值,当前值从array[0]开始,索引从0开始
// 没有初始值的时候,累计器初始化的值为array[0],当前值从array[1]开始,索引从1开始

实际需求

  1. 过滤 HTML 字符串,防止用户输入恶意内容。
let sender = '<script>alert("abc")</script>'; // 恶意代码

let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
    let s = templateData[0];

    for (let i = 1; i < arguments.length; i++) {
    
        let arg = String(arguments[i]);
    
        // Escape special characters in the substitution.
        s += arg.replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
    
        // Don't escape special characters in the template.
        s += templateData[i];
    }

    return s;
}
// after deal, message is:
"<p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>"
  1. 多语言转换(国际化处理)
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
// "欢迎访问xxx,您是第xxxx位访问者!"

// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
let libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
  1. oneLine

出于可读性或者其他原因,我希望书写的时候是换行的,但是最终输出的字符是在一行,这就需要借助模板标签来实现了,我们尝试写一个这样的函数:

let message = oneLine `
    Hi,
    Daisy!
    I am
    Kevin.
`;
console.log(message); // Hi, Daisy! I am Kevin.

function oneLine(arrStr, ...values) {
    let result = arrStr.reduce((acc, curr, i) => {
        let value = values[i-1]
        return acc + value + curr
    })

    result = result.replace(/(\n\s*)/g, " ")
    result = result.trim()

    return result
}

原理:匹配拼接回的字符串换行符前面多个空格以及后面多个空格,然后替换成一个空格

总结

标签模板让我们可以通过函数的形式对字符串进行处理,通过结合正则表达式根据我们的具体需求封装成一个 utils 库进行使用。

js深浅拷贝以及类数组对象

js深浅拷贝以及类数组对象

类数组对象

有数组特征的对象,它有length属性;常见的类数组对象arguments、getElementsByTagName

let a = {'0':1, 'length':1}
a.push() // 抛出错误

可以发现类数组对象并没有数组的原型上的方法,比如push、slice、concat

怎么转换一个类数组对象:

  1. Array.form(arrayLike, callback) 第二个参数为可选,回调函数,每个新数组成员会调用这个函数
  2. Array.prototype.slice.call(arrayLike)
  3. for循环,将arguments的属性赋值给一个新数组
  4. 使用ES6的 ... 运算符

以上四种方法中性能排行:

  1. 利用 ES6 中的 rest 参数转换性能最好
  2. 其次使用 for 循环方式转换
  3. [].slice 的方式性能较弱
  4. 最差的就是用 Array.from 进行转换

arguments的一些特点

  1. length属性,代表的是实参的长度;函数的length,代表形参的长度
  2. 在非严格模式下,如果传入参数时,形参和arguments的值保持共享,如果没有传入,形参的值与arguments不保持共享;在严格模式下,形参的值与arguments不保持共享。
  3. 箭头函数中不存在arguments对象

浅拷贝

浅拷贝常用的方法,concat()、slice()会返回一个新数组,实现拷贝

let a = ['old', 1, null]
let new_a = a.concat()
let new_b = a.slice()

如果数组嵌套了对象或者数组:

let a = [{'old': 'old'}, [1]]
let new_a = a.concat()

new_a[1][0] = 2

console.log(a) // [{'old': 'old'}, [2]]
console.log(new_a) // [{'old': 'old'}, [2]]

可以发现,改变新数组,同样会改变原数组的值,克隆并不彻底。

如果数组元素是基本元素类型,那么拷贝一份,互不影响;如果是对象或数组,就只会拷贝对象和数组的引用,无论修改新旧数组任何一个,另一个也会被影响。

因此concat()、slice()是一种浅拷贝

深拷贝

深拷贝,拷贝的新数组,如果数组元素有对象或者数组,不在拷贝它的引用,而直接拷贝它的值;最后不论新旧数组任何一个改变都不会改变另一个的值。

常用的技巧,争对数组中有数组或对象的深拷贝,不能拷贝函数

let a = [1, null, [1, 2], {'name': 'tom'}]
let new_a = JSON.parse(JSON.stringify(a))
console.log(new_a)

如果数组中有函数,会转换成null

JSON.stringify方法对undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function()

深拷贝的实现:

如果传入参数是对象,递归调用这个对象给新数组赋值。

function deepCopy (obj) {
    if(typeof obj !== 'object') return
    var new_arr = Array.isArray(obj) ? [] : {}

    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
            if(obj[key]==null) {
                new_arr[key] = null
            } else{
                new_arr[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
            }
        }
    }
    return new_arr
}

总结

深拷贝虽然不会产生副作用,但是因为递归实现会比较耗性能;因此在实际开发过程中,尽量避免数组出现对象或数组,让数组拷贝为浅拷贝。

js变量对象

js变量对象

当执行一段JavaScript代码时,会创建相应的可执行上下文。对于每个上下文都有三个重要属性:

  • 变量对象
  • 作用域链
  • this

变量对象

变量对象是与可执行上下文相关的数据作用域,储存了在上下文定义的变量和函数声明。

因为不同上下文的变量对象稍有不同,大致分为全局上下文的变量对象和函数上下文的变量对象

全局上下文

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

// 都能生效
console.log(Math.random());
console.log(this.Math.random());

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

// 在客户端中,this引用就是Windows对象
console.log(this);

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

this.parseInt == parseInt // true

函数上下文

在函数上下文中,我们用活动对象(activation object),来表示变量对象。

活动对象是在进入函数上下文时被创建,它通过函数的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);

首先进入代码上下文,此时的活动变量对象:

OA = {
    arguments: {
        0: 1, // 如果参数超过2个以上,那么超过的参数会以callee的方式存储
        length: 1 // 表示参数的个数
    },
    a: 1,
    b: undenfined,
    c: reference to function c(){},
    d: undenfined
}

然后按顺序执行代码,根据代码修改活动变量的值,执行之后的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. 在执行代码阶段,相应代码执行会再次修改变量对象属性值

排序

排序

比较类排序和非比较类排序

比较类排序:通过比较来确定元素次序,常常对对象、类、字符串之类根据关系来进行排序,由于其时间复杂度不能突破O(nlog(n)),因此也称作非线性时间比较类排序

非比较类排序:不通过比较来决定元素间相对次序,常常对数字整形进行比较,通过额外的空间辅助,常常能达到线性的时间排序,因此也称为线性时间非比较类排序。

排序算法的分类

排序:比较类和非比较类

比较类:

  1. 交换排序

    a. 冒泡排序

    b. 快速排序

  2. 插入排序

    a.简单插入排序

    b.希尔排序

  3. 选择排序

    a.简单选择排序

    b.堆排序

  4. 归并排序

    a.二路归并排序

    b.多路归并排序

非比较类:

  1. 计数排序
  2. 桶排序
  3. 基数排序

初级排序

初级排序包含了选择排序、插入排序、冒泡排序,他们时间复杂度都是O(n^2)

高级排序

快速排序,归并排序,它们时间复杂度都是O:n(log(n))的

快速排序思路:找一个标杆位置pivot,要求pivot左边的元素小于pivot这个位置的元素;
大于pivot这个位置的元素放在pivot的右边;即对pivot左子数组和右子数组分别排序即可,直到整个数组有序。

怎么找到标杆位置:随便初始化pivot位置,假设pivot为最后一个元素;使用一个计数器counter = begin;循环begin到end,如果arr[i] < arr[pivot],那么对a[i]和arr[counter]做一次交换,counter++;循环结束,对arr[counter]和arr[pivot]做交换,即有counter个元素小于pivot,pivot的位置应该在counter这个位置。

// 快排代码模板
/**
* @param {array[]} arr
* @param {Number} begin
* @param {Number} end
* @return void
*/
function quicksort(arr, begin, end){
    if(begin >= end) return
    let pivot = partition(arr, begin, end)
    quicksort(arr, begin, pivot-1)
    quicksort(arr, pivot+1, end)
}

// partition:找出标杆位置
function partition(arr, begin, end) {
    // pivot标杆位置从end开始,counter表示小于pivot位置的元素个数
    let pivot = end, counter = begin
    for(let i=begin; i<end; i++) {
        if(arr[i] < arr[pivot]) {
            // swap:即把小于pivot的值全部防止到0~counter-1的位置
            let temp = arr[counter]
            arr[counter] = arr[i]
            arr[i] = temp
            counter++
        }
    }
    // swap:把pivot换到该到的位置arr[counter]
    let temp = arr[pivot]
    arr[pivot] = arr[counter]
    arr[counter] = temp

    return counter
}

归并排序思路:

  1. 把长度为n的输入子序列分成两个长度为n/2的子序列
  2. 对这个两个子序列分别采用归并排序
  3. 把两个排序好的子序列合并成一个最终的排序序列

代码模板

function mergeSort(arr, left, right) {
    if(left >= right) return
    let mid = ~~((right+left)/2)

    mergeSort(arr, left, mid)
    mergeSort(arr, mid+1, right)
    merge(arr, left, mid, right)
}

function merge(arr, left, mid, right) {
    let temp = [] // 长度为right-left+1
    let i=left, j=mid+1, k=0 // k表示temp中目前有多少个成员

    while(i<=mid && j<=right) {
        temp[k++] = arr[i] >= arr[j] ? arr[j++] : arr[i++]
    }
    while(i<=mid) temp[k++] = arr[i++]
    while(j<=right) temp[k++] = arr[j++]

    //把排序好的temp给arr
    for(let p=0; p<temp.length; p++) {
        arr[left+p] = temp[p]
    }
}

堆排序: 利用堆的性质,如果小顶堆,那么最小的元素在上面;即把arr的元素放入堆,把顶部元素依次pop出来,就得到了排序之后的数组。

js原型和原型链

js原型和原型链

什么是原型

prototype

只有函数拥有prototype这个属性,prototype属性指向一个对象,这个对象是用这个函数创建实例的原型。例子:

function animal () {

}
animal.prototype.age = 'ten'
let dog = new animal()
let cat = new animal()
console.log(dog.name) // console result --> ten
console.log(cat.name) // console result --> ten
console.log(dog.prototype) // console result --> undefined

对于上面例子来说,只有animal()这个构造函数有prototype属性,而dog这个实例没有;animal()的prototype指向的对象就是animal创建的实例(dog、cat)的原型;实例会从原型继承属性。

__proto__

每一个JavaScript对象都有一个_proto_属性,这个属性指向对象的原型,即:

function animal() {

}
let dog = new animal()
console.log(dog.__proto__ === animal.prototype) // console result --> true

这说明实例对象和构造函数都可以指向原型

constructor

原型并没有指向实例的属性,不过指向构造函数的就是constructor这个属性

function animal() {

}
let dog = new animal()
console.log(animal == animal.prototype.constructor) // ture
console.log(Object.getPrototypeOf(dog) == animal.prototype) // ture

Object.getPrototypeOf()方法可以获得实例的原型

原型与实例

function animal() {

}
animal.prototype.age = 10
let dog = new animal()
dog.age = 5
console.log(dog.age) // 5
delete dog.age
console.log(dog.age) // 10

当读取原型属性时,先从实例上找,如果找不到,那么会到实例的原型上找,如果还找不到,就会到原型的原型上找,一直找到顶层为止。

原型的原型

function animal() {

}
let dog = new animal()
console.log(animal.prototype.__proto__.constructor) // Object() 

可以看出原型的原型就是通过Object()构造函数实例的

console.log(Object.prototype.__proto__ === null) // ture

这说明搜索到Object.prototype,Object()构造函数的原型的原型不存在了。所以原型链的顶层也就找到Object.prototype了。

什么是原型链

原型链

原型只具体实例的原型到Object的原型与Object原型的原型的链状逻辑,即图中蓝色部分。

ES6之迭代器与 for...of

ES6之迭代器与 for...of

为什么要使用迭代器和 for...of ?

当我们使用传统的循环时,需要使用索引,数组长度来访问数组的元素成员,但是有时候我们只希望访问数组成员,而循环如果嵌套复杂之后,就需要多个索引值,代码变得更为复杂.

为了解决这种问题,ES6 引入了迭代器和for...of来共同解决这个问题.

迭代器

迭代器其实是一个有next()方法的对象,每次调用返回一个结果对象,结果对象包含value(表示当前的值)属性和done(表示是否结束)属性.

使用ES5来创建一个迭代器:

function createIterator(items) {
    var i = 0
    return {
        next: function() {
            var done = i >= items.length
            var value = !done ? items[i++] : undefined

            return {
                done: done,
                value: value
            }
        }
    }
}

var iterator = createIterator([3, 4, 5])
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

for...of

ES6提供for...of语句来遍历可以迭代器(是一种可遍历的对象).

var obj = {
    value: 1
};

for (value of obj) {
    console.log(value);
}

// TypeError: iterator is not iterable

在js中,一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是"可遍历的"(iterable)。

我们直接遍历一个对象会报错,如果我们给这个对象添加Symbol.iterator属性:

const obj = {
    value: 1
};

obj[Symbol.iterator] = function() {
    return createIterator([1, 2, 3]);
};

for (value of obj) {
    console.log(value);
}

因此,可以看出for...of 遍历是对象的Symbol.iterator属性

默认可迭代对象

我们如果直接遍历一个数组对象:

var a = [1, 2, 3]
for(const i of a) {
    console.log(i)
}

尽管我们没有手动添加 Symbol.iterator 属性,还是可以遍历成功,这是因为 ES6 默认部署了 Symbol.iterator 属性,当然我们也可以手动修改这个属性:

var a = [1, 2, 3]

a[Symbol.iterator] = function() {
    return createIterator(['Tom', 'JieNi'])
}

for(const i of a) {
    console.log(i)
}

除了数组之外,还有一些其他的数据结构默认部署了Symbol.iterator:

模拟实现for...of

for...of 就是把对象的Symbol.iterator中的迭代器,用while进行遍历,把迭代器中的value拿出来:

// 原本的for...of
var a = [1, 2, 3]
for(const i of a) {
    console.log(i)
}
// 模拟实现for...of
function forOf (obj, cb) {
    let iterator, res
    if(typeof obj[Symbol.iterator] != 'function')
        throw new TypeError(obj+'is not iterator')
    if(typeof cb != 'function')
        throw new TypeError('cb must be callable')
    
    iterator = obj[Symbol.iterator]()

    res = iterator.next()
    while(!res) {
        cb(res.value)
        res = iterator.next()
    }
}
function cb(i) {
    // cb 函数模拟for...of循环代码内部的逻辑
    console.log(i)
}

内建迭代器

为了更好访问对象中的内容,比如有时候我们需要访问值,而有时候即需要值也需要索引值; ES6为数组,Map,Set集合内建了以下三种迭代器:

  1. entries(),返回一个可遍历对象,用来遍历[键名,键值]组成的数组; 在数组中,键名就是索引
  2. keys(),返回一个可遍历对象,用来遍历所有的键名
  3. values(),返回一个可遍历对象,用来遍历所有键值

以数组为例:

var a = ['Tom', 'JieNi']

for(const i of a.keys()) {
    console.log(i)
}

for(const i of a.values()) {
    console.log(i)
}

for(const i of a.entries()) {
    console.log(i)
}

Map 类型与数组相似,但对于Set 类型需要注意:

var a = new Set(['Tom', 'JieNi'])

for(const i of a.keys()) {
    console.log(i)
}

for(const i of a.values()) {
    console.log(i)
}

for(const i of a.entries()) {
    console.log(i)
}

可以看出,在Set中,键值和键名是相同的.

而且每个集合类型都有一个默认的迭代器,在 for-of 循环中,如果没有显式指定则使用默认的迭代器。数组和 Set 集合的默认迭代器是 values() 方法,Map 集合的默认迭代器是 entries() 方法。

js闭包和垃圾回收机制

js闭包

定义

闭包在高程中的定义:闭包是指有权访问另一个函数作用域中的变量的函数;创建闭包的常见方式,就是在一个函数内部创建另一个函数。

闭包在MDN中的定义:闭包是指那些能够访问自由变量的函数;自由变量是指在函数中使用,但既不是函数参数,也不是局部变量。

因此产生两个理解,理论上和实践上理解闭包:

  1. 从理论角度来说,所有的函数在创建时,就将上下文保存在栈中,即便是全局变量,因此在函数中访问全局变量就相当于在访问自由变量,这个时候使用的是最外层的作用域
  2. 从实践的角度来说,闭包即便创建它的上下文已经销毁,但它仍然存在(比如,父函数返回一个内部函数);在代码中引用了自由变量。

题目分析

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案都是3.js没有词法作用域,每次循环会覆盖之前的i值,因此每次打印的i是最后的值。
那么从上下文来分析:

data[0]执行之前,全局的VO是:

globalContext = {
    VO: {
        data:[...],
        i:3
    }
}

执行data[0]函数时,创建VO,它的作用域链为:

data[0]Context = {
    scope: [VO, globalContext]
}

因此data[0]的答案是3,data[1]、data[2]同理

把它改成闭包:

var data = []

for(var i=0; i<3; i++) {
    data[i] = (function (i) {
        return function () {
            console.log(i)
        }
    })(i)
}

data[0]();
data[1]();
data[2]();

data[0]函数之前,全局上下文和之前一样:

globalContext = {
    VO: {
        data: [...],
        i:3
    }
}

data[0]执行时,作用域不一样了:

data[0]Context = {
    scope:[VO, 匿名函数Context.VO, globalContext.VO]
}

匿名函数的上下文:

匿名函数Context = {
    VO:{
        arguments:{
            0:0,
            length:1
        },
        i: 0
    }
}

执行data[0]时,在查找自身函数的VO,没有i,那么沿着作用域链,查找匿名函数的VO,因此打印结果为0。这里就可以解释实践理论中,即使闭包的上下文被销毁,它依然存在在内存中,匿名函数已经在循环时就执行结束了,但是data[0]执行仍然可以访问到它的作用域,因为data[0]函数的上下文保存了匿名函数的作用域,也就形成了闭包。

闭包中的this指向

闭包中this指向当前执行作用域,一般来说指向函数,函数为匿名函数则指向window对象。

var data = []

for(var i=0; i<3; i++) {
    data[i] = (function (i) {
        return function () {
            console.log(this.i)
        }
    })(i)
}

data[0]();
data[1]();
data[2]();

结果为undefined,因为创建的函数VO并没有i,而this指向当前执行作用域,也就返回undefined。

内存泄漏

上面说到闭包会保存之前函数的作用域,即使它的上下文销毁,它仍然存在内存中。那么这部分内存就无法正常被js的垃圾回收机制处理,因此也就造成内存泄漏。

js的垃圾回收机制

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

引用计数

引用:在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

引用计数:此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

限制:循环引用,如果两个对象相互引用,那么垃圾回收机制无法判定对象不再需要,因此这部分内存并不会被回收。DOM中同样存在循环引用的情况,也会造成内存泄漏。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

标记清除

标记清除算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫根的对象(在JavaScript中,根是全局对象)。垃圾回收器将定期从根开始,找到所有从根引用的对象,然后找到这些对象引用的对象......从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

js中call和apply的模拟实现

js中call和apply的模拟实现

call

call()方法,函数A调用call()方法,传递一个this指向的函数或方法,从而改变函数A的this指向

var foo =  {
    value : 1
}

function bar() {
    console.log(this.value)
}
bar.call(foo) // 1

发生了什么:

  1. call改变了this的指向,指向到foo
  2. bar函数执行了

call的模拟

模拟的步骤可以为:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除这个属性

一些需要考虑的边界情况:

  1. 如果call2()中调用的函数有多个参数,arguments = [foo, arg1, arg2, ...args],提出需要执行的参数args = [arg1, arg2, ...args];
  2. 如果call2()中传入null,那么此时this指向window
  3. 函数是有return的情况的,返回一个执行完的结果即可
Function.prototype.call2 = function(context) {
    context = context || window
    // this是调用call2的函数,也就是需要执行的函数bar
    context.fn = this

    const args = []
    for(let i=1; i<arguments.length; i++) {
        args.push(arguments[i])
    }

    const result = context.fn(...args)

    delete context.fn

    return result
}

var value = 1

var foo = {
    value: 2
}

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

bar.call2(null) // 1

bar.call2(foo, 20, 'tom')
// Object = {
//    age: 20
//    name: "tom"
//    value: 2
//}

apply的模拟实现

apply与call实现本质没有太大区别,唯一区别在于传参时apply可以传类数组对象

Function.prototype.apply2 = function(context) {
    context = context || window
    // this是调用call2的函数,也就是需要执行的函数bar
    context.fn = this
    
    var result
    if(arguments.length==1) {
        result = context.fn()
    } else{
        // apply接收一个参数数组
        const args = arguments[1]
        const result = context.fn(...args)
    }

    delete context.fn
    return result
}

apply的常见应用

将一个数组添加到另一个数组

var arr1 = [1,2,3]
var arr2 = [4,5,6]

arr1.push.apply(arr1, arr2)
console.log(arr1) // [1, 2, 3, 4, 5, 6]

获取数组最大或最小值

/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];

var max = Math.max.apply(null, numbers); 
var min = Math.min.apply(null, numbers);

如果数组长度大于参数限制,会出现预期不对。可以将数组分块来循环使用该方法:

function minOfArray(arr) {
  var min = Infinity; // 无穷大
  var QUANTUM = 32768; // 参数上限长度

  for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
    var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
    min = Math.min(submin, min);
  }

  return min;
}

var min = minOfArray([5, 6, 2, 3, 7]);

动态规划

动态规划

动态规划:分治+最优子结构,用Fibonacci数举例,如果用递归+记忆化搜索其实就是动态规划,不过动态规划一般习惯思维是自底向上,而分治+最优子结构是一种自顶向下思考的方式。

如何去思考DP的题

  1. 寻找重复子问题
  2. 定义状态数组
  3. 定义状态转义方程

常见dp题目分析

Fibonacci数列

  1. 递归的方式,没有任何优化; time:O(2^n) space:O(n)
function f(n) {
    if(n<=1) return 1
    return f(n-1) + f(n-2)
}
  1. 记忆化递归,将已经计算过的递归结点直接使用; time:O(n) space:O(n)
let men = new Map()
function f(n) {
    if(n<=1) return 1
    if(!men.has(n)) {
        map.set(n, f(n-1)+f(n-2))
    }
    return m.get(n)
}
  1. 动态递推,自顶向上的模拟递归的过程;状态转移方程:f(n) = f(n-1) + f(n-2); time:O(n) space:O(n)
function f(n) {
    dp = [1, 1]
    for(let i=2;i<=n; i++) {
        dp[i] = dp[i-1] + dp[i-2]
    }
    return dp[n]
}
  1. 动态递推,发现递推过程中使用的状态只有三种,使用滑动窗口的方式,优化内存空间; time:O(n) space:O(1)
function f(n) {
    fn0 = 1
    fn1 = 1
    fn2
    for(let i=2;i<=n; i++) {
        fn2 = fn1 + fn2
        fn0 = fn1
        fn1 = fn2
    }
    return fn2
}

不同路径

  1. 递归,递归公式 f(x, y) = f(x-1, y) + f(x, y-1); time:O(mn) space:O(mn)
function f(m, n) {
    if(m<=0 || n<=0) return 0
    if(m==1 && n==1) return 1
    return f(m-1, n) + f(m, n-1)
}
  1. 动态递推,状态转移方程,f(x, y) = f(x-1, y) + f(x, y-1); time:O(mn) space:O(mn)
function f(m, n) {
    let dp = Array(m).fill(1).map(x => Array(n).fill(1))
    for(let i=1; i<m; i++) {
        for(let j=1; j<n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
        }
    }
    return dp[m-1][n-1]
}

打家劫舍问题

  1. 动态递推, dp[i] = max(dp[i-1], dp[i-2]+nums[i]);time: O(n) space:O(n)
var rob = function(nums) {
    if(nums.length==0) return 0
    if(nums.length==1) return nums[0]
    dp = []
    dp[0] = nums[0]
    dp[1] = Math.max(dp[0], nums[1])
    for(let i=2;i<nums.length; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i])
    }
    return dp[dp.length-1]
}
  1. 在第一种的基础上优化空间,我们发现整个递推的过程中,只需要保存dp[i-1],和dp[i-2]+nums[i]这个变量;time:O(n) space:O(1)
var rob = function(nums) {
    if(nums.length==0||nums==null) return 0
    if(nums.length==1) return nums[0]
    let f1 = nums[0]
    let f2 = Math.max(nums[0], nums[1])
    let temp
    for(let i=2; i<nums.length; i++) {
        temp = f2
        f2 = Math.max(f2, f1+nums[i])
        f1 = temp
    }
    return f2
}
  1. 动态递推,利用二维数组来定义状态方程
// dp[i][0] 表示第i没偷,dp[i][1] 表示第i偷,那么前一天一定没偷,今天偷.
dp[i][0] = max(dp[i-1][0], dp[i-1][1])
dp[i][1] = dp[i-1][0] + nums[i]

三角形最小路径和

动态递推:状态转移方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1])

var minimumTotal = function(triangle) {
    for(let i=triangle.length-2; i>=0; i--) {
        for(let j=0; j<triangle[i].length; j++) {
            triangle[i][j] = Math.min(triangle[i+1][j], triangle[i+1][j+1]) + triangle[i][j]
        }
    }
    return triangle[0][0]
};

股票买卖问题

思路:找到最低价格,用当前价格减去最低价格,更新最大利润; time:O(n) space:O(1)

var maxProfit = function(prices) {
    let minPrice = prices[0]
    let maxProfit = 0
    for(let i=1; i<prices.length; i++) {
        if(prices[i] < minPrice) {
            minPrice = prices[i]
        } else if(prices[i]-minPrice>maxProfit) {
            maxProfit = prices[i] - minPrice
        }
    }
    return maxProfit
};

对于一系列的股票问题,可以抽象出一套状态数组,dp[i][k][0]:

  1. i 表示天数
  2. k 表示已经交易的次数
  3. 0 表示没有持仓,1 表示持仓
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])

ES6之async函数

ES6之async函数

async是什么

async是Generator的语法糖。与Generator比较,在语法上,async仅仅是把 * 换成 async;yield 换成了 await 而已。

模拟一个异步例子:

// Generator写法
function f(time) {
    return new Promise(
        (resolve, reject) => {
            setTimeout(() => {
                console.log("after time:", time)
                resolve()
            }, time)

        }
    )
}


function* gen() {
    yield f(1000)
    yield f(2000)
}

var g = gen()

如果要获取结果,需要手动执行Genrator函数:

g.next().value.then(()=>{console.log('successs')})
g.next().value.then(()=>{console.log('successs')})

用 async 改写:

async function asyncGen() {
    await f(1000).then(()=>{console.log('success')})
    await f(2000).then(()=>{console.log('success')})
}

如果要获取结果,直接执行asyncGen函数:

asyncGen()

可以发现 async 函数与 Geneartor 函数的区别:

  1. async 内置执行器,不需要像Geneartor那样引入co模块再能自动执行
  2. 更好的语义,async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async与Promise

async与Promise都是ES6对异步操作的方案,那async对于一些场景有什么优劣势?

代码更简洁

/**
 * 处理单个异步操作
 */
function fetch() {
  return (
    fetchData()
    .then(() => {
      return "done"
    });
  )
}

async function fetch() {
  await fetchData()
  return "done"
};
/**
 * 对单个异步操作返回值进行操作
 */
function fetch() {
  return fetchData()
  .then(data => {
    if (data.moreData) {
        return fetchAnotherData(data)
        .then(moreData => {
          return moreData
        })
    } else {
      return data
    }
  });
}

async function fetch() {
  const data = await fetchData()
  if (data.moreData) {
    const moreData = await fetchAnotherData(data);
    return moreData
  } else {
    return data
  }
};
/**
 * 对于流操作,一系列异步操作有着关联,一个异步操作需要使用到上一个异步操作的结果
 */
function fetch() {
  return (
    fetchData()
    .then(value1 => {
      return fetchMoreData(value1)
    })
    .then(value2 => {
      return fetchMoreData2(value2)
    })
  )
}

async function fetch() {
  const value1 = await fetchData()
  const value2 = await fetchMoreData(value1)
  return fetchMoreData2(value2)
};

错误处理

function fetch() {
  try {
    fetchData()
      .then(result => {
        const data = JSON.parse(result)
      })
      .catch((err) => {
        console.log(err)
      })
  } catch (err) {
    console.log(err)
  }
}

这段代码中,Promise异步操作可以使用try cathch捕获fetchData()一些promise构造的错误,不能捕获JSON.parse中的错误,如果要捕获这个错误,需要添加catch函数重复一些异常处理的逻辑。

在实际项目中,错误处理逻辑可能会很复杂,这会导致冗余的代码。

async function fetch() {
    try {
        const data = JSON.parse(await fetchData())
    } catch(err) {
        console.log(err)
    }
}

async/await 的出现使得 try/catch 就可以捕获同步和异步的错误。

async 地狱

async 地狱指开发者贪图语法上的简洁,让原本可以并行执行的操作,变成顺序执行,从而影响了性能。

例子一:

(async () => {
  const getList = await getList();
  const getAnotherList = await getAnotherList();
})();

getList 和 getAnotherList 逻辑上没有依赖关系,但是这种写法,虽然简洁,却导致getAnotherList() 必须在 getList() 执行完之后执行,浪费了一倍的时间。

改进:

(async () => {
    const listPromise = getList()
    const anotherListPromise = getAnotherList()
    await listPromise
    await anotherListPromise
})()

也可以使用Promise.all()

(async () => {
    Promise.all([getList(), getAnoterList()]).then()
})()

例子二:
如果函数中既有依赖关系,又有并发的关系,该怎么处理呢?

(async () => {
  const listPromise = await getList();
  const anotherListPromise = await getAnotherList();

  // do something

  await submit(listData);
  await submit(anotherListData);

})();

基本步骤:

  1. 找出依赖关系,例子中,submit(listData)需要在getList()之后,submit(anotherListData)在getAnotherList()之后
  2. 将互相依赖的语句包裹在 async 函数中
async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}
  1. 并发执行 async 函数
async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}

// 方法一
(async () => {
  const handleListPromise = handleList()
  const handleAnotherListPromise = handleAnotherList()
  await handleListPromise
  await handleAnotherListPromise
})()

// 方法二
(async () => {
  Promise.all([handleList(), handleAnotherList()]).then()
})()

继发与并发

问题:给定一个url数组,如何实现接口的继发和并发?

async继发实现:

// 继发一
async function loadData() {
    const res1 = await fetch(url1)
    const res2 = await fetch(url2)
    const res3 = await fetch(url3)
    return 'done'
}

// 继发二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

async并发实现:

// 并发一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return "whew all done";
}
// 并发二
async function loadData(urls) {
  // 并发读取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

async错误捕获

尽管我们可以使用 try catch 捕获错误,但是当我们需要捕获多个错误并做不同的处理时,很快 try catch 就会导致代码杂乱,就比如:

async function asyncTask(cb) {
    try {
       const user = await UserModel.findById(1);
       if(!user) return cb('No user found');
    } catch(e) {
        return cb('Unexpected error occurred');
    }

    try {
       const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    } catch(e) {
        return cb('Error occurred while saving task');
    }

    if(user.notificationsEnabled) {
        try {
            await NotificationService.sendNotification(user.id, 'Task Created');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    if(savedTask.assignedUser.id !== user.id) {
        try {
            await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    cb(null, savedTask);
}

为了简化这种错误的捕获,我们可以给 await 后的 promise 对象添加 catch 函数,为此我们需要写一个 helper:

// to.js
export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}

整个错误捕获的代码可以简化为:

import to from './to.js';

async function asyncTask() {
     let err, user, savedTask;

     [err, user] = await to(UserModel.findById(1));
     if(!user) throw new CustomerError('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) throw new CustomError('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if (err) console.error('Just log the error and continue flow');
    }
}

async的讨论

async 会取代 Generator 吗?

Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。

async 会取代 Promise 吗?

  1. async 函数返回一个 Promise 对象

  2. 面对复杂的异步流程,Promise 提供的 all 和 race 会更加好用

  3. Promise 本身是一个对象,所以可以在代码中任意传递

  4. async 的支持率还很低,即使有 Babel,编译后也要增加 1000 行左右。

高级搜索

高级搜索

  • 剪枝
  • 双向BFS
  • 启发式搜索

剪枝

由于朴素递归的状态空间一般很大,很多结点重复计算,就需要剪枝进行优化

leetcode题目

//  暴力法:每一行的检测,每一列的检测,9个小块检测 map 
var isValidSudoku = function(board) {
    let cols = {}
    let rows = {}
    let boxes = {}

    for(let i=0; i<9; i++) {
        for(let j=0; j<9; j++) {
            
            let num = board[i][j]
            if(num != '.') {
                let boxIndex = parseInt(i/3)*3 + parseInt(j/3)
                if(cols[j+'-'+num] || rows[i+'-'+num] || boxes[boxIndex+'-'+num]) {
                    return false
                }
                cols[j+'-'+num] = true
                rows[i+'-'+num] = true
                boxes[boxIndex+'-'+num] = true
            }
        }
    }

    return true
};

关键点,对有效数组判定,对3*3的区块的区分,boxeindex = (row/3)*3 + col/3

回溯:在空格处用1~9试错,如果有效那么继续遍历;如果无效,就返回上一层尝试其它数字

var solveSudoku = function(board) {
    if(board.length == 0 || board == null) return
    dfs(board)
};

var dfs = function(board) {
    for(let i=0; i<9; i++) {
        for(let j=0; j<9; j++) {
            if(board[i][j] == '.') {
                for(let c=1; c<=9; c++) {
                    if(isValidSudoku(board, i, j, c)) {
                        board[i][j] = `${c}`                            
                        if(dfs(board)) {
                            return true
                        }
                        board[i][j] = '.'
                    }
                }

                return false            
            }
        }
    }
    return true
}

var isValidSudoku = function(board, row, col, c) {
    for(let i=0; i<9; i++) {
        if(board[i][col] != '.' && board[i][col] == c) return false
        if(board[row][i] != '.' && board[row][i] == c) return false
        let x = parseInt(row/3) * 3 + parseInt(i/3)
        let y = parseInt(col/3) * 3 + i%3
        if(board[x][y] != '.' && board[x][y] == c) return false
    }
    return true
};

双向BFS

相对单向bfs来说,通过从双端来进行优化搜索时间。

leetcode题目

单向bfs:

思路:

  1. 对 wordlist 预处理,键值是单词通用状态,值是单词
  2. 用数组表示以及访问的单词,避免重复访问
  3. 用队列存在 beginWord 和 1 ,找出它的所有的队列,然后在预处理的状态中找,找出对应的单词,如果发现endWord,那么 lever+1;如果没找到,单词入队列。
  4. 考虑边界,如果beginWord不在wordlist中,直接返回0
var ladderLength = function(beginWord, endWord, wordList) {
    if(!beginWord || !endWord || wordList.indexOf(endWord) == -1) return 0

    // 对wordlist预处理
    let commonDict = {}
    let len = beginWord.length
    for(let i=0; i<wordList.length; i++) {
        for(let r=0; r<len; r++) {
            // let comState = wordList[i].substring(0,r)+'*'+wordList[i].substring(r+1,len);
            let comState = wordList[i].substring(0,r)+'*'+wordList[i].substring(r+1,len);
            (!commonDict[comState]) && (commonDict[comState] = [])
            commonDict[comState].push(wordList[i])
        }
    }

    // queue for bfs
    let queue = [[beginWord, 1]]
    // visted
    let visted = {beginWord: true}
    while(queue.length > 0) {
        let currNode = queue.shift()
        let currWord = currNode[0]
        let currLevel = currNode[1]
        for(let i=0; i<len; i++) {
            let comState = currWord.substring(0, i) + '*' + currWord.substring(i+1, len)
            if(comState in commonDict) {
                let temp = commonDict[comState]
                for(let j=0; j<temp.length; j++) {
                    if(temp[j] == endWord) {
                        return currLevel + 1
                    }
                    if(!visted[temp[j]]) {
                        visted[temp[j]] = true
                        queue.push([temp[j], currLevel+1])
                    }
                }
            }
        }
    }
    return 0
};

时间复杂度:O(n*m) ; 空间复杂度: O(n*m) 。n是单词的长度,m是单词列表的长度

双向bfs:

思路:

  1. 用beginSet来替换之前从前向开始访问的队列,用endSet代表从后的队列
  2. 用wordListSet替换之前visited
  3. 从beginSet开始扩散,然后a的ACII码是97循环得到a~z,看s是否在endSet中,如果在直接返回level+1;否则,看s是否在wordlListSet中,如果存在那么添加到一个中间set中(nextBeginSet),替换掉beginSet;比较头尾Set大小,从size小的一段扩散
// 相对之前的单向bfs,终结条件变成两端访问过同一个结点
var ladderLength = function(beginWord, endWord, wordList) {
    let wordListSet = new Set(wordList)
    if(!wordListSet.has(endWord)) return 0
    let beginSet = new Set()
    beginSet.add(beginWord)
    let endSet = new Set()
    endSet.add(endWord)
    let level = 1
    // bfs
    while(beginSet.size > 0) {
        let nextBeginSet = new Set()
        for(let key of beginSet) {
            for(let i=0; i<key.length; i++) {
                for(let j=0; j<26; j++) {
                    let s = String.fromCharCode(97+j)
                    if(key[i] != s) {
                        let newWord = key.slice(0, i) + s + key.slice(i+1)
                        if(endSet.has(newWord)) {
                            return level + 1
                        }
                        if(wordListSet.has(newWord)) {
                            nextBeginSet.add(newWord)
                            wordListSet.delete(newWord)
                        }
                    }
                }
            }
        }
        beginSet = nextBeginSet
        level++
        // 做一个交换,让size小的端先进行扩散
        if(beginSet.size > endSet.size) {
            [beginSet, endSet] = [endSet, beginSet]
        }
    }
    return 0
};

时间复杂度:O(n*m) ; 空间复杂度: O(n*m) 。n是单词的长度,m是单词列表的长度;
与单向搜索相同的是,找到所有的变换需要 M∗N 次操作。但是搜索时间会被缩小一半,搜索空间变小,因为两个搜索会在中间某处相遇。

启发式搜索

相对bfs来说,本质把queue换成了(priority queue)优先队列;

如何定义优先队列就需要根据具体问题,来指定估计函数;

估计函数

启发式函数:h(n),它用来评价哪些结点最有希望是我们要找到的结点,h(n)会返回一个非负实数,可以认为返回值越大越有希望找到目标结点。

leetcdoe题目和参考资料

js之跟着underscore学节流

js之跟着underscore学节流

什么是节流

节流的原理:如果你持续触发事件,每隔一段时间,只执行一次事件。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

时间戳

功能要求:事件触发,会立即执行回调;接下来如果持续触发,会在n秒后执行一次回调;

思路:当触发事件时,取出当前的时间戳,然后减去之前的时间戳(初始值设为0);如果大于设置的时间,就执行回调函数并更新之前的时间戳;如果小于,就不执行。

function throttle(func, wait) {
    var context, args
    var pre = 0
    return function() {
        var now = +new Date()
        args = arguments
        context = this
        if(now-pre >= wait) {
            func.apply(context, args)
            pre = now
        }
    }
}

使用定时器

功能要求:事件触发,回调不会立即执行而是在n秒后触发;触发停止后,会再一次触发一次回调事件

思路:当事件触发时,设置一个定时器,再次触发时,如果定时器存在就不执行,等待定时器的回调执行,回调中把定时器清空;如果不存在,就设置一下定时器。

function throttle(func, wait) {
    var context, args
    var timeout
    return function() {
        context = this
        args = arguments
        if(!timeout) {
            timeout = setTimeout(function(){
                func.apply(context, args)
                timeout=null
            }, wait)
        }
    }
}

时间戳+定时器

功能要求:事件触发立即执行回调函数,并且事件停止触发后依然能执行一次回调函数。

思路:当第一次触发函数时,采用时间戳的方式执行回调;之后的触发就是用定时器的方式来执行回调;用一个变量 rest = wait - (now-pre) 纪录下次触发回调函数的时间,如果rest<0 :表示第一次触发函数,如果>0表示,后续触发事件

function throttle(func, wait) {
    var timeout, context, args
    var pre = 0
    var timer = function () {
        pre = +new Date()
        func.apply(context, args)
        timeout = null
    }
    var main = function () {
        var now = +new Date()
        var rest = wait - (now - pre)
        context = this
        args = arguments
        // 如果没有剩余的时间了或者你改了系统时间
        if (rest <= 0 || rest > wait) {
            // 清除定时器
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            pre = now
            func.apply(context, args)
        } else if (!timeout) {
            timeout = setTimeout(timer, rest)
        }
    }
    return main
}

优化

但是我有时也希望无头有尾,或者有头无尾,这个咋办?

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

  • leading:false 表示禁用第一次执行
  • trailing: false 表示禁用停止触发的回调

思路:我们只需要,对传入的参数进行判断,如果leading为false,那么我们让第一次的rest>0,这样一直执行定时器分支;如果trailing为fasle,我们让一直rest<0,这样一直执行的时间戳分支。

注意点:在该思路的基础上,因为节流函数始终在时间戳和定时器两个形式切换,因此当leading和triling同时设置为false的时候,第一次就会设置定时器,出现bug

function throttle(func, wait, options) {
    var timeout, context, args
    var pre = 0
    if(!options) options={}
    var timer = function () {
        pre = +new Date()
        func.apply(context, args)
        timeout = null
        if (!timeout) context = args = null;
    }
    var main = function () {
        var now = +new Date()
        if(!pre && options.leading === false) pre = now
        var rest = wait - (now - pre)
        context = this
        args = arguments
        // 如果没有剩余的时间了或者你改了系统时间
        if (rest <= 0 || rest > wait) {
            // 清除定时器
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            pre = now
            func.apply(context, args)
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(timer, rest)
        }
    }
    return main
}

取消按钮

取消功能:点击后取消按钮后,就可以立马触发回调函数

思路:让pre重置为0,把定时器清空

main.cancel = function() {
    clearTimeout(timeout);
    pre = 0;
    timeout = null;
}

js参数按值传递

js参数按值传递

ECMAScript中所有函数的参数都是按值传递。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

基本类型的传递

基本类型值的传递实质上把这个值拷贝一份,然后复制给参数。

// 例子1
var value = 1
function foo (v) {
    v = 2
    console.log(v)
}
foo(value) // 2
console.log(value) // 1

当传递value到函数中时,复制了一份value,假定拷贝的这份叫_value,函数中修改的是_value的值,而不会影响之前的value。

引用类型的传递

引用类型的传递实质上会传递对象引用的副本;如果函数修改的引用属性的值,可以通过属性的值找到原值并修改,如果直接修改引用,那么并不会修改原值。

// 例子2
var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
// 例子3
var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

基本类型和引用类型在js中存贮方式:

  1. 基本类型值存储于栈内存中,传递的就是当前值的拷贝(另外一份栈空间),修改不会影响原有变量的值;
  2. 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;

例子1

改变前:

栈内存 堆内存
value 1
v 1

改变后:

栈内存 堆内存
value 1
v 2

例子2

改变前:

栈内存 堆内存
obj, o 指针地址 {value: 1}

改变后:

栈内存 堆内存
obj, o 指针地址 {value: 2}

例子3

改变前:

栈内存 堆内存
obj, o 指针地址 {value: 1}

改变后:

栈内存 堆内存
obj 指针地址 {value: 1}
o 2

leetcode剑指 Offer 09. 用两个栈实现队列

leetcode剑指 Offer 09. 用两个栈实现队列

leetcode链接

思路

用两个栈实现队列添加元素、删除操作;stack1用于添加元素,stack2维护删除时,每次判断本身是否有元素,有就直接pop(),否则把stack1全部压入stack2,出栈。因为stack1中的元素本身先进后出,那么进入stack2后再出栈就是队列需要的那个'先进先出'的顺序。

代码

// 思路:用两个栈,一个栈维护进栈的元素,另一个维护出栈的元素
var CQueue = function() {
    this.stackIn = []
    this.stackOut = []
};


CQueue.prototype.appendTail = function(value) {
    this.stackIn.push(value)
};

CQueue.prototype.deleteHead = function() {
    const {stackIn, stackOut} = this
    if(stackOut.length) {
        return stackOut.pop()
    } else{
        while(stackIn.length) {
            stackOut.push(stackIn.pop())
        }
        return stackOut.pop() || -1
    }
};

总结

队列先进先出、栈先进后出;查询操作时间复杂度O(1),添加、删除时间复杂度O(1)。

js之跟着underscore学防抖

js之跟着underscore学防抖

为什么需要防抖

在前端开发中会遇到一些频繁的事件触发,比如:

  1. window 的 resize、scroll
  2. mousedown、mousemove
  3. keyup、keydown

引用一个例子:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
        #container {
            width: 100%;
            height: 200px;
            line-height: 200px;
            text-align: center;
            color: #fff;
            background-color: #444;
            font-size: 30px;
        }
    </style>
</head>

<body>
    <div id="container"></div>
    <script>
        var count = 1;
        var container = document.getElementById('container');

        function getUserAction() {
            container.innerHTML = count++;
        };

        container.onmousemove = getUserAction;
    </script>
</body>

</html>

可以打开浏览器测试一下,从左滑到右,大概触发了165次getUserAction()函数。

因为这个函数本身处理的逻辑复杂,当回调函数复杂或者ajax请求时,假设1秒触发了60次,每个回调必须在1000 / 60 = 16.67ms内完成,否则就会有卡顿出现。

为了解决这种问题,通常采用debounce(防抖)和throttle (节流)。

防抖是什么

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

模拟实现underscore中防抖函数

实现效果:使用debounce函数后,返回一个函数,这个函数在设置n秒后,才触发回调事件;即不论触发多少次,事件一定是在触发后n秒才执行,

实现基本思路:

  1. debounce函数返回一个新的函数,利用定时器,每一次先清除定时器,再定时setTimeoUt触发要绑定的事件
  2. 需要考虑this的指向,原本绑定的this是指向绑定的元素的;但是setTimeout中this会指向window,因此我们保存原来的this,用apply让this重新指向需要绑定的元素
  3. event对象,对于event,只需要把传入的arguments,随着apply一起传入回调执行即可

实现的功能:

  1. immediate,如果为true,表示事件触发后立即执行;如果为false,表示过一段时间触发
  2. 如果绑定的函数有返回值,只是在immediate为true时返回结果
  3. 有取消功能函数,将定时器清除,定时器id设置为null
function debounce(func, wait, immediate) {

    var timeout, result;

    var debounced = function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
}

布隆过滤器实现及应用

布隆过滤器实现及应用

定义

布隆过滤器(Bloom Filter):一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以查询一个元素是否在一个集合中。

与Hash Table的区别:在于布隆过滤器只能查询这个元素在或不在;而哈希表可以查询到这个元素更多的信息。

优点:空间效率和查询时间都远远超一般的算法;因为二进制存储节省很多空间,查询采用模糊查询。

缺点:有一定的误识别率和删除困难。

当一个布隆过滤器插入一些数之后,对于测试元素,也就新来的元素要验证它是否存在;如果这个元素对应的二进制都为1,只能说它可能存在,因为它对应的二进制可能恰好和之前多个元素重合;如果有一个0,那么它一定不存在过滤器中。

因此,布隆过滤器常用于快速查询的前置工作,比如:在查询DB之前,先经过布隆过滤器,如果不存在,直接丢掉;如果存在,再查询DB来确定元素是否存在数据库中。

python实现

## bitarray 一个bit数组
from bitarray import bitarray
## python3 用来hash算法的库
import mmh3


class BloomFilter:
    def __init__(self, size, hash_num):
        self.size = size
        self.hash_num = hash_num
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)

    def add(self, s):
        for seed in range(self.hash_num):
            ## 取模,避免index溢出bit_array的下标长度
            index = mmh3.hash(s, seed) % self.size
            self.bit_array[index] = 1

    def lookup(self, s):
        for seed in range(self.hash_num):
            index = mmh3.hash(s, seed) % self.size
            if self.bit_array[index] == 0:
                return "Nope"
        return "Probably"


if __name__ == "__main__":
    bf = BloomFilter(500000, 7)
    bf.add("zero")
    print(bf.lookup("zero"))
    print(bf.lookup("two"))

实际场景

  1. 比特币网络
  2. 分布式系统(Map-reduce)、search engine
  3. Redis缓存
  4. 垃圾邮件、评论等过滤

js作用域链

js作用域链

当查找变量时,先从当前执行上下文的变量对象中查找,如果没有找到;继续沿上一层执行上下文的变量对象中查找,如果也没查到,一直重复这个过程直到全局变量;这样由多个执行上下文变量对象构成的链表叫做作用域链。

一个函数的创建和激活两个时期作用域链是怎样变化的?

函数创建

js的词法作用域是静态作用域,即函数创建的时候作用域也就创建了;这是因为函数有一个[[socpe]]属性,当函数创建的时候,就会把所有父级变量对象保存到[[socpe]]这个属性中;但是[[socpe]]这个属性并不等同于整个作用域链。

函数激活

当函数激活(执行)时,进入函数上下文,就会把活动变量添加到作用域链的前端。

总结

作用域与js代码与可执行栈、变量对象息息相关,通过分析一个函数创建到执行过程:函数定义的时候,首先会将其父级作用域添加到[[scope]]属性;执行函数,创建函数上下文并压入可执行栈;随后函数不会马上执行,先复制函数[[scope]]属性创建作用域,然后创建函数的活动变量并压入作用域的顶端,最后函数的作用域就会包含当前活动变量和之前父级作用域;执行函数,根据代码修改活动变量的值;函数执行完,函数上下文出栈。

js中的Array

js中的Array

标准数组

c中数组的特点:相同类型的元素、连续的内存、索引

与c的数组,即标准数组

  1. 数组为什么有类型限制?
  2. 数组的长度不可以改变?
  3. 数组的下标为什么从0开始的?

在c语言中,创建数组,就是向内存申请一块固定大小的内存空间;空间的大小根据数组长度和数据类型来得到的。

在申请一个标准数组后,cpu会根据类型和大小来申请一个固定的空间;并纪录这个空间的首地址为headAddress;寻址公式 array[i]Address = headAddress + i*dataTypesize;如果数组长度随便改变,那么可能会覆盖后面内存的空间,处于安全考虑,即大小固定;如果下标从1开始,那么寻址公式会变成 array[i]Address = headAddres + (i-1)*dataTypesize,那么每次寻址会多进行一次减法计算,从0开始避免多余的计算。

js的类型存储

js中基本类型存储在栈内存,引用类型存储在堆内存;因为基本类型大小固定,因此放在栈内存可以很容易得到内存地址,其中string的大小在ES5之前采用unicode,中文和英文都占两个字节,而在ES6之后,编码采用utf-8英文占1个字节,中文占3个字节;而引用类型的数据放在堆内存中,栈内存中会存放堆内存的内存指针。

如何实现自动扩容

Array无参数创建的时候,默认创建一个容量为10的数组。每次添加元素会检查容量是否够用,如果不够在内部创建一个容量为原数组1.5倍的数组。再将原数组的数据搬移到新数组中。如果容量还是不够,会直接创建一个符合和所需要的容量的数组。

Array 存储基本类型时,存储的是值。存储引用类型时,存储的是内存引用。javascript数组,本质类似一个对象。

let a = []
a['0'] = 1
a[0] // 1
a['0'] // 1

// 等价于
let a = {
    '0' : 1
}

// js中对象的key会被转换成string存储,尽管是number类型

在v8中对array的处理

数组长度,length;通过length可以删除或添加array的长度。

let arr = [1, 2]
arr.length = 5
console.log(arr) [1, 2, empty x 3]

可以发现数组后面多个3个empty类型的元素,empty表示空的对象的引用。

那么ECMAScript规范中又没有empty类型,那么empty又属于什么类型呢?

console.log(typeof arr[3]); // undefined
arr[3] === undefined // true

但是empty真的等于undefined么?

arr.indexOf(undefined) // -1
arr.filter(item => item===undefined) // []

arr.forEach(item=>{
    console.log(item)
}) // 1 , 2

arr.map(item => {
    console.log(item)
})
// map会保留empty

从上面的例子看出,在indexOf、filter、forEach中empty并不等于undefined

在v8内部,数组类型被分为密集数组和稀疏数组;如果一个元素只包含整形那么数组类型被标记为PACKED_SMI_ELEMENTS;如果包含整数和浮点类型被标记PACKED_DOUBLE_ELEMENTS;如果还包含了其它类型的元素,那么被标记为PACKED_ELEMENTS。PACKED前缀的数组为密集数组,HOLEY前缀为稀疏数组。

密集数组是一块连续的堆栈;稀疏数组是一块内存散列的链表。

如果含有empty元素,那么数组类型会在原有基础上打上HOLEY标记,比如:

let a = [1, 2] // PACKED_SMI_ELEMENTS
a.length = 5 // HOLEY_SMI_ELEMENTS

因此密集数组中包含类型越杂,性能随之下降;稀疏数组的性能是劣于密集数组的;此外,一旦开辟empty这个类型,计算empty这个内存被填充,也不会重新标记为PACKED_ELEMENTS

数组操作微小的性能处理

  1. 避免读取超过数组长度的内容;超过数组长度会执行原型链去查找
  2. 避免-0,NaN,Infinity进入数组,他会把数组类型转换成PACKED_DOUBLE_ELEMENTS
  3. 避免直接使用类数组,比如arguments、getElementBytag就是类数组;使用Array.from(arraylike)把类数组对象转换成数组;或者使用ES6的扩展符...
  4. 避免创建(HOLEY)稀疏数组,创建新数组时,如果使用new Array的方式会创建empty元素,因此尽量使用字面量的形式。

快慢数组的转换

密集数组也称快数组,稀疏数组也称慢数组;在v8底层,快慢数组是会发生相互转换的。

快数组转换成慢数组:

  1. 新容量 >= 3*扩容后的容量*2,会转换成慢数组
  2. 赋值的后数组,出现大于1024个empty时,转换成慢数组

慢数组转换成快数组:

  1. 当慢数组的元素可存放在快数组中且长度在 smi 之间且仅节省了50%的空间,则会转变为快数组

总结

v8底层对数组长度远远过大时,会把数组转换成慢数组,也就是hash的存储结构,牺牲效率来节省内存空间;而快数组不在比慢数组节省50%的空间时,数组会转换成快数组,申请大块连续内存,来提高效率。

ES6之Symbol类型特性和应用场景

ES6之Symbol类型特性和应用场景

Symbol

ES6 引入了一种新的原始数据类型 symbol,表示独一无二的值。symbol 的一些特性:

1.Symbol 的值通过 Symbol 函数生成,用 typeof 检查为 Symbol

const a = Symbol('a')
typeof a // symbol

2.symbol 不能用new命令,否则会报错;这是因为symbol是一种原始的数据类型,不是对象。

3.instanceof 结果为flase

const a = Symbol('a')
a instanceof Symbol // false

4.symbol函数可以接收一个字符串参数,表示对实例的描述;

const a = Symbol('a')
console.log(a) // Symbol(a)

5.如果symbol参数是一个对象,会调用对象的tostring方法,将其转换成字符串,再生成一个symbol值

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)

6.Symbol函数的参数只是对当前symbol值进行描述,相同参数的Symbol函数返回值是不一样的

var a1 = Symbol()
var a2 = Symbol()
console.log(a1===a2) // flase

var b1 = Symbol('tom') 
var b2 = Symbol('tom') 
console.log(b1===b2) // flase

7.symbol值不能与其它类型值运算,会报错

var b1 = Symbol('tom') 
b1 + 'as' // Uncaught TypeError: Cannot convert a Symbol value to a string

8.symbol可以显示转换成字符串

var b1 = Symbol('tom') 
console.log(String(b1)) // Symbol(tom)
console.log(b1.toString()) // Symbol(tom)

9.symbol值可以作为对象的属性名,可以保证不会出现同名的属性

var objName = Symbol()

// 第一种写法
var obj1 = {}
obj1[objName] = 'tom'

// 第二种写法
var obj2 = {
    [objName]: 'tom'
}

// 访问这个属性
console.log(obj[objName]) // 'tom'

10.symbol作为属性名,该属性不会出现for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是symbol并不是私有属性,可以通过Object.getOwnPropertySymbols方法,获取对象所有Symbol属性名。

var mySymbol = Symbol('address')
var o = {name: 'tom'}
o[mySymbol] = '安阳路'

for(const i in o) {
    console.log(i)
} // name

var objectSymbols = Object.getOwnPropertySymbols(o) // [Symbol(address)]

11.如果我们想使用同一个symbol值,使用symbol.for(),该方法传入一个字符串参数,然后搜索有没有以这个字符串为名称的symbol值,如果有就返回这个symbol值;如果没有,就以这个字符串名称创建一个symbol值并返回。

var s1 = Symbol.for('address')
var s2 = Symbol.for('address')

console.log(s1 === s2) // true

12. Symbol.keyFor(sym) 方法用来获取全局symbol 注册表中与某个 symbol 关联的键。

var globalSym = Symbol.for('foo')
console.log(Symbol.keyFor(globalSym)) // foo

var localSym = Symbol()
console.log(Symbol.keyFor(localSym)) // undefined

symbol类型应用场景

定义类的私有变量/方法

const AGE = Symbol()
const GET_AGE = Symbol()
class User {
    constructor(name, sex, age) {
        this.name = name
        this.sex = sex
        this[AGE] = age
        this[GET_AGE] = function () {
            return this[AGE]
        }
    }
    printAge() {
        console.log(this[GET_AGE]())
    }

}
// test

let u1 = new User('xm', 'M', 18)
let u2 = new User('xh', 'W', 20)
console.log(u1.name) // xm
console.log(u1.age) // undefined
u1.printAge() // 18
console.log(u2.name) // xh
console.log(u2.age) // undefined
u2.printAge() // 20

运行在单例模式中

单例模式也称为单体模式,规定一个类只有一个实例,并且提供可全局访问点;

// Phone.js
class Phone {
    constructor() {
        this.name = '小米'
        this.price = '1999'
    }
}

let key = Symbol.for('Phone')

if (!global[key]) {
    global[key] = new Phone()
}

module.exports = global[key]
// test.js
let p1 = require('./Phone')
let p2 = require('./Phone')
console.log(p1 === p2) // true

js的词法作用域

js的词法作用域

作用域

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

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

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

静态作用域和动态作用域

JavaScript采用的是静态作用,即函数的作用域在函数定义的时候就决定了。

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

例子来验证:

var a = 1
function foo() {
    console.log(a)
}
function bar() {
    var a = 2
    foo()
}
bar()
// reuslt a = 1

这个例子说明,如果js是动态作用域,执行bar()时,局部变量a = 2,那么此时继续执行foo(),打印的a会在这时作用域下查找,a应该等于2,实际上并没有;因为foo()在创建的时候,就建立函数作用域,发现局部变量没有a,那么沿着作用域链查找,发现全局变量a = 1,所以最好打印结果是1。证明js是静态作用域。

作用域链:函数活动对象 --> 包含环境的活动对象--> .. -->全局环境变量对象

ES6之Set

ES6之Set

Set是什么

Set是E6中新型的数据结构,类似数组,但是元素成员没有重复的,是唯一的。

Set的初始化

Set本身是一个构造函数,用来创建Set数据结构。

let s = new Set()

Set可以接收一个数组,或者拥有interable接口的数据接口来作为参数进行初始化:

let s_arr = new Set([1,2,3])
console.log(s_arr) // Set(3) {1, 2, 3}

let map = new Map()
Map.set('name','tom')
let s_map = new Set(map)
console.log(s_map) // Set(1) {Array(2)}

Set的属性和方法

操作方法有:

  1. add(value):添加某个值,返回 Set 结构本身。
  2. delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  3. has(value):返回一个布尔值,表示该值是否为 Set 的成员。
  4. clear():清除所有成员,无返回值。

遍历的方法:

  1. Set.keys(),返回键名的遍历器
  2. Set.vales(),返回键值的遍历器
  3. entries(),返回键值对的遍历器
  4. forEach(),使用回调函数遍历每个成员,无返回值
let s_arr = new Set([1, 2, 3])
s_arr.forEach( (key, value) => {console.log(key, value)}) // forEach不会改变原Set

属性:

  1. Set.prototype.size,表示Set所有成员的数量
  2. Set.prototype.constructor,构造函数默认指向Set

Set注意点

在Set中添加多个 NAN 时,Set的size仍是1:

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

console.log(NaN === NaN) // false

以上说明,在Set内部,NaN 和 NaN 是相等的

在Set添加多个对象,对象总是不相等的:

let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2

js执行上下文

js执行上下文

执行上下文包含了上下栈的顺序、作用域链和上下文创建。

分析代码执行顺序

  1. 创建可执行上下文栈,上下文包含(VO, Scope, this)

  2. 函数创建时,将父级作用域保存到函数内部属性[[Scope]],即把父级的上下文的VO添加到函数内部属性[[Scope]]

  3. 函数上下文进栈,初始化函数上下文:

    • 复制函数[[Scope]]属性创建作用域链
    • 用arguments创建活动对象
    • 初始化活动对象:形参、变量声明、函数声明
    • 将活动对象压入该函数Scope,作用域的顶端
  4. 函数执行,执行相应的代码,也可以修改相应活动变量的值;执行完毕,函数上下文出栈

具体代码分析

比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

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

B代码具体分析

  1. 执行全局代码,创建全局可执行上下文globalContext,并压入可执行上下文栈Estack
Estack = {
    globalContext
}
  1. 全局上下文初始化
globalContext = {
    VO: global,
    Scope: [globalContext.Vo],
    this: globalContext.Vo
}
  1. checkscope函数创建,保存作用域链到函数内部属性[[Scope]]
checkscope.[[Scope]] = {
    globalContext.Vo
}
  1. 创建checkscope函数的上下文checkscopeContext,并压入可执行上下文栈
Estack = {
    checkscopeContext,
    globalContext
}
  1. checkscopeContext的初始化
checkscopeContext = {
    VO: {
        arguments: {
            length: 0
        },
        scope: undefined,
        f: reference to function f(){}
    },
    Scope: [VO, globalContext.Vo]
}
  1. 创建f函数,保存作用域链到函数内部的[[Scope]]属性
f.[[Scope]] = {
    chechscopeContext.VO,
    globalContext.VO
}
  1. checkscope函数执行完毕,从可执行上下栈弹出,此时scope已经赋值为"local scope"
Estack = {
    globalContext
}
  1. 执行f函数,并创建函数上文,并压入可执行栈,初始化fContext
Estack = {
    fContext,
    globalContext
}
fContext = {
    VO: {
        arguments: {
            length: 0
        }
    },
    Scope: [VO, chechscopeContext.VO, globalContext.VO]
}
  1. 执行完f,此时沿作用域查找scope,结果为"local scope",弹出f.Context
Estack = {
    globalContext
}

总结

A,B代码虽然最后返回值是一样,但是在可执行栈的环境是不同的

字符串基础知识

字符串基础知识

在JavaScript中,字符串是immuatable(不可变的)。

不可变的意思是,如果每次改变字符串里面的内容,本质上是创建了一个新的字符串。

var myString = "abbdef"; 
myString[2] = 'c'
console.log(myString) // abbdef

我们发现,令myString[2]赋值为'c',原来的字符串并没有发生改变。

trim、slice等字符串操作方法返回新的字符串。

immuatable好处在于在多线程中式安全的。

基础题目

  1. https://leetcode-cn.com/problems/to-lower-case/

string的ASCLL码之间转换:

  1. str to ascll: str.charCodeAt() ; str若'abc',如果传入参数下标1,表示str[1]的acsll码
  2. ascll to str: String.fromCharCode();参数为ascll码值
// ACSLL+遍历:a-z:[97,122] A-Z:[65,90]
var toLowerCase = function(str) {
    let res = ''
    for(const i of str) {
        if(i>='A' && i<='Z') {
            res+=String.fromCharCode(i.charCodeAt()+32)
        } else{
            res+=i
        }
    }
    return res
};
  1. https://leetcode-cn.com/problems/length-of-last-word/

从末尾开始遍历,找到第一个不为空的下标,标记为end,在end的基础上找到为空的下标为start,发现单词长度

var lengthOfLastWord = function(s) {
    let end = s.length-1
    while(end>=0 && s[end]==' ') end--
    let start = end
    while(start>=0 && s[start]!=' ') start--
    return end-start
};
  1. https://leetcode-cn.com/problems/jewels-and-stones/

哈希存储J中值,遍历S,在每次查询的复杂度变为O(1) 整体的时间复杂度:O(m+n)

var numJewelsInStones = function(J, S) {
    const set = new Set();
    for(const s of J) {
        set.add(s);
    }
    let ans = 0;
    for(const s of S) {
        if(set.has(s)){
            ans++;
        }
    }
    return ans;
};
  1. https://leetcode-cn.com/problems/first-unique-character-in-a-string/

计数法,用一个数组存贮26个字母,统计每个字母出现的次数

var firstUniqChar = function(s) {
    let a = Array(26).fill(0)

    for(const i of s) {
        a[i.charCodeAt()-97]++
    }
    for(let j=0; j<s.length; j++) {
        if(a[s[j].charCodeAt()-97]==1) {
            return j
        }
    }
    return -1
};
  1. https://leetcode-cn.com/problems/string-to-integer-atoi/
var myAtoi = function(s) {
    // sign:符号位,total:绝对值,flag:排除多个正负号情况
    let index = 0, sign = 1,  total = 0, flag=0
    // 空字符
    if(s.length == 0) return 0 
    
    // 移除空字符,trim会产生新的字符串,使用额外的内存
    while(s[index] == ' ' && index < s.length) {
        index++
    }

    // 处理正负号
    while(s[index] == '+' || s[index] == '-') {
        sign = s[index] == '+' ? 1 : -1
        flag++
        index++
    }

    // 找出数字部分
    while(index < s.length) {
        let digit = s[index].charCodeAt() - '0'.charCodeAt()
        if(digit > 9 || digit < 0) break
        total = total*10 + digit
        index++
        // 检测total是否大于题目给的整数范围
        if(total>=Math.pow(2,31)) {
            return sign==1 ? Math.pow(2,31)-1 : -1*Math.pow(2, 31)
        }
    }

    return flag > 1 ? 0 : sign*total 
};

字符串匹配算法

字符串匹配:以知A、B字符串,找A在B中什么位置出现;或者B在A中什么位置出现。

常用的算法:

  1. 暴力法 :O(mn) m为字符串A的长度、n为字符串B的长度

代码模板:

/**
* @param {string} a
* @param {string} b
* @return {number} 
*/
function forceSearch(a, b) {
    // a:待字符串 b:需要匹配的字符串
    let m = a.length
    let n = b.length
    for(let i=0; i<m; i++) {
        let j = 0
        for(j; j<n; j++) {
            if(a.charCodeAt(i+j) != b.charCodeAt(j)) {
                break
            }
        }
        // 在a中匹配到b
        if(j==n) {
            return i
        }
    }
    return -1
}
  1. Rabin-Karp算法

在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么,是否有别的方法可以用来判断目标字符串是否包含子串呢?

答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。这个新方法在速度上比暴力法有显著提升。

算法**:

  1. 假设子串的长度为M(pat),目标字符串的长度为N(txt)
  2. 计算子串的hash值hash_pat
  3. 计算目标字符串txt中每个长度为M的子串的hash 值(共需要计算N-M+1次)
  4. 比较hash 值:如果hash值不同,字符串必然不匹配;如果hash值相同,还需要使用朴素算法再次判断

代码模板:

function RabinKarpSearch(a, b) {
    // a:待字符串 b:需要匹配的字符串
    let m = a.length
    let n = b.length
    // 定义哈希值,因为字符是256进制 d=256; q是为了避免每次乘256数值过大,模一个大的素数
    let d = 256
    let q = 9997
    let aHash = 0, bHash = 0
    let i,j
    // 初始化匹配字符串和待匹配字符串的哈希值
    for(i=0; i<n; i++) {
        aHash = (d*aHash + a.charCodeAt(i)) % q
        bHash = (d*bHash + b.charCodeAt(i)) % q
    }
    // 计算出最高位的幂次数
    let highestPow = 1
    for(i=0; i<n-1; i++) {
        highestPow = (d*highestPow) % q
    }

    for(i=0; i<m-n+1; i++) {
        if(aHash==bHash) {
            for(j=0; j<n; j++) {
                if(a.charCodeAt(i+j) != b.charCodeAt(j)) {
                    break
                }
                if(j==n) {
                    return i
                }
            }
        }
        // 如果两个hash值不相等,利用滑动窗口**,让之前高位移除,把下一个字符的asccl加入到aHash中,整个hash移动的过程是O(1)的复杂度
        if(i<n-m) {
            aHash = (d*( aHash - highestPow*a.charCodeAt(i) ) + b.charCodeAt(i+n) )% q
            if(aHash<0) {
                aHash+=q
            }
        }
    }

    return -1
}
  1. KMP算法

KMP算法( Knuth-Morris-Pratt)的**就是,当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那一部分的字符(包括子串与目标字符串)。KMP算法的想法是,设法利用这个已知信息,不要把“搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。

参考资料:

  1. https://www.bilibili.com/video/av11866460?from=search&seid=17425875345653862171

  2. http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

js之this指向

js之this指向

JavaScript高级程序设计关于this对象的说法:

我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象同窗指向window。

全局函数

this在全局中执行环境是全局对象,this等于window

function fun(){
    console.log(this)
}
fun()
// Window

被作为某个对象调用

this被作为某个对象调用时,this指向被调用对象的活动变量

var a = 2
var obj = {
    foo: function () {console.log(this.a)},
    a:1
}
obj.foo()
// a = 1

匿名函数

this在匿名函数中,即使被调用对象使用,还是会指向全局的活动变量

var a = 2
var obj = {
    foo: (function () {
        console.log(this.a)
        })(),
    a:1
}
obj.foo

常见应用场景

var obj = {a: 1, b: function(){console.log(this);}}

  1. 作为对象调用时,指向该对象 obj.b(); // 指向obj
  2. 作为函数调用, var b = obj.b; b(); // 指向全局window
  3. 作为构造函数调用 var b = new Fun(); // this指向当前实例对象
  4. 作为call与apply调用 obj.b.apply(object, []); // this指向当前的object

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.