Skip to content

Javascript Guide

数据类型

引用类型

  • Object

值类型

  • Null
  • Undefined
  • Number
  • Boolean
  • Symbol
  • BigInt
  • String

内置对象

  • Date
  • Array
  • JSON
  • ...

深拷贝和浅拷贝

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。

1. JSON 拷贝

深拷贝。 缺点:

  • 会忽略 undefined
  • 会忽略 symbol
  • 如果对象的属性为 Function,因为 JSON 格式字符串不支持 Function,在序列化的时候会自动删除
  • 诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失
  • 不支持循环引用对象的拷贝。

2. Object.assign

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。

缺点:

  • 对象嵌套层次超过 2 层,就会出现浅拷贝的状况
  • 非可枚举的属性无法被拷贝。

3. MessageChannel

js
// 有undefined + 循环引用
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
  f: undefined,
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

function deepCopy(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = (ev) => resolve(ev.data);
    port1.postMessage(obj);
  });
}

deepCopy(obj).then((copy) => {
  // 请记住`MessageChannel`是异步的这个前提!
  let copyObj = copy;
  console.log(copyObj, obj);
  console.log(copyObj == obj);
});

缺点:

  • 这个方法是异步的
  • 拷贝有函数的对象时,还是会报错

4. 递归版深拷贝

js
function deepCopy(obj) {
  // 创建一个新对象
  let result = {};
  let keys = Object.keys(obj),
    key = null,
    temp = null;

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    temp = obj[key];
    // 如果字段的值也是一个对象则递归操作
    if (temp && typeof temp === "object") {
      result[key] = deepCopy(temp);
    } else {
      // 否则直接赋值给新对象
      result[key] = temp;
    }
  }
  return result;
}

循环引用

js
var obj = {
  name: "coffe1891",
  sex: "male",
};
obj["deefRef"] = obj;

此时如果调用上面的 deepCopy 函数的话,会陷入一个死循环,从而导致堆栈溢出。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:

