Coder Social home page Coder Social logo

javascript-basic-learn's Introduction

javascript-basic-learn's People

Contributors

yrmatou avatar

Stargazers

 avatar

Watchers

 avatar

javascript-basic-learn's Issues

词法环境

参考文章

概念

词法环境是一种规范类型(specification type),它基于 ECMAScript 代码的词法嵌套结构,
来定义标识符与特定变量和函数的关联关系。
词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

V8 中 JS 的编译过程来更加直观的解释

  • 第一步 词法分析 :V8 刚拿到执行上下文的时候,会把代码从上到下一行一行的进行分词/词法分析(Tokenizing/Lexing),
    例如 var a = 1; ,会被分成 var 、 a 、 1 、 ; 这样的原子符号((atomic token)。
    词法分析=指登记变量声明+函数声明+函数声明的形参。

  • 第二步 语法分析 :在词法分析结束后,会做语法分析,引擎将 token 解析成一个抽象语法树(AST),
    在这一步会检测是否有语法错误,如果有则直接报错不再往下执行

var a = 1;
console.log(a);
a = ;
// Uncaught SyntaxError: Unexpected token ;
// 代码并没有打印出来 1 ,而是直接报错,说明在代码执行前进行了词法分析、语法分析
  • 注意: 词法分析跟语法分析不是完全独立的,而是交错运行的。
    也就是说,并不是等所有的 token 都生成之后,才用语法分析器来处理。一般都是每取得一个 token ,
    就开始用语法分析器来处理了

  • 第三步 代码生成 :最后一步就是将 AST 转成计算机可以识别的机器指令码
    在第一步中,我们看到有词法分析,它用来登记变量声明、函数声明以及函数声明的形参,
    后续代码执行的时候就可以知道要从哪里去获取变量值与函数。这个登记的地方就是词法环境。

静态作用域 vs 动态作用域

  • 词法环境与我们自己写的代码结构相对应,也就是我们自己代码写成什么样子,词法环境就是什么样子。
    词法环境是在代码定义的时候决定的,跟代码在哪里调用没有关系。
    所以说 JS 采用的是词法作用域(静态作用域),即它在代码写好之后就被静态决定了它的作用域。

  • 动态作用域是基于栈结构,局部变量与函数参数都存储在栈中,
    所以,变量的值是由代码运行时当前栈的栈顶执行上下文决定的。
    而静态作用域是指变量创建时就决定了它的值,源代码的位置决定了变量的值。

  var x = 1;
  
  function foo() {
    var y = x + 1;
    return y;
  }
  
  function bar() {
    var x = 2;
    return foo();
  }
  
  foo(); // 静态作用域: 2; 动态作用域: 2
  bar(); // 静态作用域: 2; 动态作用域: 3

  在此例中,静态作用域与动态作用域的执行结构可能是不一致的,bar 本质上就是执行 foo 函数,
  如果是静态作用域的话, bar 函数中的变量 x 是在 foo 函数创建的时候就确定了,也就是说变量 x 一直为 1 
  两次输出应该都是 2 。而动态作用域则根据运行时的 x 值而返回不同的结果。

  所以说,动态作用域经常会带来不确定性,它不能确定变量的值到底是来自哪个作用域的。

  大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript等,
  采用动态作用域的语言有 
  Emacs Lisp、Common Lisp(兼有静态作用域)、Perl(兼有静态作用域)。
  C/C++的宏中用到的名字,也是动态作用域。

词法环境与闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),
这样的组合就是闭包(closure)也就是说,闭包是由 函数 以及声明该函数的 词法环境 组合而成的

  var x = 1;
  
  function foo() {
    var y = 2; // 自由变量
    function bar() {
      var z = 3; //自由变量
      return x + y + z;
    }
    return bar;
  }
  
  var test = foo();
  
  test(); // 6

前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。
虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。
所谓闭包函数,即这个函数封闭了它自己的定义时的环境,形成了一个闭包,
所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。

为了实现闭包,我们不能用动态作用域的动态堆栈来存储变量
如果是这样,当函数返回时,变量就必须出栈,而不再存在,这 与最初闭包的定义是矛盾的。
事实上,外部环境的闭包数据被存在了“堆”中,
这样才使得即使函数返回之后内部的变量仍然一直存在(即使它的执行上下文也已经出栈)。

async-await

参考文献

概念

async 函数是什么?一句话,它就是 Generator 函数的语法糖

  const fs = require('fs');
  
  const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
      fs.readFile(fileName, function(error, data) {
        if (error) return reject(error);
        resolve(data);
      });
    });
  };
  
  const gen = function* () {
    const f1 = yield readFile('/etc/fstab');
    const f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
  };
  上面代码的函数gen可以写成async函数,就是下面这样。
  
  const asyncReadFile = async function () {
    const f1 = await readFile('/etc/fstab');
    const f2 = await readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
  };

  async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
  当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

