2018212632 / myblog Goto Github PK
View Code? Open in Web Editor NEW用于纪录前端学习的个人笔记
用于纪录前端学习的个人笔记
通过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树...
关键点:
a.左旋:右右子树时,3个结点成一个棍子状
b.右旋:左左子树时,3个结点成一个棍子状
c.左右旋:左右子树,先左旋,然后右旋
d.右左旋:右左子树,先右旋,然后左旋
e.如果有子树的情况,需要考虑相应转换:可以看下图
不足点:需要额外存储信息,且调整次数频繁
红黑树是一种近似平衡二叉搜索树,它能够保证左右子树的高度的高度差小于两倍(大的高度最多比小的两倍)。满足以下特点:
虽然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来获取属性,因此这种模式非常适合在某些安全执行环境。
它于工厂模式的不同:
缺点:仍然无法识别对象所属类型
let func = (arg1, arg2) => {
return arg1 + arg2
}
// 上面写法等价于:
let func = function(arg1, arg2) {
retrun arg1 + arg2
}
箭头函数与普通函数的不同:
箭头函数中没有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
Button.prototype.bindEvent = function() {
this.element.addEventListener("click", this.setBgColor.bind(this), false);
};
Button.prototype.bindEvent = function() {
this.element.addEventListener("click", (event)=>{this.setBgColor(event)}, false)
}
由于箭头函数本身没有this,因此会指向外层非箭头函数,即指向了Button
注意:如果将this传递给call、bind、或者apply来调用箭头函数,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为null。
在箭头函数内部直接访问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)}
因为箭头函数对于call方法,传参时会设置为null,因此new背后调用call方法,就无法通过new来调用箭头函数。
由于无法使用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,因此箭头函数适合非方法函数。
柯里化:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
例子:
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
}
}
// 示意而已
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");
例子:
// 假设我们有一个数据
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"]
创建一个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与普通函数的区别:
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
yield与return的不同:
function* gen() {
yield 123 + 456;
}
上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在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输入值的。
如果在 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
由于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 可以暂停函数执行,返回任意表达式的值。因此我们可以把异步操作放在 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
}
}
当Genreator中yield表达式中有异步操作,我们是不能直接next来调用的,因为这样无法保证异步完成再进行下一步的操作。
由此可以看到 Generator 函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权。
而两种方法可以做到这一点:
回调函数。将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权。
Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
实现Generator的逻辑:
// 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);
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这个实例继承了构造函数的属性,也继承了构造函数原型的属性。
步骤:
注意点:
如果构造函数有返回值,返回的是对象,那么实例只能访问这个对象的属性;如果返回的基本类型,那么访问的仍然是构造函数的属性
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运算符创建的实例,可以访问构造函数的属性,也可以访问构造函数原型的属性;如果构造函数有返回值,当返回值为对象时,实例访问的这个对象的属性,如果为基本类型,实例访问的仍是构造函数的属性。
本文对于组件的是什么,以及组件的挂载,组件的初始化部分源码来源于React17.0.1版本;对于生命周期参考资料是React15版本.
React主要用于构建UI,很多人认为 React 是 MVC 中的 V(视图);React本质上只处理DOM的更新和响应事件;
React以渲染函数为基础。这些函数读入当前的状态,将其转换为目标页面上的一个虚拟表现。只要React被告知状态有变化,他就会重新运行这些函数,计算出页面的一个新的虚拟表现,接着自动把结果转换成必要的DOM更新来反映新的表现。
用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');
};
从注释代码中,可以看出:
为什么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组件有一下属性的对象:
我们知道可以通过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进行处理:
之后执行componentShouldUpdate。这说明在 componentShouldUpdate 所有的state会被合并处理。
最后如果 componentShouldUpdate 为true,执行_performComponentUpdate方法,该方法中执行componentWillUpdate生命周期方法,更新完成后执行componentDidUpdate方法。
整个流程:
React实现了SyntheticEvent层处理事件,合成事件会将所有我们在jsx中编写的事件进行拦截,并进行一些封装变成一个React的事件,最终只会绑定一个事件到document元素中,通过事件冒泡的方式传递到绑定到document的统一事件进行分发。
总结:
需要注意的是,绑定事件之前会通过 isInteractiveTopLevelEventType 函数检测当前事件类型是否React支持的事件类型,如果当前的事件并不是React配置中所处理的事件,那么将会直接绑定 dispatchEvent ,否则绑定的将会是 dispatchInteractiveEvent 。区别在于dispatchEvent 不会异步setState,而 dispatchInteractiveEvent 会异步 setState。
在触发阶段,通过事件的触发 dispatchEvent/dispatchInteractiveEvent(前者不会异步setState),找到事件源对象上的对应事件的回调函数,并组合成一个"react-事件名"的自定义事件,通过创建一个 react 元素,通过 element.dispatchEvent 函数自主触发事件。
在触发阶段,如果父级元素绑定了同样事件名的函数,那么会冒泡一层一层触发。可以在子组件中使用 event.stopPropagation() 来阻止父组件的触发。
在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,结果仅仅是轻量级的 JavaScript 对象,我们称之为 virtual DOM。
虚拟 DOM 是 React 的一大亮点,具有 batching(批处理) 和高效的 Diff 算法。
传统的diff算法通过循环递归的方式对比新老树来更新dom树,需要的时间复杂度是O(n^3)
ReactV16.8中利用其特殊的diff算法做到了O(n^3)到O(n)提升,依赖于下面这三条的diff策略:
tree diff
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
component diff
因此当我们对于一些修改比较小的同类型组件,可以使用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的地方都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的大致流程为:
// 循环的方式
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),因为有很多重复计算的结点,因此可以通过记忆化优化,通过另外的数组存在已经算过的结点,如果发现计算了直接使用。大多数递归都可以都过自顶向上的循环来改写。
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语言环境
要求:找出数组任意重复的数字
for(let i=0; i<nums.length; i++) {
for(let j=0; j<nums.length; j++) {
}
}
Last Recently Use Cache:
js中基于map来实现LRU;map本身有两个特性:
LRU.get:
LRU.put:
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缓存常用于优化计算,把最近使用作为可能尝使用作缓存来减少计算量。
bind():方法会创建一个新函数A。当函数A被调用时,bind()的第一个参数会作为它运行时的this,之后的一序列参数将会在实参前传入作函数A的参数。
总结bind两个特点:
思路:
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的原型的属性。
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。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
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
弱引用:
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
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会自动清除这部分内存。
当我们需要在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的属性。
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进行求解,题目本身的约束条件是剪纸重要的依据。
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise简单的来说是一个对象,里面保存的是一个未来才会结束的事件(通常是一个异步操作)的结果。比如:我们在向服务器发起一个请求的时候,对请求处理的一些逻辑放在promise中,请求结果就是未来才会结束的事件结果。
ES6中规定,Promise是一个构造函数,用来创建promise实例。
构造函数接收两个参数resolve和reject。它们是两个内置函数:
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()函数会返回一个和原来不同的新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
下面代码输出什么?
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
分析:
回调地狱的导致了什么问题?
因此,回调函数不仅导致了代码可读性下降,也涉及到其它的问题。
promise怎么来解决的问题:
我们再使用第三方API的时候可能会遇到以下问题:
Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 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 的结果。
// bad
function anAsyncCall() {
var promise = doSomethingAsync();
promise.then(function() {
somethingComplicated();
});
return promise;
}
// good
function anAsyncCall() {
var promise = doSomethingAsync();
return promise.then(function() {
somethingComplicated()
});
}
// 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内部的抛出的异常并不会阻断外部代码
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 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
为什么需要单元测试:
常用的测试框架:
参考资料:
合作开发时,为了防止一些不规范的代码commit并push到远端;可以在git命令执行前用一些狗子来检测并阻止。目前前端流行的git钩子插件:husky,pre-commit
一段husky的配置代码:
"husky": {
"hooks": {
"pre-commit": "npm run lint", // 在git commit之前执行 "npm run lint"
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS" // 检测提交信息的规范
}
}
使用:我们可以在提交前,执行test或者eslint的规范,来让提交的代码规范化。
用于规范commit说明的工具。
良好的提交信息应该有:
git-cz时,一些type的意思参考地址:https://juejin.im/post/6844903831893966856
ESLint 是一个开源的 JavaScript 代码检查工具,由 Nicholas C. Zakas 于2013年6月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。
JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。
有两种主要的方式来配置 ESLint:
配置可参考:
在JavaScript中0.1+0.2 == 0.3?不是的,0.1 + 0.2 != 0.3 ,原因是浮点数在贮存的过程精度丢失导致的。
js中浮点数采用IEEE754双精度储存,用64个bit来存贮浮点数。其中对于浮点先转换成二进制数,对二进制数进行存储。
在这个标准中:
比如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"
// 暴力法
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,不加括号,得到的预期不符。
遍历链表,利用栈先进后出的**,模拟存贮进一个数组。time:O(n) space:O(n)
unshift()方法将元素往左添加进数组,并返回该数组的新长度(该方法会修改原有的数组)
var reversePrint = function(head) {
let a = []
while(head != null) {
a.unshift(head.val)
head = head.next
}
return a
};
链表的读取操作复杂度是O(n)的,因为链表只能通过遍历一遍才能找到访问的元素。删除和插入操作是O(1)的复杂度。
奇偶判断:
x%2 == 1 <--> (x&1) == 1
x%2 == 0 <--> (x&1) == 0
x>>1 <--> x/2;常用于二分查找时,位运算计算更快
x = x&(x-1)将x最低位的1清零
x&(-x)得到最低位的1;负数的二级制以补码形式存在,原码、取反、加1(0001)。
x&(~x) --> 0
不建议在 js 中使用位运算,理由是 js 天生就会进行类型转换,使得效率降低。为了保持可读性,尽量避免使用复杂的位运算。
// 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 ?
var reverseBits = function(n) {
let res = 0
let count = 32
while(count--) {
res *= 2 // 可以用 res << 1 替换
res += n&1
n>>=1
}
return res
};
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
};
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 了。
解决:
var arr = []
for(var i=0; i<10; i++) {
(function(val) {
arr[i] = function() {
console.log(i)
}
})(i)
}
arr[3]()
var arr = []
for(let i = 0; i<10; i++) {
arr[i] = function() {
console.log(i)
}
}
arr[3]()
const:const声明一个只读的常量。一旦声明,常量的值就不能改变。const的作用域与let命令相同:只在声明所在的块级作用域内有效。
本质:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
// ES6
let a = 1
// after babel
"use strict";
var a = 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
// 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]();
不会绑定全局作用域
块级作用域(解决之前闭包来写块级作用域的痛点)
暂时性死区(不会被变量提升)
重复声明报错(约束代码规范,避免重复命名差错)
split()方法将字符串按指定的分割符将字符串切割成子字符串数组。
join()方法以指定分隔符将数组成员拼接成以该分隔符连接的字符串。
var replaceSpace = function(s) {
if (typeof s == "string" && s.length >= 0 && s.length <= 10000) {
return s.split(' ').join('%20');
}
return '';
};
读于字符串的操作,不能直接 for 循环修改 s[i] 的值,通常转换成数组最后转换字符串操作。
惰性载入表示函数执行的分支仅会发生一次。
实现惰性函数的方法有两种,一种是在执行分支的时候覆盖函数;另一种使用函数表达式,在声明函数的时候就给每个分支指定适当的函数。
// 方式一
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的可执行代码包含:全局代码、函数代码、eval()
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弹出。
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)
};
根据二叉树中序、前序遍历顺序找到根节点,左右子树的个数,由递归的**重构二叉树;
let obj ={name:'jack',age:'20'};
// 字符串拼接
console.log('我的名字'+obj.name+'年龄是'+obj.age);
// ES6的模板字符串
console.log(`es6 我的名字${obj.name},年龄是${obj.age}`);
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。
标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
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()的参数:
// 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开始
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
// after deal, message is:
"<p><script>alert("abc")</script> has sent you a message.</p>"
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>
`;
出于可读性或者其他原因,我希望书写的时候是换行的,但是最终输出的字符是在一行,这就需要借助模板标签来实现了,我们尝试写一个这样的函数:
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 库进行使用。
有数组特征的对象,它有length属性;常见的类数组对象arguments、getElementsByTagName
let a = {'0':1, 'length':1}
a.push() // 抛出错误
可以发现类数组对象并没有数组的原型上的方法,比如push、slice、concat
怎么转换一个类数组对象:
以上四种方法中性能排行:
浅拷贝常用的方法,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
}
深拷贝虽然不会产生副作用,但是因为递归实现会比较耗性能;因此在实际开发过程中,尽量避免数组出现对象或数组,让数组拷贝为浅拷贝。
当执行一段JavaScript代码时,会创建相应的可执行上下文。对于每个上下文都有三个重要属性:
变量对象是与可执行上下文相关的数据作用域,储存了在上下文定义的变量和函数声明。
因为不同上下文的变量对象稍有不同,大致分为全局上下文的变量对象和函数上下文的变量对象
全局对象是预定义的对象,作为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属性初始化;即进入函数上下文时,活动变量上的属性才能被访问。
执行上下文的代码会被分成两个阶段处理:
当进入执行上下文时,这个时候并没有执行代码
变量对象会包括:
函数的形参(如果是函数上下文)
函数声明
变量声明
例子分析:
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",
}
比较类排序:通过比较来确定元素次序,常常对对象、类、字符串之类根据关系来进行排序,由于其时间复杂度不能突破O(nlog(n)),因此也称作非线性时间比较类排序
非比较类排序:不通过比较来决定元素间相对次序,常常对数字整形进行比较,通过额外的空间辅助,常常能达到线性的时间排序,因此也称为线性时间非比较类排序。
排序:比较类和非比较类
比较类:
交换排序
a. 冒泡排序
b. 快速排序
插入排序
a.简单插入排序
b.希尔排序
选择排序
a.简单选择排序
b.堆排序
归并排序
a.二路归并排序
b.多路归并排序
非比较类:
初级排序包含了选择排序、插入排序、冒泡排序,他们时间复杂度都是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
}
归并排序思路:
代码模板
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出来,就得到了排序之后的数组。
只有函数拥有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)的原型;实例会从原型继承属性。
每一个JavaScript对象都有一个_proto_属性,这个属性指向对象的原型,即:
function animal() {
}
let dog = new animal()
console.log(dog.__proto__ === animal.prototype) // console result --> true
这说明实例对象和构造函数都可以指向原型
原型并没有指向实例的属性,不过指向构造函数的就是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原型的原型的链状逻辑,即图中蓝色部分。
为什么要使用迭代器和 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())
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 就是把对象的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集合内建了以下三种迭代器:
以数组为例:
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() 方法。
闭包在高程中的定义:闭包是指有权访问另一个函数作用域中的变量的函数;创建闭包的常见方式,就是在一个函数内部创建另一个函数。
闭包在MDN中的定义:闭包是指那些能够访问自由变量的函数;自由变量是指在函数中使用,但既不是函数参数,也不是局部变量。
因此产生两个理解,理论上和实践上理解闭包:
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指向当前执行作用域,一般来说指向函数,函数为匿名函数则指向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的垃圾回收机制处理,因此也就造成内存泄漏。
垃圾收集机制的原理:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
引用:在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
引用计数:此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:循环引用,如果两个对象相互引用,那么垃圾回收机制无法判定对象不再需要,因此这部分内存并不会被回收。DOM中同样存在循环引用的情况,也会造成内存泄漏。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
标记清除算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫根的对象(在JavaScript中,根是全局对象)。垃圾回收器将定期从根开始,找到所有从根引用的对象,然后找到这些对象引用的对象......从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
call()方法,函数A调用call()方法,传递一个this指向的函数或方法,从而改变函数A的this指向
var foo = {
value : 1
}
function bar() {
console.log(this.value)
}
bar.call(foo) // 1
发生了什么:
模拟的步骤可以为:
一些需要考虑的边界情况:
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与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
}
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数举例,如果用递归+记忆化搜索其实就是动态规划,不过动态规划一般习惯思维是自底向上,而分治+最优子结构是一种自顶向下思考的方式。
function f(n) {
if(n<=1) return 1
return f(n-1) + f(n-2)
}
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)
}
function f(n) {
dp = [1, 1]
for(let i=2;i<=n; i++) {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
function f(n) {
fn0 = 1
fn1 = 1
fn2
for(let i=2;i<=n; i++) {
fn2 = fn1 + fn2
fn0 = fn1
fn1 = fn2
}
return fn2
}
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)
}
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]
}
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]
}
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
}
// 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]:
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])
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 函数的区别:
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
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 () => {
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);
})();
基本步骤:
async function handleList() {
const listPromise = await getList();
// ...
await submit(listData);
}
async function handleAnotherList() {
const anotherListPromise = await getAnotherList()
// ...
await submit(anotherListData)
}
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);
}
}
尽管我们可以使用 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');
}
}
Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。
async 函数返回一个 Promise 对象
面对复杂的异步流程,Promise 提供的 all 和 race 会更加好用
Promise 本身是一个对象,所以可以在代码中任意传递
async 的支持率还很低,即使有 Babel,编译后也要增加 1000 行左右。
由于朴素递归的状态空间一般很大,很多结点重复计算,就需要剪枝进行优化
// 暴力法:每一行的检测,每一列的检测,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来说,通过从双端来进行优化搜索时间。
思路:
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,终结条件变成两端访问过同一个结点
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)会返回一个非负实数,可以认为返回值越大越有希望找到目标结点。
节流的原理:如果你持续触发事件,每隔一段时间,只执行一次事件。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 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,那么我们让第一次的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;
}
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
改变前:
栈内存 | 堆内存 | ||
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 |
用两个栈实现队列添加元素、删除操作;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)。
在前端开发中会遇到一些频繁的事件触发,比如:
引用一个例子:
<!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 秒内不再触发事件,我才执行。
实现效果:使用debounce函数后,返回一个函数,这个函数在设置n秒后,才触发回调事件;即不论触发多少次,事件一定是在触发后n秒才执行,
实现基本思路:
实现的功能:
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来确定元素是否存在数据库中。
## 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"))
当查找变量时,先从当前执行上下文的变量对象中查找,如果没有找到;继续沿上一层执行上下文的变量对象中查找,如果也没查到,一直重复这个过程直到全局变量;这样由多个执行上下文变量对象构成的链表叫做作用域链。
一个函数的创建和激活两个时期作用域链是怎样变化的?
js的词法作用域是静态作用域,即函数创建的时候作用域也就创建了;这是因为函数有一个[[socpe]]属性,当函数创建的时候,就会把所有父级变量对象保存到[[socpe]]这个属性中;但是[[socpe]]这个属性并不等同于整个作用域链。
当函数激活(执行)时,进入函数上下文,就会把活动变量添加到作用域链的前端。
作用域与js代码与可执行栈、变量对象息息相关,通过分析一个函数创建到执行过程:函数定义的时候,首先会将其父级作用域添加到[[scope]]属性;执行函数,创建函数上下文并压入可执行栈;随后函数不会马上执行,先复制函数[[scope]]属性创建作用域,然后创建函数的活动变量并压入作用域的顶端,最后函数的作用域就会包含当前活动变量和之前父级作用域;执行函数,根据代码修改活动变量的值;函数执行完,函数上下文出栈。
c中数组的特点:相同类型的元素、连续的内存、索引
与c的数组,即标准数组
在c语言中,创建数组,就是向内存申请一块固定大小的内存空间;空间的大小根据数组长度和数据类型来得到的。
在申请一个标准数组后,cpu会根据类型和大小来申请一个固定的空间;并纪录这个空间的首地址为headAddress;寻址公式 array[i]Address = headAddress + i*dataTypesize;如果数组长度随便改变,那么可能会覆盖后面内存的空间,处于安全考虑,即大小固定;如果下标从1开始,那么寻址公式会变成 array[i]Address = headAddres + (i-1)*dataTypesize,那么每次寻址会多进行一次减法计算,从0开始避免多余的计算。
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类型
数组长度,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
密集数组也称快数组,稀疏数组也称慢数组;在v8底层,快慢数组是会发生相互转换的。
快数组转换成慢数组:
慢数组转换成快数组:
v8底层对数组长度远远过大时,会把数组转换成慢数组,也就是hash的存储结构,牺牲效率来节省内存空间;而快数组不在比慢数组节省50%的空间时,数组会转换成快数组,申请大块连续内存,来提高效率。
ES6 引入了一种新的原始数据类型 symbol,表示独一无二的值。symbol 的一些特性:
const a = Symbol('a')
typeof a // symbol
const a = Symbol('a')
a instanceof Symbol // false
const a = Symbol('a')
console.log(a) // Symbol(a)
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)
var a1 = Symbol()
var a2 = Symbol()
console.log(a1===a2) // flase
var b1 = Symbol('tom')
var b2 = Symbol('tom')
console.log(b1===b2) // flase
var b1 = Symbol('tom')
b1 + 'as' // Uncaught TypeError: Cannot convert a Symbol value to a string
var b1 = Symbol('tom')
console.log(String(b1)) // Symbol(tom)
console.log(b1.toString()) // Symbol(tom)
var objName = Symbol()
// 第一种写法
var obj1 = {}
obj1[objName] = 'tom'
// 第二种写法
var obj2 = {
[objName]: 'tom'
}
// 访问这个属性
console.log(obj[objName]) // 'tom'
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)]
var s1 = Symbol.for('address')
var s2 = Symbol.for('address')
console.log(s1 === s2) // true
var globalSym = Symbol.for('foo')
console.log(Symbol.keyFor(globalSym)) // foo
var localSym = Symbol()
console.log(Symbol.keyFor(localSym)) // undefined
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
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
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是静态作用域。
作用域链:函数活动对象 --> 包含环境的活动对象--> .. -->全局环境变量对象
Set是E6中新型的数据结构,类似数组,但是元素成员没有重复的,是唯一的。
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)}
操作方法有:
遍历的方法:
let s_arr = new Set([1, 2, 3])
s_arr.forEach( (key, value) => {console.log(key, value)}) // forEach不会改变原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
执行上下文包含了上下栈的顺序、作用域链和上下文创建。
创建可执行上下文栈,上下文包含(VO, Scope, this)
函数创建时,将父级作用域保存到函数内部属性[[Scope]],即把父级的上下文的VO添加到函数内部属性[[Scope]]
函数上下文进栈,初始化函数上下文:
函数执行,执行相应的代码,也可以修改相应活动变量的值;执行完毕,函数上下文出栈
比较下面两段代码,试述两段代码的不同之处
// 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()();
Estack = {
globalContext
}
globalContext = {
VO: global,
Scope: [globalContext.Vo],
this: globalContext.Vo
}
checkscope.[[Scope]] = {
globalContext.Vo
}
Estack = {
checkscopeContext,
globalContext
}
checkscopeContext = {
VO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [VO, globalContext.Vo]
}
f.[[Scope]] = {
chechscopeContext.VO,
globalContext.VO
}
Estack = {
globalContext
}
Estack = {
fContext,
globalContext
}
fContext = {
VO: {
arguments: {
length: 0
}
},
Scope: [VO, chechscopeContext.VO, globalContext.VO]
}
Estack = {
globalContext
}
A,B代码虽然最后返回值是一样,但是在可执行栈的环境是不同的
在JavaScript中,字符串是immuatable(不可变的)。
不可变的意思是,如果每次改变字符串里面的内容,本质上是创建了一个新的字符串。
var myString = "abbdef";
myString[2] = 'c'
console.log(myString) // abbdef
我们发现,令myString[2]赋值为'c',原来的字符串并没有发生改变。
trim、slice等字符串操作方法返回新的字符串。
immuatable好处在于在多线程中式安全的。
string的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
};
从末尾开始遍历,找到第一个不为空的下标,标记为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
};
哈希存储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;
};
计数法,用一个数组存贮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
};
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中什么位置出现。
常用的算法:
代码模板:
/**
* @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
}
在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么,是否有别的方法可以用来判断目标字符串是否包含子串呢?
答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。这个新方法在速度上比暴力法有显著提升。
算法**:
代码模板:
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
}
KMP算法( Knuth-Morris-Pratt)的**就是,当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那一部分的字符(包括子串与目标字符串)。KMP算法的想法是,设法利用这个已知信息,不要把“搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。
参考资料:
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);}}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.