function deepCopy(obj, parent = null) {
    // 创建一个新对象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp = null,
        _parent = parent;
    // 该字段有父级则需要追溯该字段的父级
    while (_parent) {
        // 如果该字段引用了它的父级则为循环引用
        if (_parent.originalParent === obj) {
            // 循环引用直接返回同级的新对象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp = obj[key];
        // 如果字段的值也是一个对象
        if (temp && typeof temp === 'object') {
            // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });

        } else {
            result[key] = temp;
        }
    }
    return result;
}

非循环引用的子对象之拷贝

上面已经解决了深拷贝循环引用的问题,但是还不是特别的完善。遇到下边这种情况就会出问题。

非父级的引用也是一种引用,那么只要记录下对象 A 中的所有对象,并与新创建的对象一一对应即可。

js
function deepCopy(obj) {
  // Hash表 记录所有的对象引用关系
  let map = new WeakMap();
  function dp(obj) {
    let result = null;
    let keys = null,
      key = null,
      temp = null,
      existObj = null;

    existObj = map.get(obj);
    // 如果这个对象已被记录则直接返回
    if (existObj) {
      return existObj;
    }
    keys = Object.keys(obj);
    result = {};
    // 记录当前对象
    map.set(obj, result);
    for (let i = 0; i < keys.length; i++) {
      key = keys[i];
      temp = obj[key];
      // 如果字段的值也是一个对象则递归复制
      if (temp && typeof temp === "object") {
        result[key] = dp(temp);
      } else {
        // 否则直接赋值给新对象
        result[key] = temp;
      }
    }
    return result;
  }
  return dp(obj);
}

5. lodash cloneDeep

lodash 是用一个栈记录了所有被拷贝的引用值,如果再次碰到同样的引用值的时候,不会再去拷贝一遍,而是利用之前已经拷贝好的。

  • 支持循环对象、大量的内置类型

判等

函数常见四种形态

js
//函数的声明形态
function func() {
  console.log("函数的声明形态");
}

//函数的表达式形态 之一
let func0 = (function() {
  console.log("函数的表达式形态");
})(
  //函数的表达式形态 之二
  function func1() {}
);

//函数的嵌套形态
let func2 = function() {
  console.log("函数的嵌套形态");
  let func3 = function() {
    console.log("func2嵌套在func1里");
  };
  func3();
};

// 函数的闭包形态
let func4 = function() {
  var a = "func4";
  return function() {
    console.log("我是以闭包形态存在的函数:" + a);
  };
};
//所有的函数都通过一对括号“()”调用
func();
func0();
func1();
func2();
func4()();

new.target vs. instanceof

Using this instanceof Foo you will check if this instance is a Foo, but you can't ensure that was called with a new. I can just do a thing like this

js
function Foo() {
  console.log(this instanceof Foo, 1);
  console.log(new.target, 2);
}
var foo = new Foo("Test");
var notAFoo = Foo.call(foo, "AnotherWord");

TIP

And will work fine. Using new.target you can avoid this issues. I suggest you to read this book https://leanpub.com/understandinges6/read

声明提前

函数声明提前

js
foo(); //1
function foo() {
  console.log(1);
}
bar(); //bar is not defined
let bar = function() {
  console.log(2);
};

console.log(voo); //undefined
voo(); //Uncaught TypeError: voo is not a function
var voo = function() {
  console.log(3);
};

变量声明提前

js
console.log(foo); // foo
var foo = "foo";

console.log(bar); // VM300:1 Uncaught ReferenceError: bar is not defined
let bar = "bar";

(function() {
  for (var i = 0; i < 100; i++) {
    //……很多行代码
  }
  function func() {
    //……很多行代码
  }
  //……很多行代码
  console.log(i); //>> 100
})();

(function() {
  for (let i = 0; i < 100; i++) {
    //……很多行代码
  }
  function func() {
    //……很多行代码
  }
  //……很多行代码
  console.log(i); //>> ReferenceError
})();

IIFE

(function(){
    console.log("我是立即运行的匿名函数");
})();

(function(){
    console.log("我也是立即运行的匿名函数");
}());

箭头函数

注意

箭头函数并且没有自己的 thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数(new 出来)。

js
() => {};

((a) => {
  console.log(a); //>> 1

  console.log(arguments.length); //>> Uncaught ReferenceError: arguments is not defined
})(1);

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

作用域

作用域即函数或变量的可见区域。 通俗点说,函数或者变量不在这个区域内,就无法访问到。

  • 块级作用域和函数作用域也可以统称为局部作用域。

    作用域:全局作用域函数作用域IIFEES6 块级作用域

JS 引擎会先从离自己最近的作用域 A 查找变量 a,找到就不再继续查找,找不到就去上层作用域

var a = "coffe";//在全局作用域
function func(){
    var b="coffe";//在函数作用域内
    console.log(a);
}

console.log(a);//>> coffe
console.log(b);//>> Uncaught ReferenceError: b is not defined
func();//>> coffe

块级作用域

ES6 规定,在块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。 向后(backward)兼容的考虑,在块级作用域中声明的函数依然可以在作用域外部引用。如果需要函数只在块级作用域中起作用,应该用 let 关键字写成函数表达式,而不是函数声明语句。 为了证明该段论述,我们来看一段代码。

js
{
  function func() {
    //函数声明
    return 1;
  }
}
console.log(func()); //>> 1

{
  var func = function() {
    //未使用let关键字的函数表达式
    return 1;
  };
}
console.log(func()); //>> 1

{
  let func = function() {
    return 1;
  };
}
console.log(func()); //>> func is not defined

执行上下文

类数组

先说数组,这我们都熟悉。它的特征有:可以通过索引(index)调用,如 array[0];具有长度属性 length;可以通过 for 循环或 forEach 方法,进行遍历。 那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象 。比如,下面的这个对象,就是一个类数组。

js
let arrayLike = {
  0: 1,
  1: 2,
  2: 3,
  length: 3,
};

实现 .call

js
/* 
1. 参考call的语法规则,需要设置一个参数thisArg,也就是this的指向;
2. 将thisArg封装为一个Object;
3. 通过为thisArg创建一个临时方法,这样thisArg就是调用该临时方法的对象了,会将该临时方法的this隐式指向到thisArg上(参考上一篇文章4. 《壹.2.3 彻底搞懂this》);
5. 执行thisArg的临时方法,并传递参数;
6. 删除临时方法,返回方法的执行结果。
*/

Function.prototype.mcall = function(thisArg, ...rest) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }
  let key = Symbol();
  thisArg[key] = this;
  let result = thisArg[key](...rest);
  delete thisArg[key];
  return result;
};
console.log(Object.prototype.toString.mcall(1));

