算法
验证回文字符串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。就是倒过来也一样
var isPalindrome = function(s) {
var _s = s.toLocaleLowerCase().replace(/[^a-z0-9]/g, ''),
low = 0,
high = _s.length - 1;
while(low < high){
if(_s[low] !== _s[high]){
return false;
}
++low;
--high;
}
return true
};
字符串转整数 (atoi)
实现 atoi,将字符串转为整数。
var myAtoi = function(str) {
var num = parseInt(str);
var INT_MAX = 2147483647;
var INT_MIN = -2147483648;
if (isNaN(num)) {
return 0;
}
if (num > INT_MAX) {
return INT_MAX;
}
if (num < INT_MIN) {
return INT_MIN;
}
return num;
};
实现 strStr()
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
var strStr = function(haystack, needle) {
if(needle.length === 0 || haystack.length <= needle.length){
if(haystack === needle || needle.length === 0){
return 0
}else{
return -1
}
}
for(var i=0; i<haystack.length; i++){
if(haystack[i] === needle[0]){
var _idx = i;
for(var j=0; j<needle.length; j++){
if(haystack[i+j] !== needle[j]){
_idx = -1;
break;
}
}
if(_idx !== -1){
return _idx
}
}
}
return -1
};
数数并说
报数序列是指一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:
- 1:1
- 2:11
- 3:21
- 4:1211
- 5:111221
- 6:312211
- 7:13112221
var countAndSay = function(n) {
if(n <= 0) return '';
var str = '1';
var newStr = str;
while(--n){
str = newStr;
newStr = '';
let num = 0;
let numStr = '';
for(var i=0; i<str.length; i++){
if(numStr !== str[i]){
if(i !== 0){
newStr += (num+numStr);
num = 0;
}
numStr = str[i];
}
num++;
}
newStr += (num+numStr);
}
return newStr
};
最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ““。
var longestCommonPrefix = function(strs) {
if(!strs.length) return ''
var _str = strs[0];
var cmn = '';
for(var i=0; i<_str.length; i++ ){
var _tem = _str[i];
var tag = true;
for(var j=0;j<strs.length; j++){
if(_tem !== strs[j][i]){
tag = false;
break;
}
}
if(tag){
cmn += _tem;
}else{
break;
}
}
return cmn;
};
删除链表中的节点
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
- head = [4,5,1,9], node = 5
- [4,1,9]
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
var removeNthFromEnd = function(head, n) {
let dummy = new ListNode(0)
dummy.next = head
let first = dummy
let second = dummy
for (let i = 1; i <= n + 1; i++){
first = first.next
}
console.log(first)
while(first != null){
first = first.next
second = second.next
}
second.next = second.next.next
return dummy.next
};
反转链表
递归实现
var reverseList = function(head) {
if(head === null || head.next === null){
return head;
}else{
var newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead
}
};
迭代实现
var reverseList = function(head) {
if(head === null || head.next === null){
return head;
}else{
var prev = null;
while(head){
var newHead = new ListNode(head.val);
head = head.next;
newHead.next = prev;
prev = newHead;
}
return prev
}
};
合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
var mergeTwoLists = function(l1, l2) {
var next1 = l1;
var next2 = l2;
var head = {};
var next = head;
while(next1 && next2){
if(next1.val < next2.val){
var node = new ListNode(next1.val);
next1 = next1.next;
}else{
var node = new ListNode(next2.val);
next2 = next2.next;
}
next.next = node;
next = node;
}
next.next = next1 || next2;
return head.next
};
回文链表
请判断一个链表是否为回文链表。
var isPalindrome = function(head) {
if(!head || !head.next) return true;
var prev = new ListNode(head.val);
var l1 = head;
while(l1.next){
var node = new ListNode(l1.next.val);
node.next = prev;
prev = node;
l1 = l1.next;
}
while(prev && head){
if(prev.val === head.val) {
prev = prev.next;
head = head.next;
}else{
return false
}
}
return true
};
判断环形链表
给定一个链表,判断链表中是否有环。在当前指针上添加额外的参数来判断,算法必须要循环 N 次。
var hasCycle = function(head) {
if(!head) return false;
while(head){
if(head.name){
return true;
}
head.name = true;
head = head.next;
}
return false
};
两个指针比快慢,如果两个指针相遇说明有环
var hasCycle = function(head) {
let fast = head;
let slow = head;
while (fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
if (fast === slow) {
return true;
}
}
return false;
};
二叉树的最大深度
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
var maxDepth = function(root) {
var max = 0;
if(!root) return max;
const findRoot = (tree, len) => {
if(!tree.left && !tree.right){
max = Math.max(max, len);
}
tree.left && findRoot(tree.left, len + 1);
tree.right && findRoot(tree.right, len + 1);
}
findRoot(root, 1);
return max
};
最大和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
var maxSubArray = function(nums) {
var total = nums[0];
var arrNum = nums[0];
for(let i=1; i<nums.length; i++){
arrNum = Math.max(arrNum + nums[i], nums[i]);
total = Math.max(total, arrNum);
}
return total;
};
ES6
let 和 const
在前端终于有了块级作用域。不能在同一个块级作用域中重复声明,babel 的实现是,保证在当前作用域中,不会有其他作用域的命名与当前 let 命名重复:
// 编译前
let num = 1;
// 编译后的结果
var num1 = 1;
const 指的是存储地址不能发生改变,如果这个值是个引用类型,是允许引用地址里的值发生改变,但是不能指向新的引用地址。babel 在编译阶段如果发现重新赋值到其他的地址就会报错。
解构
数组的解构赋值
下面是一些类型的结构,结构还可以设置默认值,但是只有当值等于 undefined 时才会对结构进行赋默认值。
let [x, y, ...z,a = 0] = ['a'];
// x = 'a'
// y = undefined
// z = []
// a = 0
// f 不会执行,只有当对应的值是 undefined 时才会执行
let [x = 1, y = 2] = [undefined, null];
// x = 1
// y = null
对于 Set 结构,也可以使用数组的结构赋值
let [x, y, z] = new Set(['a', 'b', 'c']);
// x = "a"
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
对象的解构赋值
对象的解构赋值和数组类似
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
// line = 1
// loc = Object {start: {line:1,column:5}}
// start = Object {line: 1, column: 5}
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
// obj = {prop:123}
// arr = [true]
字符串解构
const [a, b, c, d, e] = 'hello';
// a = h, b = e ...
数值和布尔值的解构赋值
let {toString: s} = 123;
s === Number.prototype.toString // true
函数的解构
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
解构出错
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c
解构用途
// 交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
// 返回多个函数值赋值
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 函数参数的定义及附默认值
function f([x = 0, y, z]) { ... }
f([1, 2, 3]);
// 遍历 map
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
字符串扩展
字符串 for…of
for (let codePoint of 'foo') {
console.log(codePoint)
}
字符串新 API
// 字符串重复
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
// 字符串重复
'hello'.repeat(2) // "hellohello"
// 字符串补全
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
// 模板字符串
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// Hello Bob, how are you today?
// 模板字符串嵌套
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
<table>
<tr><td><Jane></td></tr>
<tr><td>Bond</td></tr>
<tr><td>Lars</td></tr>
<tr><td><Croft></td></tr>
</table>
// 模板标签
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
数值的扩展
// 判断整数
Number.isInteger(14) // true
Number.isInteger(3.0000000000000002) // true
// 返回整数部分
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
// 返回整数负数还是0
Math.sign()
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
函数的扩展
// 默认值,使用参数默认值时,函数不能有同名参数
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
默认值的作用域
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
rest 参数,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
// 报错
function f(a, ...b, c) {}
常见的箭头符号书写,箭头函数不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
let foo = () => { a: 1 };
foo() // undefined
箭头函数绑定 this
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
尾调用优化,我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。
尾递归,递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
- func.arguments:返回调用时函数的参数。
- func.caller:返回调用当前函数的那个函数。
数组的扩展
// ...运算符,并且有浅拷贝的作用
console.log(1, ...[2, 3, 4], 5)
Array.from
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike);
Array.of方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
find() 和 findIndex()
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
fill
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
数组实例的 entries(),keys() 和 values()
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
includes()
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
对象的扩展
Object.is() 相等
Object.is('foo', 'foo') // true
Object.is({}, {}) // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign() 合并
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
对象的枚举
(1)for…in
for…in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
Object.entries 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptor 方法会返回某个对象属性的描述对象,主要是为了解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题。
super 关键字, 下面例子中 super.foo 等同于 Object.getPrototypeOf(this).foo(属性)或 Object.getPrototypeOf(this).foo.call(this)(方法)。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
symbol、Set、Map
symbol
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。
// symbol 的申明
let s = Symbol();
typeof s
// "symbol"
// symbol 不相等
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
Symbol 枚举
Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
symbol.for() 和 symbol.keyFor()
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
在不同的 iframe 中 symbol.for() 的值也是相同的。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
Symbol.hasInstance 属性
对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是FooSymbol.hasInstance。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
Symbol.species
对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // true
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
Symbol.match
对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
Symbol.replace
对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
Symbol.search
对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
Symbol.split
对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 例一 参数为数组
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二 返回size
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三 参数为类数组
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
Set 结构的实例有以下属性。
- Set.prototype.constructor:构造函数,默认就是Set函数。
- Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
- add(value):添加某个值,返回 Set 结构本身。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
可以通过 Array.from 方法将 Set 结构转为数组
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
map 的初始化,set 值时,后面的会覆盖前面的,map 的 key 是用地址来判断的,值相同的两个引用地址不同的 key 会被当成两个 key
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
// 接受数组
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
属性和方法
- size:属性返回 Map 结构的成员总数。类似 length
- set(key, value):set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
- get(key):get方法读取key对应的键值,如果找不到key,返回undefined。
- has(key):has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
- delete(key):delete方法删除某个键,返回true。如果删除失败,返回false。
- clear():clear方法清除所有成员,没有返回值。
遍历
Map 的遍历顺序就是插入顺序。
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历 Map 的所有成员。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合,他们的关系类似 set 和 WeakSet。WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制
就不展开详述了
proxy、Reflect、promise、generator、async
proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。但是 this 会有问题
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
复杂一点的例子
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
Reflect
将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
promise
- Promise.prototype.catch:promise 内部发生错误时的回调
- Promise.prototype.finally:不管 Promise 状态如何都会执行 finally 方法
- Promise.all():所有的 promise 都是 resolve 才会进 resolve,只要有一个 reject 就会进 reject
- Promise.race():第一个返回的结果作为 race 的结果
// 三种状态
const PENDING = Symbol()
const FULFILLED = Symbol()
const REJECTED = Symbol()
function Prometheus (fn) {
// fn 必须是函数
if (typeof fn !== 'function') {
throw new Error('fn must be a function!')
}
let state = PENDING // 初始状态是 PENDING
let value = null // 返回值
let handler = {}
function fulfill (result) {
state = FULFILLED
value = result
next(handler)
}
// 完成时调用的方法,这里做了容错
function resolve (result) {
try {
fulfill(result)
} catch (err) {
reject(err)
}
}
// 拒绝时调用的方法
function reject (error) {
state = REJECTED
value = error
next(handler)
}
function next({ onFulfill, onReject }) {
switch (state) {
case FULFILLED:
onFulfill && onFulfill(value)
break
case REJECTED:
onReject && onReject(value)
break
case PENDING:
handler = { onFulfill, onReject }
}
}
this.then = function (onFulfill, onReject) {
return new Prometheus((resolve, reject) => {
next({
onFulfill: val => {
resolve(onFulfill(val))
},
onReject: err => {
reject(onReject(err))
}
})
})
}
fn(resolve, reject)
}
async 和 generator
// readFile 是个 promise
// generator
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
// async reject 需要 try catch 来捕获
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
查漏补缺
CSS 盒模型
- 标准盒模型:width = content width
- IE 盒模型:width = content width + padding + border
css 布局
圣杯布局
左右两边固定,中间自适应;
#container .column {
height: 200px;
position: relative;
float: left;
}
#center {
background-color: #e9e9e9;
width: 100%;
}
#left {
background-color: red;
width: 200px; /* LC width */
right: 200px; /* LC width */
margin-left: -100%;
}
#right {
background-color: blue;
width: 150px; /* RC width */
margin-right: -150px; /* RC width */
}
// 或者 container overflow:hidden 清除浮动
#footer {
clear: both;
}
BFC
BFC就是“块级格式化上下文”的意思,创建了 BFC的元素就是一个独立的盒子,不过只有Block-level box可以参与创建BFC, 它规定了内部的Block-level Box如何布局,并且与这个独立盒子里的布局不受外部影响,当然它也不会影响到外面的元素
BFC有一下特性:
- 内部的Box会在垂直方向,从顶部开始一个接一个地放置。
- Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生叠加
- 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
- BFC的区域不会与float box叠加。
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。
- 计算BFC的高度时,浮动元素也参与计算。
圣杯布局就是用到了 BFC,默认情况下,如果只是给左边的元素加 float:left,中间和左边的元素会重叠,但是给整个父级加一个 position:relative 就会触发 BFC
flex 布局
容器上写是 display:flex,容器上就具备以下属性。
flex-direction:主轴方向
flex-wrap:如果一条轴线排不下,如何换行
flex-flow:flex-direction 和 flex-wrap 的简写
justify-content:主轴上的对齐方式
align-items:交叉轴上如何对齐
align-content:多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用
子属性:
order:定义项目的排列顺序。数值越小,排列越靠前,默认为0
flex-grow:属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大
flex-shrink:属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小
flex-basis:定义了在分配多余空间之前,项目占据的主轴空间(main size)
flex:flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto
align-self:属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性
中间件的实现
express
use 的时候将中间件的都 push 到一个队列中,然后使用的时候遍历队列,拿出来执行回调,传入 req、res、next。next 是一个回调,next 再去执行下一个回调。
koa
也是和上面的差不多,不过队列中的任务都是 generator。
iOS input 下无法 focus
input[type='text'],textarea{
-webkit-user-select:auto;
-moz-user-select:auto;
user-select:auto;
}
graphql
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
Vue 和 react 的区别
总结一下,我们发现的,Vue的优势是:
- 模板和渲染函数的弹性选择
- 简单的语法和项目配置
- 更快的渲染速度和更小的体积
React的优势是:
- 更适合大型应用和更好的可测试性
- Web端和移动端原生APP通吃
- 更大的生态系统,更多的支持和好用的工具
然而,React和Vue都是很优秀的框架,它们之间的相似之处多过不同之处,并且大部分的优秀功能是相通的:
- 用虚拟DOM实现快速渲染
- 轻量级
- 响应式组件
- 服务端渲染
- 集成路由工具,打包工具,状态管理工具的难度低
- 优秀的支持和社区
throttle
var throttle = function(delay, action){
var last = 0
return function(){
var curr = +new Date()
if (curr - last > delay){
action.apply(this, arguments)
last = curr
}
}
}
debounce
var debounce = function(idle, action){
var last
return function(){
var args = arguments
clearTimeout(last)
last = setTimeout(() =>{
action.apply(this, args)
}, idle)
}
}
delegate 的实现
function delegate(parent, type, selector, fn){
parent.addEventListener(type, fuction(e){
var target = e.target;
var current = e.currentTarget;
var bubble = true;
while(bubble && target != current){
if(filiter(agent, selector, target)){
bubble = fn.call(target, e);
}
target = target.parentNode;
return bubble;
}
}, false);
function filiter(parent, selector, target){
var nodes = parent.querySelectorAll(selector);
for(var i=0; i<nodes.length; i++){
if(nodes[i] === target) rertun true
}
}
}
测试案例
String.prototype.trim = function(){
return this.replace(/^\s|\s$/g, '');
}
// TDD是“测试驱动的开发”
suite('trim', function() {
test('test trim string', function() {
var aaa = ' a a ';
assert.equal(aaa.trim(), 'a a');
});
});
// BDD是“行为驱动的开发”
describe('trim', function() {
it('test trim string', function() {
var aaa = ' a a ';
assert.equal(aaa.trim(), 'a a');
});
});
PWA 实现
基于 service worker,ServiceWorker 能访问 cache,可以将一些 js 文件缓存到本地,可以监听所有的请求和初始化,也可以推送消息。最后通过一个 mainfest 文件进行展示。
getBoundingClientRect() 来获取页面元素的位置
document.querySelector('.demo').getBoundingClientRect(),获取之后会引发重排
严格模式
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
weex 分析
- weex 初始化 SDK,加载 js framework
- 注册 Component、module、handle
weex 初始化 SDK,WXSDKEngine
[self _registerDefaultComponents];
[self _registerDefaultModules];
[self _registerDefaultHandlers];
当注册好以后不一定是会直接注册到 js framework 中。如果 framework 没有加载好,就会放在一个堆栈中,等 js framework 加载好后再对栈进行遍历将 Component、module 通过 bridge 进行注册。
js framework 执行在一个线程中,线程会有个 run loop 来保证线程一直是活的。
- 挂载全局属性方法及 VM 原型链方法
- 创建于客户端通信桥
- 弥补环境差异
js bundle 的执行。
- 初始化生命周期
- 数据双绑
- 模板解析
- 绘制 Native UI
事件传递,在初始化 js bundle 的时候,会传递事件类型,和一个 ref 过去,客户端会将这个事件绑定到这个 UI 上,
taskCenter.send(
'dom',
{ action: 'addEvent' },
[this.ref, type]
)
当事件触发之后,客户端会抛一个事件出来,会带有这些数据,ref、type、event、domChanges,一开始还会有 weex 实例 ID,往上传递的时候再进行剥离。传递给 jsfm,jsfm 找到对应的前端 js 事件进行触发,如果事件会对 DOM 进行改动,这个时候又会触发更行。
方法论
最小发布 -> 高内聚
最短依赖 -> 低耦合
webpack 性能优化
常规手段:tree shacking、抽取公共代码、代码压缩混淆、loader 编译去除 node_module、根据不同环境选择不同的 devtool。
happypack:让 loader 可以多进程去处理文件
cacheDirectory:重新构建时用缓存来构建没有修改过的文件不会重新编译。
减少构建搜索或编译的路径,DLL 针对第三方 NPM 包,这些包我们并不会修改它,但仍然每次都要在 build 的过程消耗构建性能,我们可以通过 DllPlugin 来前置这些包的构建
实现 bind 方法
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
实现高度是宽度的一半
padding 百分比是 content width
部署 http2
一般部署 http2 都需要基于 ssl,所以一般我们在配置的时候都需要验证 CA 证书。
server {
server_name YOUR_DOMAINNAME_HERE;
listen 443 ssl http2;# http2 is available only since OpenSSL version 1.0.2
listen 80;
if ($scheme = http) {
rewrite ^(.*)$ https://$server_name$1 permanent;
}
ssl_certificate server.crt;
ssl_certificate_key server.key;
keepalive_timeout 70;
}
自助机架构
整体技术机构
整体 ZZJ 系统是一套常规的 B/S 系统,但是由于 YY 环境是内网环境不接入公网,所以我们与 YY 通信是用专线通信,为了安全考虑我们公司服务器和 YY 服务器中间设有一个中心机房,中心机房主要是做防火墙和一套双向 IP 映射主要为了让 YY 隐藏自己的内网 IP,这样我们和 YY 存放的自助机就能通信了。
整体前端是由客户端和业务前端组成,客户端负责提供前端展示的宿主环境 chromium 内核,硬件交互比如读取硬件卡、YB 分解等。
前端基于 Vue 开发的单页面应用,页面量不大,单页面主要是因为自助机的前端宿主环境性能较差,使用多页面的话会有短暂的白屏,恰好客户端此时在做一些同步操作页面就会卡住发生白屏,体验会更加不好。
技术难点
前端和客户端之间是有一套通信机制,按照常规的桥的方式去设计的,客户端在 window 上提供一个对象,客户端所有提供的接口都存放在这个对象上,前端调用客户端大部分为异步操作,都是采用回调的方式,同步操作会占用整个 UI 线程,页面会出现卡顿。
自助机可能为 24 小时开机,并且前端又是单页面的展现形式,所以更新是一个问题,更新的要求可能是某一些 YY 的某一些自助机需要更新。所以这里前端自己写了一套简单的更新机制,前端业务和后端之间建立一个简单的 ws,当然会有常规的心跳,确认送达的逻辑。
前端代码发生更新会往 node 的服务器上请求一个接口,node 接到请求之后会根据约定格式解析,根据哪些 YY 和哪些自助机,对自助机进行 push,前端接到 push 之后判断当前自助机有没有人使用,没有使用就会刷新页面。
业务架构
整体业务架构由这么几个主要的系统构成:基础信息,用户中心,报价,订单中心,支付中心,清算中心等。
基础信息:存放 YY 的基础信息系统,主要是一些静态数据不太经常变动。
用户中心:存放用户信息的系统
报价:由于我们没有库存系统,库存是存放在各个 YY 的,我们的报价也不会常变,但是由于 YY 的承受并发量的能力极弱,所以我们需要有一个单纯的报价系统,主要是同步 YY 的报价信息和库存信息,
订单中心:由于库存不在我们这儿,我们没有真是的库存,所以我们下单之后会往医院发起一次锁号,锁定号源期限为 15 分钟,超时我们向 YY 发起号源释放,支付完成我们向医院进行结果确认,确认完成之后才算挂上号,失败发起退款。
支付中心:主要支付方式有银联、微信、卡支付。我们比常规的支付方式多了一种硬件卡支付,这是一个第三方的卡管系统,只负责写入金额和扣减金额,最后统一结算。银联也是前端和客户端之间进行交互。
清算中心:我们涉及到与卡管中心、YY、YGJ 进行分账和结算,这个对账不会涉及到供应商结算就稍微简单一些,我们还有一套为我们盈利业务结算系统。这套结算要复杂一些涉及到供应商。结算基本上是属于剥离结算,当订单支付以后对应会有多条流水,比如支付流水、附加服务流水、供应商流水等
业务难点
业务主要的难点在于订单支付,由于是单页面应用,所以期望达到最大程度的复用,所有自助机的业务生成订单和支付都在同一个页面。常规的支付中心只用完成收钱的功能。自助机的支付中心还有一套 YB 业务,并且这部分业务还需要根据订单状态进行。所以支付中心还揉入了订单的逻辑。
首先会根据订单状态进行轮询,此时会有一个拆单支付的业务,这块业务是需要调用客户端,客户端调用 YB 前置服务器完成,调用完成之后由前端通知客户端扣减多少,做一个类似动态扣减的过程,然后后端到支付中心生成支付表单…
由于可能从中途任何一个状态断掉,所以整个支付是由后端订单状态驱动,订单不同的状态策略映射前端一个不同的 action,不同的 action 会去执行不同的业务返回,轮询订单的状态并不会停,前端不保存状态机。
微信端
以 Vue2 + vuex + vue-router 单页面系统。涉及一百多个页面。自行设计了页面间的动画,达到 APP 的交互效果。
路由设计自己保存堆栈,判断新开、浏览器左上角返回、手势返回等。保证正常的动画效果。
我们每个页面启动前都要获取用户 openid,这个需要进行重定向,单页面获取 openid 会丢失 hash,所以在重定向时会把 hash 当做 query 词放在 URL 中,在主 hash 映射的业务中再次进行 replace 跳转到对应的业务。
API 层基于 axios 自己封装了一层请求挂在 Vue 的原型链上,定义一层 API 层,主要是做请求定义,字段过滤,字段类型校验等