优点

  • 内置执行器
  • 更好的语义
  • 更广的适用性
  • 返回值是 Promise

错误处理

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

  await 命令
  正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
  
  async function f() {
    // 等同于
    // return 123;
    return await 123;
  }
  
  f().then(v => console.log(v))
  // 123

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

解决方法

    1.可以将await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行
    async function f() {
      try {
        await Promise.reject('出错了');
      } catch(e) {
      }
      return await Promise.resolve('hello world');
    }
    
    f()
    .then(v => console.log(v))
    // hello world
  
    2.await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

    async function f() {
      await Promise.reject('出错了')
        .catch(e => console.log(e));
      return await Promise.resolve('hello world');
    }
    
    f()
    .then(v => console.log(v))
    // 出错了
    // hello world

    3.多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    let foo = await getFoo();
    let bar = await getBar();
    上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。
    这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。
    
    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    
    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
    上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。
    
    第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。
    
    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      // 报错
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }
    上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题
    
    4.实际开发中,经常遇到一组异步操作,需要按照顺序完成。
       比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

      Promise 的写法如下。
      
      function logInOrder(urls) {
        // 远程读取所有URL
        const textPromises = urls.map(url => {
          return fetch(url).then(response => response.text());
        });
      
        // 按次序输出
        textPromises.reduce((chain, textPromise) => {
          return chain.then(() => textPromise)
            .then(text => console.log(text));
        }, Promise.resolve());
      }
      上面代码使用fetch方法,同时远程读取一组 URL。
      每个fetch操作都返回一个 Promise 对象,放入textPromises数组。
      然后,reduce方法依次处理每个 Promise 对象,然后使用then,
      将所有 Promise 对象连起来,因此就可以依次输出结果。
      
      这种写法不太直观,可读性比较差。下面是 async 函数实现。
      
      async function logInOrder(urls) {
        for (const url of urls) {
          const response = await fetch(url);
          console.log(await response.text());
        }
      }
      上面代码确实大大简化,问题是所有远程操作都是继发。
      只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。
      我们需要的是并发发出远程请求。
      
      async function logInOrder(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);
        }
      }
      上面代码中,虽然map方法的参数是async函数,但它是并发执行的,
      因为只有async函数内部是继发执行,外部不受影响。
      后面的for..of循环内部使用了await,因此实现了按顺序输出。

基础类型和引用类型

参考文章

基本类型