let a = [];
Array.prototype.push.mcall(a, 1, 2, 3, 4);
console.log(a);

实现 .apply

js
Function.prototype.mapply = function(thisArg, arr) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }
  let key = Symbol();
  thisArg[key] = this;

  function isArrayLike(args) {
    if (
      args &&
      typeof args === "object" &&
      isFinite(args.length) &&
      args.length >= 0 &&
      args.length === parseInt(args.length) &&
      args.length < 4294967295
    )
      return true;
    else return false;
  }

  let result;
  if (arr) {
    if (!Array.isArray(arr) || !isArrayLike(arr)) {
      throw new TypeError("参数不为数组或类数组");
    } else {
      arr = Array.from(arr);
      result = thisArg[key](...arr);
    }
  } else {
    result = thisArg[key]();
  }
  delete thisArg[key];
  return result;
};

let foo = [1];

Array.prototype.push.mapply(foo, 1);
console.log(foo);

实现 .bind

js
Function.prototype.mbind = function(objThis, ...rest) {
  let fn = this;
  let newFunc = function(...args) {
    let thisArg = new.target ? this : Object(objThis); // 这里用new.target判断比用 this instanceof newFunc好 ,查看文档 new.target vs instanceof
    return fn.call(objThis, ...rest, ...args);
  };
  return newFunc;
};

let a = [1];
let b = Array.prototype.push.mbind(a, 2);
b(3);
new b(4);
console.log(a);

为什么使用 Object.prototype.toString 判断类型,而不使用 typeof

js
typeof null === "object";
typeof [] === "object";

函数都有 prototype 属性,为什么 Array.prototype.push.protorype 为 undefined

  • 创建出来的函数都不能删除prototype,因为函数构造的时候这个属性writablefalse,但是可以赋值为undefinedArray上的prototypepush方法就没有prototype属性
js
Array.prototype.mpush = function() {
  return Array.prototype.push.apply(this, arguments);
};
delete Array.prototype.mpush.prototype; //false
Array.prototype.mpush.prototype = undefined;

let a = [1];
Array.prototype.mpush.call(a, 2);
console.log(a);

console.log(Array.prototype.mpush.hasOwnProperty("prototype")); //true
console.log(Array.prototype.push.hasOwnProperty("prototype")); //false

原因

Array 是通过 class 类构造器构造的, 通过类构造器在原型添加的方法,没有 prototype 属性 但是如果直接在prototype上添加的方法拥有prototype属性

js
class A {
  test() {
    console.log("foo");
  }
}
A.prototype.newFunc = function() {};

Object.defineProperty(A.prototype, "myFunc", {
  value: function() {
    console.log("bar");
  },
  enumerable: false,
  writable: false,
  configurable: true,
});

console.log(A.prototype.test.hasOwnProperty("prototype")); //false
console.log(A.prototype.newFunc.hasOwnProperty("prototype")); //true
console.log(A.prototype.myFunc.hasOwnProperty("prototype")); //true

class 和 function 的区别

class是特殊的函数,作为创建对象实例的模板,不可以直接调用,只能通过new来实例化对象。

class 声明的函数,如class Foo{ myfunc(){} },函数myFuncprototype属性。

但是通过Foo.prototype.foo = function() 形式声明的原型函数foo拥有prototype属性

new.target

new.target 属性允许你检测函数或构造方法是否是通过 new 运算符被调用的。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined

原型与原型链

  • 所有对象都有__proto__属性,其实__proto__在浏览器内部有个符号为[[Prototype]]的属性
  • 函数也是对象
  • prototype 只是 函数对象 拥有的一个 普通对象 的属性
  • Function.prototype 是 所有函数 和 内置对象 的原型
  • Function.prototype.__proto__ === Object.prototype
  • Object.prototype.__proto__ === null
  • Object.__proto__ === Function.prototype

Event Loop 事件循环

link

实现栈 Stack

LIFO后入先出

堆的属性定义