Undefined、Null、Boolean、Number、String、Symbol (new in ES 6)
基本类型的访问是按值访问的,
就是说你可以操作保存在变量中的实际的值

  • 基本类型的值是不可变的
  ar str = "123hello321";
  str.toUpperCase();     // 123HELLO321
  console.log(str);      // 123hello321
  • 不能给基本类型添加属性和方法,再次说明基本类型时不可变的
  var person = 'jozo';
  person.age = 22;
  person.method = function(){//...};
  
  console.log(person.age); // undefined
  console.log(person.method); // undefined
  • 基本类型的比较是值的比较
  只有在它们的值相等的时候它们才相等
  var a = 1;
  var b = true;
  console.log(a == b);//true
  console.log(a === b);//false
  其实这是类型转换和 == 运算符的知识了,
  也就是说在用==比较两个不同类型的变量时会进行一些类型转换

  当两个值的类型相同的时候,即使是==也相当于是===

  var a = 'jozo';
  var b = 'jozo';
  console.log(a === b);//true
  • 基本类型的变量是存放在栈区的(栈区指内存里的栈内存)
var name = 'jozo';
var city = 'guangzhou';
var age = 22;

image
栈区包括了 变量的标识符和变量的值

引用类型

Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等
引用类型的值是按引用访问的

  • 引用类型的值是可变的
  var obj = {name:"zyj"};   // 创建一个对象
  obj.name = "percy";       // 改变 name 属性的值
  obj.age = 21;             // 添加 age 属性
  obj.giveMeAll = function(){
    return this.name + " : " + this.age;
  };                        // 添加 giveMeAll 方法
  obj.giveMeAll();
  • 引用类型的比较是引用的比较
  var obj1 = {};    // 新建一个空对象 obj1
  var obj2 = {};    // 新建一个空对象 obj2
  console.log(obj1 == obj2);    // false
  console.log(obj1 === obj2);   // false
  因为 obj1  obj2 分别引用的是存放在堆内存中的2个不同的对象,
  故变量 obj1  obj2 的值(引用地址)也是不一样的
  • 引用类型的值是保存在堆内存(Heap)中的对象(Object)
    与其他编程语言不同,JavaScript 不能直接操作对象的内存空间(堆内存)
  var a = {name:"percy"};
  var b;
  b = a;
  a.name = "zyj";
  console.log(b.name);    // zyj
  b.age = 22;
  console.log(a.age);     // 22
  var c = {
    name: "zyj",
    age: 22
  };

栈内存中保存了变量标识符和指向堆内存中该对象的指针
堆内存中保存了对象的内容

image

检测类型

  var a;
  typeof a;    // undefined
  
  a = null;
  typeof a;    // object
  
  a = true;
  typeof a;    // boolean
  
  a = 666;
  typeof a;    // number 
  
  a = "hello";
  typeof a;    // string
  
  a = Symbol();
  typeof a;    // symbol
  
  a = function(){}
  typeof a;    // function
  
  a = [];
  typeof a;    // object
  a = {};
  typeof a;    // object
  a = /aaa/g;
  typeof a;    // object   

  typeof Array // function
  • 简单说就是判断一个引用类型的变量具体是不是某种类型的对象
  ({}) instanceof Object              // true
  ([]) instanceof Array               // true
  (/aa/g) instanceof RegExp           // true
  (function(){}) instanceof Function  // true
  • Object.prototype.toString.call()
  Object.prototype.toString.call(null); // "[object Null]"
  Object.prototype.toString.call(undefined); // "[object Undefined]"
  Object.prototype.toString.call(“abc”);// "[object String]"
  Object.prototype.toString.call(123);// "[object Number]"
  Object.prototype.toString.call(true);// "[object Boolean]"
  Object.prototype.toString.call(fn); // "[object Function]"
  Object.prototype.toString.call(date); // "[object Date]"
  Object.prototype.toString.call(arr); // "[object Array]"
  Object.prototype.toString.call(reg); // "[object RegExp]"
  Object.prototype.toString.call(person); // "[object Object]"

实例:为Array对象添加一个去除重复项的方法

  [false, true, undefined, null, NaN, 0, 1, {}, {}, 'a', 'a', NaN]

  Array.prototype.uniq = function () {
      var res = [];
      var flag = true;
      this.forEach(function(x) {
          if (res.indexOf(x) == -1) {
              if (x != x) {
                  if (flag) {
                      res.push(x);
                      flag = false;
                  }
              } else {
                  res.push(x);
              }
          }
      })
      return res;
  }

扩展

判断实例的原型对象方法

var,let,const区别

阮老师文章

1.var 存在变量的提升,会绑定到window上

变量提升

 很多人认为,var是存在变量提升了,因此,如果你有如下代码:

 console.log(foo);      // undefined
 var foo = 123;
 程序不会报错,而是打印出undefined。
 这是因为,编译器预编译的时候,第一步只会记录变量和函数的定义,
 第二步才会执行程序(hoisting是对一种现象的描述,而不是一种编译器具体的行为),
 所以程序看起来等价于下面的代码:

  var foo;
  console.log(foo);      // undefined
  foo = 123;
  再来看let是否存在变量提升:

  作为对照,我们直接运行下面的代码,看看会出现什么错误:
  
  console.log(foo);      //  Uncaught ReferenceError: foo is not defined
  错误原因:foo没有定义。
  
  为什么let和const定义的变量不会被初始化呢?主要是因为const。
  const,顾名思义:常量,const的引用不应被改变。
  如果编译器把const初始化为undefined,之后,又让它等于我们定义的那个值,就改变了const的引用。
  因此,委员会决定let和const虽然也会发生变量提升,但是没有任何初始值。

2.var、let和const另外一个重要区别就是let和const只在块级作用域中有效

  {
      var a = 'Smiley';
  }
  console.log(a)  	// 能正确打印出Smiley
  但是:
  
  {
      let a = 'Smiley';
  }
  console.log(a)    	//  报错: Uncaught ReferenceError: a is not defined

3.关于var和let还有一些微小的区别,比如:

暂时性死区
let和const不允许重复声明
let和const不会绑定全局作用域

暂时性死区:在代码块内,使用let命令声明变量之前,该变量都是不可用的。
这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

  只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
  
  var tmp = 123;
  
  if (true) {
    tmp = 'abc'; // ReferenceError
    let tmp;
  }
  上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,
   导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错

  ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,
  防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,
  现在有了这种规定,避免此类错误就很容易了。
  
  总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,
  但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

为什么需要块级作用域

  ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
  
  第一种场景,内层变量可能会覆盖外层变量。
  
  var tmp = new Date();
  
  function f() {
    console.log(tmp);
    if (false) {
      var tmp = 'hello world';
    }
  }
  
  f(); // undefined
  上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。
   但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
  
  第二种场景,用来计数的循环变量泄露为全局变量。
  
  var s = 'hello';
  
  for (var i = 0; i < s.length; i++) {
    console.log(s[i]);
  }
  
  console.log(i); // 5
  上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

  另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,
  JavaScript 引擎就认为不存在块级作用域。

  // 第一种写法,报错
  if (true) let x = 1;
  
  // 第二种写法,不报错
  if (true) {
    let x = 1;
  }

   顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一
    var a = 1;
    // 如果在 Node 的 REPL 环境,可以写成 global.a
    // 或者采用通用方法,写成 this.a
    window.a // 1
    
    let b = 1;
    window.b // undefined

   全局环境中,this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,
   ES6 模块中this返回的是undefined。
   函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。
   但是,严格模式下,这时this会返回undefined。
   不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。
   但是,如果浏览器用了 CSP(Content 
   Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。

立即执行函数 IIFE

参考文章 参考文章2

IIFE(立即调用函数表达式)是一个在定义时就会立​​即执行的 JavaScript 函数

(function () {
    statements
})();

这是一个被称为自执行匿名函数的设计模式,主要包含两部分。
第一部分是包围在 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。
这避免了外部访问此IIFE中的变量,而且又不会污染类别作用域。圆括号运算符 ()

示例

  • 当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
(function () {
    var name = "Barry";
})();
// 无法从外部访问变量 name
name // 抛出错误:"Uncaught ReferenceError: name is not defined"
  • 将IIFE分配给一个变量,不是存储IIFE本身,而是存储IIFE执行后返回的结果。
var result = (function () {
    var name = "Barry";
    return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"

执行栈和任务队列

参考文章

执行栈和任务队列

  • 栈 (stack): 栈是遵循后进先出 (LIFO) 原则的有序集合,新添加或待删除的元素都保存在同一端,
    称为栈顶,另一端叫做栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
    栈在编程语言的编译器和内存中存储基本数据类型和对象的指针、方法调用等.
  • 队列 (queue): 队列是遵循先进先出 (FIFO) 原则的有序集合,队列在尾部添加新元素,
    并在顶部移除元素,最新添加的元素必须排在队列的末尾。
    在计算机科学中,最常见的例子就是打印队列。
  • 堆 (heap): 堆是基于树抽象数据类型的一种特殊的数据结构。
    image

JavaScript 中引用类型值的大小是不固定的,因此它们会被存储到 堆内存 中,由系统自动分配存储空间
JavaScript 不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,而是操作 对象的引用
而 JavaScript 中的基础数据类型都有固定的大小,因此它们被存储到 栈内存 中。我们可以直接操作保存在栈内存空间的值,
因此基础数据类型都是 按值访问。此外,栈内存还会存储 对象的引用 (指针) 以及 函数执行时的运行空间。

栈内存 堆内存
存储基础数据类型 存储引用数据类型
按值访问 按引用访问
存储的值大小固定 存储的值大小不定,可动态调整
由系统自动分配内存空间 由程序员通过代码进行分配
主要用来执行程序 主要用来存放对象
空间小,运行效率高 空间大,但是运行效率相对较低
先进后出,后进先出 无序存储,可根据引用直接获取

执行栈

当我们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。
这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数,
以及这个作用域中定义的变量和 this 的指向,而当一系列方法被依次调用的时候。
由于 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈。

任务队列

事件队列是一个存储着 异步任务 的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,
而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。
执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果事件队列不为空的话,
事件队列便将第一个任务压入执行栈中运行。

求出给定N以内的素数(质数)埃拉托斯特尼筛法

素数:(又叫质数)大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
image

埃拉托斯特尼筛法 简称埃氏筛或爱氏筛 是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。 要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。 给出要筛数值的范围n,找出以内的素数。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个质数,也就是3筛,把3留下,把3的倍数剔除掉;接下去用下一个质数5筛,以此类推。

350px-Sieve_of_Eratosthenes_animation

简单的实现 自己理解的实现

const createNum = (n) => {
    let arr = new Array();
    for (let i = 2; i < n; i++) {
        arr.push(i);
    }
    return arr;
}

const findAllNums = (n) => {
    let signs = createNum(n);
    for (let i = 2; i <= Math.sqrt(n); i++) {
        for (let j = i; j <= signs.length; j++) {
            // 剔除所有的倍数,剩下的全是质数
            if (signs[j - 1] % i === 0) {
                signs.splice(j - 1, 1);
                --j;
            }
        }
    }
    return signs;
}
console.log(findAllNums(25));

// 验证是否是质数
const isZS = (num) => {
    for (let i = 0; i <= Math.sqrt(num); i++) {
        return num % i !== 0;
    }
}

console.log(isZS(5));

事件循环机制 Event Loop

参考文章

参考文章

概念

  • JavaScript是单线程,非阻塞的
    浏览器的事件循环

  • 执行栈和事件队列
    宏任务和微任务
    node环境下的事件循环和浏览器环境有何不同
    事件循环模型
    宏任务和微任务

单线程:JavaScript的主要用途是与用户互动,以及操作DOM。
如果它是多线程的会有很多复杂的问题要处理,比如有两个线程同时操作DOM,
一个线程删除了当前的DOM节点,一个线程是要操作当前的DOM阶段,
最后以哪个线程的操作为准?为了避免这种,所以JS是单线程的。
即使H5提出了web worker标准,它有很多限制,受主线程控制,是主线程的子线程。

非阻塞:通过 event loop 实现。
执行栈,任务队列相关 #8

Event Loop

在异步代码完成后仍有可能要在一旁等待,因为此时程序可能在做其他的事情,
等到程序空闲下来才有时间去看哪些异步已经完成了。所以 JavaScript 有一套机制去处理同步和异步操作,
那就是事件循环 (Event Loop)

image

  • 所有同步任务都在主线程上执行,形成一个执行栈 (Execution Context Stack)。
  • 而异步任务会被放置到 Task Table,也就是上图中的异步处理模块,当异步任务有了运行结果,就将该函数移入任务队列。
  • 一旦执行栈中的所有同步任务执行完毕,引擎就会读取任务队列,然后将任务队列中的第一个任务压入执行栈中运行。
    主线程不断重复第三步,也就是 只要主线程空了,就会去读取任务队列,该过程不断重复,这就是所谓的 事件循环

宏任务和微任务

微任务、宏任务与 Event-Loop 的简单总结

还是以去银行办业务为例,当 5 号窗口柜员处理完当前客户后,开始叫号来接待下一位客户,
我们将每个客户比作 宏任务,接待下一位客户 的过程也就是让下一个 宏任务 进入到执行栈。
所以该窗口所有的客户都被放入了一个 任务队列 中。任务队列中的都是 已经完成的异步操作的,
而不是注册一个异步任务就会
被放在这个任务队列中(它会被放到 Task Table 中)。就像在银行中排号,如果叫到你的时候你不在,
那么你当前的号牌就作废
了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号。
在执行宏任务时,是可以穿插一些微任务进去。比如你大爷在办完业务之后,顺便问了下柜员:“最近 P2P 暴雷很严重啊,
有没有其他稳妥的投资方式”。柜员暗爽:“又有傻子上钩了”,然后叽里咕噜说了一堆。
我们分析一下这个过程,虽然大爷已经办完正常的业务,但又咨询了一下理财信息,
这时候柜员肯定不能说:“您再上后边取个号去,重新排队”。
所以只要是柜员能够处理的,都会在响应下一个宏任务之前来做,我们可以把这些任务理解成是 微任务。
大爷听罢,扬起 45 度微笑,说:“我就问问。”
柜员 OS:“艹...”
这个例子就说明了:你大爷永远是你大爷 在当前微任务没有执行完成时,是不会执行下一个宏任务的!
总结一下,异步任务分为 宏任务(macrotask) 与 微任务 (microtask)。宏任务会进入一个队列,而微任务会进入到另一个不同的
队列,且微任务要优于宏任务执行。

常见的宏任务和微任务

宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)

例题

let p = new Promise(resolve => {
  resolve(1);
  Promise.resolve().then(() => console.log(2));
  console.log(4);
}).then(t => console.log(t));
console.log(3);

//复制代码
//首先将 Promise.resolve() 的 then() 方法放到微任务队列,此时微任务队列为 ['2']
//然后打印出同步任务 4
//接着将 p 的 then() 方法放到微任务队列,此时微任务队列为 ['2', '1']
//打印出同步任务 3
//最后依次打印微任务 2 和 1

setTimeout(() => {
  console.log('A');
}, 0);
var obj = {
  func: function() {
    setTimeout(function() {
      console.log('B');
    }, 0);
    return new Promise(function(resolve) {
      console.log('C');
      resolve();
    });
  },
};
obj.func().then(function() {
  console.log('D');
});
console.log('E');
// 打印顺序 C E D A B

当 Event Loop 遇到 async/await

// 分析
async function foo() {
  // await 前面的代码
  await bar();
  // await 后面的代码
}

async function bar() {
  // do something...
}

foo();

// 上面代码可解析成
function foo() {
  // await 前面的代码
  Promise.resolve(bar()).then(() => {
    // await 后面的代码
  });
}

function bar() {
  // do something...
}

foo();

async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,
await之后的所有代码都是在Promise.then中的回调

思考题

1.
const p1 = new Promise((resolve, reject) => {
  console.log('promise1');
  resolve();
})
  .then(() => {
    console.log('then11');
    new Promise((resolve, reject) => {
      console.log('promise2');
      resolve();
    })
      .then(() => {
        console.log('then21');
      })
      .then(() => {
        console.log('then23');
      });
  })
  .then(() => {
    console.log('then12');
  });

const p2 = new Promise((resolve, reject) => {
  console.log('promise3');
  resolve();
}).then(() => {
  console.log('then31');
});
// 打印结果 promise1 promise3 then11 promise2 then31 then21 then12 then23
// 分析
1.首先打印出 promise1,then11,new Promise放入此次微任务
2.打印p2中同步任务promise3,then31放入此次微任务,当前微任务列表 [then11, new Promise,then31]
3.执行当前微任务并清空此次,打印 then11, promise2 ,then31
4.没有宏任务  then21,then12 放入此次微任务
5.打印出 then21,then12 
6.没有宏任务 把then23放入微任务
7.打印 then23

2. 这道题实际在考察 Promise 的用法,当在 then() 方法中返回一个 Promise,
    p1 的第二个完成处理函数就会挂在返回的这个 Promise  then() 方法下,因此输出顺序如下。
    关键是 then里面 return new Promise
const p1 = new Promise((resolve, reject) => {
  console.log('promise1'); // 1
  resolve();
})
  .then(() => {
    console.log('then11'); // 2
    return new Promise((resolve, reject) => {
      console.log('promise2'); // 3
      resolve();
    })
      .then(() => {
        console.log('then21'); // 4
      })
      .then(() => {
        console.log('then23'); // 5
      });
  })
  .then(() => {
    console.log('then12'); //6
  });

执行上下文

参考文章-易懂

这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了?

其实很简单,就三种,全局代码、函数代码、eval代码。

举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,
让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

笔记

  • 当函数执行的时候会把当前上下文压到执行栈里,遇到执行函数继续压到栈里,遵循后进先出

二叉树

定义

官方定义:在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)

Editor
Editor

作用域

参考文章

作用域

JS高程》讲到,每一个函数都有自己的执行环境。好像这里没有讲到额

作用域
[[scope]]:每个 javascript 函数都是一个对象,对象中有些属性我们可以访问,但有些
不可以,这些属性仅供 javascript 引擎存取,[[scope]]就是其中一个。[[scope]]指的就
是我们所说的作用域,其中存储了运行期上下文的集合。

作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我
们把这种链式链接叫做作用域链。

运行期上下文:当函数在执行的前一刻,会创建一个称为执行期上下文的内部对象。
一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下
文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执
行完毕,执行上下文被销毁。
查找变量:在哪个函数里面查找变量,就从哪个函数作用域链的顶端依次向下查找。

原型到原型链

主要参考这篇文章

个人理解

  • 原型:包含属性和方法的对象集合
  • 原型链:当一个对象访问一个属性或者方法时,它会先从本身查找,查找不到从原型上查找,
    再查找不到从原型的原型上查找,以此类推直到 Object.prototype.__proto__原型null,这样查找过程形成了一条链。

promise


Promise 参考阮一峰老师ES6

Promise 是异步编程的一种解决方案,
比传统的解决方案——回调函数和事件——更合理和更强大。
它由社区最早提出和实现,
ES6 将其写进了语言标准,统一了用法,
原生提供了Promise对象。
所谓Promise,简单说就是一个容器,
里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

1.对象的状态不受外界影响。Promise对象代表一个异步操作,
有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

2.一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,
只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,
状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。
这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,
无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。

基本用法

  Promise对象是一个构造函数,用来生成Promise实例

  const promise = new Promise(function(resolve, reject) {
    // ... some code
    if (/* 异步操作成功 */){
      resolve(value);
    } else {
     reject(error);
    }
  });

  使用示例

    1.Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
 
    promise.then(function(value) {
    // success
    }, function(error) {
    // failure
    });
  
   2.Promise 新建后就会立即执行。

   let promise = new Promise(function(resolve, reject) {
     console.log('Promise');
     resolve();
   });
   
   promise.then(function() {
     console.log('resolved.');
   });
   
   console.log('Hi!');
   
   // Promise
   // Hi!
   // resolved

  下面是异步加载图片的例子。

   function loadImageAsync(url) {
     return new Promise(function(resolve, reject) {
       const image = new Image();
   
       image.onload = function() {
         resolve(image);
       };
   
       image.onerror = function() {
         reject(new Error('Could not load image at ' + url));
       };
   
       image.src = url;
     });
   }

  3.注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。

   (1) new Promise((resolve, reject) => {
           resolve(1);
           console.log(2);
         }).then(r => {
           console.log(r);
         });
         // 2
         // 1
   (2) new Promise((resolve, reject) => {
         return resolve(1);
         // 后面的语句不会执行
         console.log(2);
       })

  4. 检验是否是Promise对象
  
   var promise = new Promise((resolve, reject) => {})
   检测对象是否有then方法
   if (promise.then) {
      return true
   }

Promise.all()、Promise.race()、Promise.allSettled()、Promise.any()、Promise.try()

   1.Promise.all()方法接受一个数组作为参数,都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 
      Promise 实例再进一步处理
      const p = Promise.all([p1, p2, p3]);
      p的状态由p1、p2、p3决定,分成两种情况。
     (1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的 
         回调函数。
     (2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调 
        函数,不会传递多个reject。如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发 
      Promise.all()的catch方法。
  2. Promise.race()
      只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的 
      回调函数
  3. Promise.allSettled()
       方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 
       fulfilled还是rejected,包装实例才会结束。
  4.Promise.any()
      该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实 
      例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

      Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束
     5.Promise.try()
       实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为 
  这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。
      try {
        database.users.get({id: userId})
        .then(...)
        .catch(...)
      } catch (e) {
        // ...
      }
      等同于
      Promise.try(() => database.users.get({id: userId}))
      .then(...)
      .catch(...)

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
1)参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

(2)参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function (value) {
  console.log(value);  // 42
});
上面代码中,thenable对象的then()方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then()方法指定的回调函数,输出42。

(3)参数不是具有then()方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。

const p = Promise.resolve('Hello');

p.then(function (s) {
  console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve()方法的参数,会同时传给回调函数。

(4)不带有任何参数

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});
上面代码的变量p就是一个 Promise 对象。

需要注意的是,立即resolve() Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

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

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

思考题

  Promise.resolve().then(() => {
      console.log(0);
      return Promise.resolve(4);
  }).then((res) => {
      console.log(res)
  })
  
  Promise.resolve().then(() => {
      console.log(1);
  }).then(() => {
      console.log(2);
  }).then(() => {
      console.log(3);
  }).then(() => {
      console.log(5);
  }).then(() =>{
      console.log(6);
  })

  打印结果:0、1、2、3、4、5、6 😱

详细地址

赋值,深拷贝,浅拷贝区别

1.参考文章 2.参考文章-推荐 3.参考文章

image

拓展

堆和栈的区别

其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。
堆和栈都是内存中划分出来用来存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;
而堆(heap)则是动态分配的内存,大小不定也不会自动释放。

数据类型

基本数据类型有五种:number、string、boolean、null、undefined。基本数据类型存放在栈中。
存放在栈中的数据具有数据大小确定,内存空间大小可以分配、直接按值存放的特点。
所以存放在栈中的数据可以直接访问。在JavaScript中,基本的数据类型值是不可更改的。

引用数据类型:是存放在堆内存中。引用数据类型的变量并不是存放的实际值,
而是一个存放在栈内存的指针,该指针指向堆内存中的某个地址。每个数据所占的空间大小不一致,
需要根据情况进行特定的分配。与基本数据类型不同,引用类型的值是可以改变的。

赋值

赋值是将某一数值或对象赋给某个变量的过程,分两种情况:

  • 基本数据类型:赋值,赋值后两个变量互不影响
  • 引用数据类型: 赋址,两个变量指向同一个地址,同一个对象,相互之间有影响
  对基本数据类型的赋值,两个变量相互不影响:
    var a = 1;
    var b = a;
    a = 2;
    console.log(a); // 2
    console.log(b); // 1
    复制代码对引用数据类型进行赋址操作,两个变量指向同一个对象,
    改变变量a的值会影响变量b的值,哪怕改变的只是对象a中的基础数据类型:
    var a = {
        name: 'Jane',
        book: {
            name: 'Vue.js',
            price: 50
        }
    };
    var b = a;
    b.name = 'hahaha';
    b.book.price = 52;
    console.log(a); // { name: 'hahaha', book: { name: 'Vue.js', price: 52 } }
    console.log(b); // { name: 'hahaha', book: { name: 'Vue.js', price: 52 } }
    复制代码通常开发中我们不希望出现这种相互影响的情况,所以需要浅拷贝或者深拷贝。

浅拷贝

重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,只拷贝一层,不能对对象中的子对象进行
拷贝
如果属性是基本数据类型,拷贝的是基本数据类型的值;如果属性是引用类型,拷贝的是内存地址,
所以如果一个对象改变了这个地址,就会影响到另外一个对象。

简单说,浅拷贝只解决了第一层的问题,即第一层的基本数据类型和引用类型数据的引用地址

  赋值示例:
  var a = {a:2,b:2}
  var b = a
  undefined
  b.a = 3
  // a {a: 3, b: 2}
  // b {a: 3, b: 2}
  浅拷贝示例:
    var ac = {a: 2, b:3, obj: {a1:2},arr: [1,2,3]}
    var c = cloneShallow(ac) // 浅拷贝函数
    // c 等于 {a: 2, b: 3, obj: {a1:2}, arr: [1,2,3]}
    c.a = 1
    // ac.a = 2
    // 结论是 普通属性不会改变
    c.obj.a1 = 3
    // ac.obj.a1 = 3
    // 结论是 引用属性指向同一个地址 会改变
浅拷贝几种方法:
  • Object.assign()
    Object.assign()把所有可枚举属性从一个或多个对象复制到目标对象,返回目标对象
   var a = {
      name: 'Jane',
      book: {
          name: 'Vue.js',
          price: 50
      }
  };
  var b = Object.assign({}, a);
  b.name = 'hahaha';
  b.book.price = 52;
  console.log(a); // { name: 'Jane', book: { name: 'Vue.js', price: 52 } }
  console.log(b); // { name: 'hahaha', book: { name: 'Vue.js', price: 52 } }
  • ES6拓展符 ...
  var b = {...a};
  b.name = 'hahaha';
  b.book.price = 52;
  console.log(a); // { name: 'Jane', book: { name: 'Vue.js', price: 52 } }
  console.log(b); // { name: 'hahaha', book: { name: 'Vue.js', price: 52 } }
  • Array.prototype.slice()
  slice()方法返回一个新的数组对象,这个对象是由begin和end(不包括end)决定的原数组的浅拷贝。原数组不会被改变。
  var a = [0, '1', [2, 3]];
  var b = a.slice(1);
  console.log(b); // ['1', [2, 3]]
  a[1] = '99';
  a[2][0] = 4;
  console.log(a); // [0, '99', [4, 3]]
  console.log(b); // ['1', [4, 3]]
  复制代码从上面的代码可以看出,a[1]改变之后b[0]并没有改变,a[2][0]改变之后相应的b[1][0]跟着改变了。
  说明slice()是浅拷贝,相应的还有concat等。
  • 常用的循环方法
  function cloneShallow(source) {
    var target = {};
    for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
  }

深拷贝

深拷贝是对对象以及对象的所有子对象进行拷贝,浅拷贝的进阶版
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。
当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相较于浅拷贝速度慢并且花销更大。
深拷贝的两个对象互不影响。

  • JSON.parse(JSON.stringify())
    缺点:这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,
    因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),
    得到的函数就不再是函数(变
    为null)了。
    image
  • 主流解决方法
   1.es6方法

     function isObject(x) {
        return Object.prototype.toString.call(x) === '[object Object]';
     }

     function cloneDeep4(source, hash = new WeakMap()) {
  
        if (!isObject(source)) return source; 
        if (hash.has(source)) return hash.get(source); 
          
        let target = Array.isArray(source) ? [...source] : { ...source }; // 改动 1
        hash.set(source, target);
        
        Reflect.ownKeys(target).forEach(key => { // 改动 2
           if (isObject(source[key])) {
                target[key] = cloneDeep4(source[key], hash); 
            } else {
                target[key] = source[key];
            }  
        });
         return target;
      }

手工实现

loadsh解决更多的复杂判断

链表

参考链接

单链示例

image

// Node类
class Node {
    static getInstance() {
        if (Node.instance) return Node.instance;
        Node.instance = new Node('head');
        return Node.instance;
    }
    constructor(element) {
        this.element = element;
        this.next = null;
    }
}

// LinkedList类
class LList {
    constructor() {
        this.head = Node.getInstance();   //头节点
    }
    //查找给定节点
    find(item) {
        let currNode = this.head;
        while (currNode.element !== item) {
            currNode = currNode.next;
        }
        return currNode;
    }
    // 插入节点
    insert(newElement, item) {
        let newNode = new Node(newElement);
        let currNode = new LList().find(item);
        newNode.next = currNode.next;
        currNode.next = newNode;
    }
    // 显示链表元素
    display() {
        let currNode = this.head;
        console.log(currNode)
        while (!(currNode.next == null)) {
            // console.log(currNode.next.element);
            currNode = currNode.next;
        }
    }
    // 找到前一个链表元素
    findPrev(item) {
        let currNode = this.head;
        while (!(currNode.next == null) && (currNode.next.element != item)) {
            currNode = currNode.next;
        }
        return currNode;
    }
    // 删除链表元素
    remove(item) {
        let prevNode = this.findPrev(item);
        if (!(prevNode.next == null)) {
            prevNode.next = prevNode.next.next;
        }
    }
}

let fruits = new LList();
fruits.insert('Apple', 'head');
fruits.insert('Banana', 'Apple');
fruits.insert('Pear', 'Banana');
fruits.insert('Red', 'Pear');
fruits.remove('Banana');
fruits.display();

双链示例

image

// Node类
class Node {
    static getInstance() {
        if (Node.instance) return Node.instance;
        Node.instance = new Node('head');
        return Node.instance;
    }
    constructor(element) {
        this.element = element; // 元素节点
        this.next = null; // 下一节点
        this.previous = null; // 上一节点
    }
}

// LinkedList类
class LList {
    constructor() {
        this.head = Node.getInstance();
    }
    //查找给定节点
    find(item) {
        let currNode = this.head;
        while (currNode.element !== item) {
            currNode = currNode.next;
        }
        return currNode;
    }
    // 插入节点
    insert(newElement, item) {
        let newNode = new Node(newElement);
        let currNode = new LList().find(item);
        newNode.next = currNode.next;
        newNode.previous = currNode;
        currNode.next = newNode;
    }
    // 删除链表元素
    remove(item) {
        let currNode = new LList().find(item);
        console.log(currNode)
        if (!(currNode.next == null)) {
            currNode.previous.next = currNode.next;
            currNode.next.previous = currNode.previous;
            currNode.next = null;
            currNode.previous = null;
        } else {
            currNode.previous.next = null;
        }
    }
    // 显示链表元素
    display() {
        let currNode = this.head;
        while (!(currNode.next == null)) {
            currNode = currNode.next;
        }
        console.log(this.head);
    }
}

let fruits = new LList();
fruits.insert('Apple', 'head');
fruits.insert('Banana', 'Apple');
fruits.insert('Pear', 'Banana');
// fruits.insert('Red', 'Pear');
// fruits.display();
fruits.remove('Banana');
fruits.display();

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.