类型描述
top [param]查看当前栈顶位置
pop [func]删除栈顶元素
push [func]将新元素入栈
peek [func]查看当前栈顶元素
length [func]查看栈元素总数
clear [func]清空栈内元素
isempty [func]是否为空
js
class Stack {
  constructor() {
    this.items = [];
  }
  push(...args) {
    this.items.push(...args);
  }
  pop() {
    return this.items.pop();
  }
  top() {
    return this.items.pop();
  }
  peek() {
    return this.items[this.items.length - 1];
  }
  isempty() {
    return this.items.length == 0;
  }
  length() {
    return this.items.length;
  }
  clear() {
    this.items = [];
  }
}

let stack = new Stack();

stack.push(1, 2, 3, 4);
console.log(stack.peek());
console.log(stack.pop());
console.log(stack.peek());

实现队列 Queue

FIFO先进先出,一端只能增加,另一端只能减少

类型描述
enqueue [func]向队列末尾添加一个元素
dequeue [func]删除队列首的元素
front [func]读取队列首的元素
back [func]读取队列尾的元素
toString [func]显示队列所有元素
clear [func]清空当前队列
empty [func]判断队列是否为空
size [func]获取队列长度
js
class Queue {
  constructor(desc) {
    this.desc = desc;
    this.items = [];
  }
  enqueue(...args) {
    this.items.push(...args);
  }
  dequeue() {
    return this.items.shift();
  }
  front() {
    return this.items[0];
  }
  back() {
    return this.items[this.items.length - 1];
  }
  empty() {
    return this.items.length == 0;
  }
  size() {
    return this.items.length;
  }
  clear() {
    this.items = [];
  }
  toString() {
    return this.items.toString();
  }
}

let foo = new Queue("bar");
foo.enqueue(1, 2, undefined, null);
console.log(foo.dequeue());
console.log(foo.dequeue());
console.log(foo.dequeue());
console.log(foo.dequeue());
console.log(foo.dequeue());

单链表

js

循环链表

js

双向链表

js

排序

稳定性: 保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。

  • 基于比较的排序算法:

    1. BUB - 冒泡排序 稳定
    2. SEL - 选择排序 不稳定
    3. INS - 插入排序
    4. MER - 归并排序
    5. QUI - 快速排序
    6. R-Q - 随机快速排序
    7. XIR - 希尔排序
  • 不基于比较的排序算法:

    1. COU - 计数排序
    2. RAD - 基数排序

冒泡排序

js
let arr = [3, 20, 6, 1, 2, 10, 5];

for (let i = 0; i < arr.length - 1; i++) {
  for (let k = 0; k < arr.length - 1 - i; k++) {
    let kItem = arr[k];
    let kItemNext = arr[k + 1];
    let tmp;
    if (kItemNext > kItem) {
      tmp = kItemNext;
      arr[k + 1] = arr[k];
      arr[k] = tmp;
    }
  }
}
console.log(arr);

选择排序

js
let arr = [3, 20, 6, 1, 2, 10, 5];
for (let i = 0; i < arr.length - 1; i++) {
  let minIndex = i;
  for (let k = i + 1; k < arr.length; k++) {
    if (arr[k] < arr[minIndex]) {
      minIndex = k;
    }
  }
  let temp = arr[i];
  arr[i] = arr[minIndex];
  arr[minIndex] = temp;
}
console.log(arr);

插入排序

打扑克原理

js
function insertionSort(arr) {
  var len = arr.length;
  var preIndex, current;
  for (var i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = current;
  }
  return arr;
}

希尔排序

js
function shellSort(arr) {
  let gap = Math.floor(arr.length / 2);
  while (gap > 0) {
    console.log("=================", gap, arr.toString());
    for (let i = 0; i < arr.length; i++) {
      let current = arr[i];
      let j = i - gap;
      console.log("!!!", j, i, arr.toString());
      for (j; j >= 0 && current < arr[j]; j -= gap) {
        arr[j + gap] = arr[j];
        console.log("gap", gap);
      }
      arr[j + gap] = current;
    }
    gap = Math.floor(gap / 2);
  }
  return arr;
}

归并排序


/* 递归归并排序 */
function merge(left, right) {
  var result = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
  while (left.length) result.push(left.shift());
  while (right.length) result.push(right.shift());
  return result;
}
function mergeSort(arr) {
  var len = arr.length;
  if (len < 2) {
    return arr;
  }
  var middle = Math.floor(len / 2),
    left = arr.slice(0, middle),
    right = arr.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}