JavaScript核心机制

JavaScript简介

JavaScript是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。其最初被称为 LiveScript,由 Netscape(Netscape Communications Corporation,网景通信公司)公司的布兰登·艾奇(Brendan Eich)在 1995 年开发。在 Netscape 与 Sun(一家互联网公司,全称为“Sun Microsystems”,现已被甲骨文公司收购)合作之后将其更名为了 JavaScript。

JavaScript基本概况

JavaScript ( JS ) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。

JavaScript相关特征

  • JavaScript 语言区分大小写。

单线程

JavaScript 本质上是一门单线程语言。在设计当初,多处理器计算机基本很少出现在网页的主机上,而且JavaScript脚本在网页承担的角色仅仅是基本的交互问题。但是,随着Web网页越来越承载更多的功能,JavaScript逐渐开始运用到许多大型复杂的应用项目中,同时多处理器计算机也逐步广泛应用。自从 (setTimeout() 和 setInterval()) 加入到 Web API 后,浏览器提供的 JavaScript 环境就已经逐渐开始包含了任务安排、多线程应用开发等强大的特性。

标识符

一个 JavaScript 标识符必须以字母、下划线(_)或者美元符号($)开头;后续的字符也可以是数字(0-9)。

JS命名空间

全局对象

self
global
window

JavaScript虚拟机

javascript对象与相关DOM节点中的内存分配情况。

javascript执行堆栈

内存泄露

JavaScript运行时

JavaScript运行时,即JavaScript 代码执行的环境,包括JavaScript 引擎与运行时API。

多个运行时互相通信

一个 web worker 或者一个跨域的 iframe 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。

栈 Stack

函数调用形成了一个由若干帧组成的栈。

函数在栈中的实际机制

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

堆 Heap

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

栈内存与堆内存

队列 Queue

一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

消息队列

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。

  • 在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
  • 函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

Call stack(调用栈)

调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

  • 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
  • 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。

一开始,我们得到一个空空如也的调用栈。随后,每当有函数被调用都会自动地添加进调用栈,执行完函数体中的代码后,调用栈又会自动地移除这个函数。最后,我们又得到了一个空空如也的调用栈。

执行上下文

当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中。每一个上下文在本质上都是一种作用域层级。每个代码段开始执行的时候都会创建一个新的上下文来运行它,并且在代码退出的时候销毁掉。

创建执行上下文时机

全局上下文

全局上下文是为运行代码主体而创建的执行上下文,也就是说它是为那些存在于JavaScript 函数之外的任何代码而创建的。

本地上下文

每个函数会在执行的时候创建自己的执行上下文。这个上下文就是通常说的 “本地上下文”。

eval() 函数

使用 eval() 函数也会创建一个新的执行上下文。

注意事项

每一个上下文在本质上都是一种作用域层级。每个代码段开始执行的时候都会创建一个新的上下文来运行它,并且在代码退出的时候销毁掉。

执行上下文栈

每个上下文创建的时候会被推入执行上下文栈。当退出的时候,它会从上下文栈中移除。

运行时代理

在执行 JavaScript 代码的时候,JavaScript运行时实际上维护了一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其它组成部分对该代理都是唯一的。

任务(Tasks)

一个 任务 就是由执行诸如从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发之类的标准机制而被调度的任意 JavaScript 代码。

注意事项

当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行。

任务队列(task queue)

事件循环驱动你的代码按照这些任务排队的顺序,一个接一个地处理它们。在当前迭代轮次中,只有那些当事件循环过程开始时 已经处于任务队列中 的任务会被执行。其余的任务不得不等待到下一次迭代。

任务被添加到任务队列的时机

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

几种特殊任务

MessageChannel、 setTimeout(Fn)、setInterval(Fn)、requestAnimationFrame(Fn)

微任务(Microtasks)

在多个实例、所有浏览器以及运行时中,一个标准的微任务队列机制意味着这些微任务可以非常可靠的以相同的顺序执行,从而避免一些潜在的难以发现的错误。

微任务队列

每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

微任务(microtask)优先级

Promise.then > MutationObserver

执行顺序

微任务(microtask)的执行顺序在所有挂起的任务(pending tasks)完成之后,在对浏览器的事件循环产生控制之前。

添加微任务 queueMicrotask

Window 或 Worker 接口的 queueMicrotask() 方法,它将在当前任务(task)完成其工作之后运行,并且在执行上下文的控制返回到浏览器的事件循环之前,没有其他代码等待运行。

self.queueMicrotask(() => {
  // 函数的内容
})

// polyfill
if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

执行时机

晚于一段 JavaScript 执行上下文主体的退出,但早于任何事件处理函数、timeouts 或 intervals 及其他回调被执行。

使用动机

确保任务顺序的一致性,即便当结果或数据是同步可用的,也要同时减少操作中用户可感知到的延迟而带来的风险。

批量操作

可以使用微任务从不同来源将多个请求收集到单一的批处理中,从而避免对处理同类工作的多次调用可能造成的开销。

const messageQueue = [];

const sendMessage = message => {
  messageQueue.push(message);

  // 巧妙运用限制消息队列长度为1来实现仅仅向微任务队列添加一次微任务
  if (messageQueue.length === 1) {

    // 当任务队列中所有任务执行完成后,才开始执行微任务队列
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;

      // 而此时获取messageQueue是所有任务执行后的最终数据。
      console.log('批量操作:', json);
    });
  }
};

sendMessage('你好-1');
sendMessage('你好-2');
sendMessage('你好-3');
sendMessage('你好-4');

并发模型

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

事件循环(Event loops)

每个代理都是由事件循环驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。

Window 事件循环

window 事件循环驱动所有同源的窗口。“window” 实际上指的用于运行网页内容的浏览器级容器,包括实际的 window,一个 tab 标签或者一个 frame。

Worker 事件循环

包括了所有种类的 worker:最基本的 web worker 以及 shared worker 和 service worker。 Worker 被放在一个或多个独立于 “主代码” 的代理中。浏览器可能会用单个或多个事件循环来处理给定类型的所有 worker。

Worklet 事件循环

worklet 事件循环用于驱动运行 worklet 的代理。这包含了 Worklet、AudioWorklet以及PaintWorklet。

零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

  • 其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。
  • 基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。
(function () {
  console.log("这是开始");

  setTimeout(function cb() {
    console.log("这是来自第一个回调的消息");
  });

  console.log("这是一条消息");

  setTimeout(function cb1() {
    console.log("这是来自第二个回调的消息");
  }, 0);

  console.log("这是结束");
})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

执行时机

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  resolve();
  console.log(3);
}).then(function() {
  console.log(4);
});
console.log(5);
// 输出
2
3
5
4
1


// 示例二
setTimeout(()=> {console.log('2ms');}, 2);  //临界值2ms
setTimeout(()=> {console.log('1秒');}, 1);
setTimeout(()=> {console.log('1000ms');}, 1000);

setTimeout(()=> {console.log('异步执行');});

console.log(0);

new Promise((resolve, reject)=> {
  resolve();
}).then((res)=> {
  setTimeout(()=> {console.log('new Promise异步执行');});
  console.log('new Promise');
});
console.log(1);
// 输出
0
1
new Promise
1秒
异步执行
new Promise异步执行


// 示例三
console.log(0);
process.nextTick(function() {
  console.log('6');
});
new Promise((resolve, reject)=> {
  resolve();
}).then((res)=> {
  console.log('new Promise');
});
console.log(1);
// 输出
0
1
new Promise
6
  • 执行时机
console.log('同步任务 | 代码片段 | 事件循环(Event Loop)机制');

setTimeout(()=> {
  console.log('宏任务 | 一级 | setTimeout | null | 001 | [同步任务颗粒]');

  new Promise((resolve, reject) => {
    console.log('宏任务 | 一级 | setTimeout | null | 001 | new Promise');
    resolve();
  }).then(() => {
    console.log('宏任务 | 一级 | setTimeout | null | 001 | new Promise | Promise.then');
  });
});

setTimeout(()=> {console.log('宏任务 | 一级 | setTimeout | 2ms');}, 2);  // 临界值2ms
setTimeout(()=> {console.log('宏任务 | 一级 | setTimeout | 1ms | 001');}, 1);
setTimeout(()=> {console.log('宏任务 | 一级 | setTimeout | 1000ms');}, 1000);

console.log('同步任务 | 代码片段 | 编号:00001');

new Promise((resolve, reject)=> {
  console.log('同步任务 | 代码片段 | 编号:00001 | Promise');
  resolve();
}).then((res)=> {
  setTimeout(()=> {console.log('宏任务 | 微任务 | setTimeout | 0ms | 微任务 | Promise.then');});
  console.log('微任务 | Promise | 001');

  new Promise((resolve, reject) => {
    console.log('微任务 | Promise | 001 | Promise | 001');
    resolve();
  }).then(() => {
    console.log('微任务 | Promise | 001 | Promise.then | 微任务 | 001');
  });
});

setTimeout(()=> {console.log('宏任务 | 一级 | setTimeout | null | 002');});

console.log('同步任务 | 代码片段 | 编号:00002');

setTimeout(()=> {console.log('宏任务 | 一级 | setTimeout | 1ms | 002');}, 1);

new Promise((resolve, reject)=> {
  resolve();
}).then((res)=> {
  console.log('微任务 | Promise | 002');

  new Promise((resolve, reject) => {
    console.log('微任务 | Promise | 002 | Promise | 001');
    resolve();
  }).then(() => {
    console.log('微任务 | Promise | 002 | Promise.then | 微任务 | 001');
  });
});


console.log('同步任务 | 代码片段 | 编号:00003');


console.log('同步任务 | 代码片段 | 编号:00004');

原型、原型链与继承

当谈到继承时,JavaScript 只有一种结构:对象。 每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。 该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

  let Person = { name: '张三', run() { console.log('赶紧跑'); } };

  console.log(Person);
  console.log(Person.constructor);        // [Function: Object]
  console.log(Person.constructor.prototype);
  console.log(Person.__proto__); 
  console.log(Person.__proto__ == Person.constructor.prototype);    // true
  console.log(Person.__proto__ === Person.constructor.prototype);   // true
  • 性能问题

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。 使用hasOwnProperty方法检查对象是否具有自己定义的属性,g.hasOwnProperty('vertices') Object.keys()、Object.entries()

  • 结论

在使用原型继承编写复杂代码之前,理解原型继承模型是至关重要的。此外,请注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。 此外,原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。

  • 原型

Function.prototype 每个函数都有一个 prototype 属性

  • 当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。
  var o = {
    a: 2,
    m: function(){
      return this.a + 1;
    }
  };

  console.log(o.m()); // 3
  // 当调用 o.m 时,'this' 指向了 o.

  var p = Object.create(o);
  // p是一个继承自 o 的对象

  p.a = 4; // 创建 p 的自身属性 'a'
  console.log(p.m()); // 5
  // 调用 p.m 时,'this' 指向了 p
  // 又因为 p 继承了 o 的 m 函数
  // 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a' 

原型

每个函数都有一个特殊的属性叫作原型(prototype)

function thing() {}
console.log( thing.prototype );

constructor 属性

constructor 属性返回 Object 的构造函数(用于创建实例对象)。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

function thing() {}
const animal = new thing();
console.log(animal.constructor === thing); // true

{
  let o = {}
  o.constructor === Object // true
}
{
  let o = new Object
  o.constructor === Object // true
}
{
  let a = []
  a.constructor === Array // true
}
{
  let a = new Array
  a.constructor === Array // true
}
{
  let n = new Number(3)
  n.constructor === Number // true
}

__proto__属性

Object.prototype的 proto 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。

注意事项

__proto__属性从来没有被包括在 EcmaScript 语言规范中,但是现代浏览器都实现了它,尽量不要在生产环境中。同时设置对象的 [[Prototype]] 是一个缓慢的操作,如果性能是一个问题,应该避免。

推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf 和Object.setPrototypeOf/Reflect.setPrototypeOf等。

function thing() {}
const animal = new thing();

console.log( animal ); // 输出:thing {}
console.log(thing.prototype); // 输出:{}
console.log( animal.__proto__ === thing.prototype); // 输出:true
console.log( animal.__proto__.constructor === thing ); // 输出:true

console.log(thing.__proto__); // 输出:ƒ () { [native code] }
console.log(thing.__proto__.__proto__); // 输出:[Object: null prototype] {}
console.log(thing.__proto__.__proto__.__proto__); // 输出:null

原型链

每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)。

注意事项

在原型链上查找属性时,原型链上的每个可枚举属性都会被枚举出来,这对性能造成很大的消耗。

继承

function Ancestry() {
  this.name = '祖先';
}
  • 原型式继承

缺陷:父类构造函数中引用类型,全部被所有子类共享,子类修改,父类改变,导致其他任何子孙类引用改变

function Current() {
  this.label = '当前';
}
Current.prototype = new Ancestry();
const current = new Current();
console.log(current.name);
  • 构造函数继承
function Current() {
  this.label = '当前';
  Ancestry.call(this);
}
const current = new Current();
console.log(current.name);
  • 组合继承:即原型式继承与构造函数继承的组合。
  • 寄生组合继承
  • ES6继承:extends
class Current extends Ancestry {
  constructor() {
    super();
    this.state = {};
  }
}
const current = new Current();
console.log(current.name);

原型链实战运用

  • 中英文切换的模型函数
const configLanguage = {
  title: {
    cn: "中文",
    en: "英文"
  },
  introduction: {
    cn: "课程介绍",
    en: "Course Introduction",
  },
  config: {
    switch: {
      text: {
        cn: "控制",
        en: "control"
      }
    }
  }
};


export function flatLanguageModel(language = {} as Record<string, any>) {
  const currentLang = isGlobal ? "en" : "cn";

  // 转换模型
  const doFlatModel = function (
    config = {} as Record<string, any>,
    instance = {} as Record<string, any>
  ) {
    if (!config || config?.constructor !== Object) return {};

    for (const key in config) {
      // 是否为有效对象
      const value = config[key];
      if (!value || value?.constructor !== Object) {
        instance[key] = value;
        continue;
      }

      // 是否为中英文配置
      else if (value.hasOwnProperty("cn") && value.hasOwnProperty("en")) {
        instance[key] = value[currentLang];
      }

      // 子级嵌套
      else {
        doFlatModel(value, (instance[key] = {}));
      }
    }
  };

  const flatLanguage: Record<string, any> = Object.create(language);
  doFlatModel(language, flatLanguage);

  // 返回转换后的语言结构
  return flatLanguage;
}


// 自动加载不同语言环境内容
export function initLanguage() {
  const flatLanguage = flatLanguageModel(configLanguage);
  return flatLanguage;
}


const Lang = initLanguage();
console.info("Lang: ", Lang);
/**
 * 转换结果
  flatLanguage:  {
    title: '中文',
    introduction: '课程介绍',
    config: { switch: { text: '控制' } }
  }
 */

this作用域

注意浏览器环境与node环境,例如浏览器环境中的self对象,而node环境中的global对象等。

浏览器环境

function Scope() {
  console.log(this);
}
const Global = {
  test: () => {
    console.log('test:', this);
  },
  debug() {
    console.log('debug:', this);
  },
  debugger: function() {
    console.log('debugger:', this);
  },
  do() {
    return function() {
      console.log('do:', this);
    }
  },
  run() {
    return () => {
      console.log('run:', this);
    }
  },
  closure() {
    (function() {
      console.log('closure:', this);
    })();
  },
  callback() {
    (() => {
      console.log('callback:', this);
    })();
  },
  currying() {
    return () => {
      return function() {
        console.log('currying:', this);
      }
    }
  },
};

console.log(this); // 输出:Window对象
console.log(self); // 输出:Window对象
Scope(); // 输出:Window对象
new Scope(); // 输出:Scope对象

Global.test(); // 输出:Window对象
// new Global.test(); // Uncaught TypeError: Global.test is not a constructor
Global.debug(); // 输出:Global对象
// new Global.debug(); // Uncaught TypeError: Global.debug is not a constructor
Global.debugger(); // 输出:Global对象
new Global.debugger(); // 输出:debugger {}构造函数
Global.do()(); // 输出:Window对象 
Global.run()(); // 输出:Global对象
Global.closure(); // 输出:Window对象 
Global.callback(); // 输出:Global对象
Global.currying()()(); // 输出:Window对象

服务器环境


this指向

[~] 指向元素 [this处于元素对象调用的函数作用域内]

<button id='element' data-name='数据'>按钮</button>
let elem = document.getElementById('element');
elem.addEventListener('click', function() {
console.log(this); 
console.log(this.getAttribute('data-name')); 
}, false);

数据

[~] 指向window [this处于window上下文环境]

  console.log(this); 

//输出结果

[object Window]

[~] 指向window [this处于通过function声明的函数对象且该函数为非匿名函数的函数内部且执行该函数对象时]

function persion() {
	console.log(this);
}
persion();

[~] 指向实例对象一

let persion = {
name: '张三',
cry: function() {
console.log(this == persion);
}
};
persion.cry();

//输出结果
true

[~] 指向实例对象二

var door = {
number: 001,
rotate: function() {
console.log(this == door);
this.open = function() {
console.log(this == window);
console.log('开门');
}
}
};
door.rotate();
door.rotate.open();

//输出结果
true
false
开门

[~] 对象中的嵌套函数其上下文环境是全局的window对象,而非包含它的那个对象,但是可以用一个名为that的变量来保存这个对象引用

var apartment = {
isLocked: false,
lock: function() {
var that = this;  //
this.isLocked = true;  // 设置isLocked属性
function doSomething() {
console.log(this === apartment);  ->false
console.log(this === window);  ->true
console.log(that === apartment);  ->true
that.isLocked = false;  //修改apartment对象的isLocked属性
}
doSomething();
}
};
apartment.lock();
console.log(apartment.isLocked); ->false

[~] 在使用new关键字创建对象时,this指通过构造函数所创建的那个对象实例

function Accommodation() {
this.floors = 0;  //创建该函数实例对象时,this指向该实例对象
		  this.rooms = 0;
		  this.sharedEntrance = false;
		  this.isLocked = false;
		  this.lock = function() {
		  	this.isLocked = true;
		  };
		  this.unlock = function() {
		  	this.isLocked = false;
		  };
}
var house = new Accommodation(); 
var apartment = new Accommodation();
	    console.log(house.floors);  ->0
house.floors = 2;
		  apartment.lock();
		  console.log(apartment.isLocked);  ->true
		  apartment.unlock();
this.isLocked = false;
		  console.log(apartment.isLocked);  ->false

[~] 方法的链式调用:要实现链式调用,只需在“类”中的每个方法最后通过this关键字返回对象实例的引用即可

function Accommodation() {}
Accommodation.prototype.isLocked = false;
Accommodation.prototype.lock = function() {
	this.isLocked = true;
	return this;
};
Accommodation.prototype.unlock = function() {
	this.isLocked = false;
	return this;
};
Accommodation.prototype.alarm = function() {
	console.log("Sounding alarm!");
	return this;
};
var house = new Accommodation(); 
house.lock().alarm().unlock();

变量与函数

var、let、const

  • var:声明一个变量,可选初始化一个值。
  • let:声明一个块作用域的局部变量,可选初始化一个值。
  • const:声明一个块作用域的只读常量。

变量作用域

程序源代码中定义这个变量的区域。

声明变量

  • 使用字面量创建变量
const name = '';
const array = [];
const student = {};

声明提前 Hoisting(变量提升)

JavaScript函数里声明的所有变量(但不涉及值)都被提前至函数体的顶部。JavaScript 只会提升声明,不会提升其初始化。如果一个变量先被使用再被声明和赋值的话,使用时的值是 undefined。[并非代码结构的移动,而是执行上下文的提升]

var print = function() {
  console.log('字面量');
}
function print() {
  console.log('函数');
}

print(); // 输出:字面量
console.log(version); // 输出:undefined
var version;
console.log(version); // 输出:undefined
version = '1.0.0';
console.log(version); // 输出:1.0.0

函数名提升

函数声明会在内部被提升到其定义所在函数体的顶部。[并非代码结构的移动,而是执行上下文的提升]

hoisting();  

function hoisting() {  
  console.log("函数名提升");  
}  

作用域对象

函数被执行的时候,解释器就会创建一个新的,特定的作用域对象。且不能从 JavaScript 代码中直接访问作用域对象,也没有 可以遍历当前作用域对象中的属性 的方法。

作用域链

作用域对象组成了一个名为作用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相类似。

形式参数与实际参数

function add(a, b) {
  // 实际参数
  console.log(arguments.length);  //1
}
add(1);

// 形式参数
console.log(add.length);  //2

高阶函数

高阶函数(Higher-Order Function),即接收另一个函数作为参数的函数。

array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
array.sort(function(a, b) {});

箭头函数

  • 箭头函数体中的 this 对象,是定义函数时执行上下文的对象,而不是使用函数时的对象。
  • 不可以作为构造函数,也就是不能使用 new 命令,否则会报错。

new 操作符操作流程

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

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤 1 新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

构造器

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function thing() {}
// thing就是animal的构造函数,即构造器。
const animal = new thing();

递归函数

每次递归调用自身都会创建一个新的上下文。这使得 JavaScript 运行时能够追踪递归的层级以及从递归中得到的返回值,但这也意味着每次递归都会消耗内存来创建新的上下文。

函数字符串

  • eval()函数:将字符串强制转换为可执行程序
let solve = "console.log(10)";
eval(solve);  //10
  • new Function():函数构造器,将字符片段转换为可执行代码
  • new Function([args...], 函数体)
let evalCode = "console.log('hello')";
(new Function(evalCode))();
let sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));

函数重载

判断传入参数数量、判断传入参数类型。

function sendMessage(x, y) {
	if (arguments.length == 3) {
		var z = x + y;
		return z;
	} else {
		console.log("传入参数错误");
	}
}
function test(a) {
	if (a == "undefined") {
		a = "默认文本";
	}
	console.log(a);
}

call、apply与bind

call

使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

const studentList = [{ name: '张三' }, { name: '张思' }, { name: '张数' }, { name: '张苏' }];
studentList.push.call(studentList, { name: "李四" });
console.log(studentList); // [{ name: '张三' }, { name: '张思' }, { name: '张数' }, { name: '张苏' }, { name: "李四" }]


// 批量添加数组 | Array.splice方法也可以实现 [].splice([].length - 1, 0, ...[]);

apply

调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

// 找出数组中最大/小的数字
console.log(Math.max.apply(null, [5, 6, 2, 3, 7]));

// 找出数组中最大/小的数字
console.log(Math.min.apply(null, [5, 6, 2, 3, 7])); 


const studentList = [{ name: '张三' }, { name: '张思' }, { name: '张数' }, { name: '张苏' }];
studentList.push.apply(studentList, [{ name: "李四" }]);
console.log(studentList);  // [{ name: '张三' }, { name: '张思' }, { name: '张数' }, { name: '张苏' }, { name: "李四" }]

bind

创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

this.age = 99;
var students = {
  age: 15,
  getAge: function() {
    return this.age; 
  }
};
console.log(students.getAge());          // 81

var zhangsan = students.getAge;          // 指向window或global对象
console.log(zhangsan());                 // 返回 9 - 因为函数是在全局作用域中调用
console.log(zhangsan.bind(students)());  // 81



this.eventModel.bind(this)();

手动实现bind

Function.prototype.bind = function(content) {
  const self = this;
  let args = Array.prototype.slice.call(arguments, 1);

  return function() {
    return self.apply(context, args);
  }
}

闭包机制

一个闭包,就是 一个函数 与其 被创建时所带有的作用域对象 的组合。在有效作用域内,通过引用避开GC垃圾回收机制,从而维持作用域相关变量状态。

注意事项

请不要滥用闭包,否则容易引起内存泄露问题。

柯里化函数 Currying

柯里化函数,把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数。

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

  return function() {
    newArgs = args.concat(Array.prototype.slice.call(arguments));
    console.log('callback:', newArgs, arguments);

    if (newArgs.length < length) {
      return currying.call(this, fn, newArgs);
    }

    return fn.apply(this, newArgs);
  }
}

function executeModel(a, b, c) {
  return a * b * c;
}
const execute = currying(executeModel);

console.log(execute(2, 3, 4));
console.log(execute(2, 3)(4));
console.log(execute(2)(3)(4));
console.log(execute(2)(3, 4));

JavaScript 闭包:https://www.runoob.com/js/js-function-closures.html 闭包:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures 案例:https://stackoverflow.com/questions/111102/how-do-javascript-closures-work

闭包机制

定义:由函数(环境)及其封闭的自由变量组成的集合体; 特征:内层的函数可以引用存在于包围它的函数内的变量,即使外层函数的执行已经终止; 用途:读取函数内部的变量;让这些变量的值始终保持在内存中,并没有在外部函数调用后被自动清除; 弊端:由于变量值保存在内存中,因此大量使用闭包会造成极大的内存开销,会严重影响CPU性能。

// 抛出问题
async function Case() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(new Date, i);
    }, 1000);
  }
  console.log('out', i);
}


// 闭包
let add = (function() {
  let count = 0;
  return function() {
    return count++;
  }
})();
  
console.log(add());
console.log(add());
console.log(add());


// 解决方案一
async function Case1() {
  function closure(i) {
    setTimeout(function() {
      console.log(new Date, i);
    }, 1000);
  }
  for (var i = 0; i < 5; i++) {
    closure(i);
  }
  console.log('out', i);
}


// 解决方案二
async function Case2() {
  for (var i = 0; i < 5; i++) {
    (function(i) {
      setTimeout(function() {
        console.log(new Date, i);
      }, 1000);
    })(i);
  }
  console.log('out', i);
}


// 解决方案三 | 巧妙使用window.setTimeout(fn, milliseconds, [params, ...]); | 兼容性 传给执行函数的其他参数(IE9 及其更早版本不支持该参数)
for (var i = 0; i < 5; i++) {
  setTimeout(function(i) {
    console.log(new Date, i);
  }, 1000, i);
}
console.log('out', i);

Promise实现机制

基于Promise的API包括WebRTC,Web Audio API,Media Capture and Streams等。

Promise状态

  • pending:初始状态,既不满足也不拒绝。
  • resolved:表示操作已成功完成。
  • rejected:表示操作失败。

Promise属性

  • Promise.length
  • Promise.prototype

Promise方法

  • Promise.all(iterable):在所有输入的 promise 的 resolve 回调都结束
  • Promise.race(iterable):一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
  • Promise.reject(reason):返回一个带有拒绝原因的 Promise 对象。
  • Promise.resolve(value):返回一个以给定值解析后的 Promise 对象。
  // 延迟获取,避免获取的值是上一次的数据
  await new Promise((resolve) => { window.setTimeout(() => { resolve() }, 50) });
  // await new Promise<void>(resolve => { window.setTimeout(() => { resolve(); }, 50); });





Promise.resolve().then(() => {});


const A = new Promise((resolve, reject)=> {setTimeout(()=> { resolve('300');}, 300);});
const B = new Promise((resolve, reject)=> {setTimeout(()=> {resolve('100');}, 100);});
const C = new Promise((resolve, reject)=> {setTimeout(()=> {resolve('500');}, 500);});

Promise.race(iterable)

等待最后一个执行完成后,依次按照参数顺序返回一个数组参数

Promise.all([A, B, C])
  .then((res)=> {
    console.log(res); // 返回数组:[ '300', '100', '500' ]
  })
  .catch(()=> {
    console.log('程序错误');
  });

Promise.all()

返回优先执行完成的第一个参数

Promise.race([A, B, C])
  .then((res)=> {
    console.log(res);  // 返回单个对象:100
  })
  .catch(()=> {
    console.log('程序错误');
  }); 

手写Promise对象

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'


/**
  * Promise构造函数
  * @param {*} executor 执行器函数
  */
function Promise(executor) {
    const self = this
    // 给promise对象指定status属性,初始值为pending
    self.status = PENDING
    // 给promise对象指定一个用于存储结果数据的属性
    self.data = undefined
    // 每个元素的结构 { onResolved(){}, onRejected() }
    self.callbacks = []


    function resolve(value) {
        // 此处做判断,使得promise的状态只能修改一次
        if (self.status === PENDING) {
            // 将状态改为 resolved
            self.status = RESOLVED
            // 保存value数据
            self.data = value

            // 如果有待执行的callback函数,立即异步执行回调
            if (self.callbacks.length > 0) {
                setTimeout(() => { // 表示在异步队列中执行
                    self.callbacks.forEach(callbacksObj => {
                        callbacksObj.onResolved(value)
                    })
                }, 0);
            }
        }
    }
    function reject(reason) {
        // 此处做判断,使得promise的状态只能修改一次
        if (self.status === PENDING) {
            // 将状态改为 resolved
            self.status = REJECTED
            // 保存value数据
            self.data = reason

            // 如果有待执行的callback函数,立即异步执行回调
            if (self.callbacks.length > 0) {
                setTimeout(() => { // 表示在异步队列中执行
                    self.callbacks.forEach(callbacksObj => {
                        callbacksObj.onRejected(reason)
                    })
                }, 0);
            }
        }
    }

    // 立即执行器
    try {
        executor(resolve, reject);
    } catch (error) { // 如果执行器抛出异常,promise状态变为rejected状态
        reject(error)
    }
}

/**
  * Promise原型对象的then方法
  */
Promise.prototype.then = function (onResolved, onRejected) {
    const self = this
    onResolved = typeof onResolved === 'function' ? onResolved : value => value
    // 指定默认的失败的回调(实现错误/异常穿透的关键点)
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }


    return new Promise((resolve, reject) => {

        /**
          * 处理onResolve和onRejected函数
          * @param {*} callback 
          */
        function resolvePromise(callback) {
            try {
                const result = callback(self.data)
                if (result instanceof Promise) {
                    result.then(resolve, reject)
                } else if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
                    // 拿到result.then
                    const then = result.then;
                    if (typeof then === 'function') {
                        then(resolve, reject)
                    } else {
                        resolve(then)
                    }
                } else {
                    resolve(result)
                }
            } catch (error) {
                reject(error)
            }
        }


        if (self.status === RESOLVED) {
            setTimeout(() => {
                /**
                  * 1. 如果抛出异常,return 的 promise就会失败,reason就是error
                  * 2. 如果回调函数返回不是promise,return的promise就会成功,value就是返回值
                  * 3. 如果回调函数返回的是一个promise,return的promise的结果就是这个promise的结果,value就是返回值
                  */
                resolvePromise(onResolved)
            }, 0);
        } else if (self.status === REJECTED) {
            setTimeout(() => {
                resolvePromise(onRejected)
            }, 0);
        } else {
            self.callbacks.push({
                onResolved(value) {
                    resolvePromise(onResolved)
                },
                onRejected(reason) {
                    resolvePromise(onRejected)
                }
            })
        }
    })
}

/**
  * Promise原型对象的catch方法
  */
Promise.prototype.catch = function (onRejected) {
    return this.then(undefined, onRejected)
}

/**
  * Promise函数对象的resolve方法
  * 
  * 返回一个指定结果的成功的promise
  */
Promise.resolve = function (value) {
    return new Promise((resolve, reject) => {
        if (value instanceof Promise) {
            value.then(resolve, reject)
        } else {
            resolve(value)
        }
    })
}

/**
  * Promise函数对象的reject方法
  */
Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason)
    })
}

/**
  * Promise函数对象的all方法
  */
Promise.all = function (promises) {
    // 判断数组
    if (!(promises instanceof Array)) {
        return Promise.reject(new Error('params must be a Array!'))
    }
    // 用来保存所有数据成功value的数组
    const resultArray = new Array(promises.length)
    // 用来保存成功数量的计数
    let resultCount = 0
    return new Promise((resolve, reject) => {
        // 遍历获取每个promise的结果
        promises.forEach((p, index) => {
            Promise.resolve(p).then(value => {
                resultCount++
                resultArray[index] = value
                // 如果全部都成功了,将return的promise变为成功
                if (resultCount === promises.length) {
                    // 表示成功
                    resolve(resultArray)
                }
            }, reason => {
                reject(reason)
            })
        })
    })
}

/**
  * Promise函数对象的race方法
  */
Promise.race = function (promises) {
    // 判断数组
    if (!(promises instanceof Array)) {
        return Promise.reject(new Error('params must be a Array!'))
    }

    return new Promise((resolve, reject) => {
        // 遍历数组
        promises.forEach((p, index) => {
            Promise.resolve(p).then(value => {
                resolve(value)
            }, reason => {
                reject(reason)
            })
        })
    })
}

/**
  * 返回一个promise对象,它在指定的时间后才确定成功结果
  * @param {*} value 
  * @param {*} time 
  */
Promise.resolveDelay = function (value, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (value instanceof Promise) {
                value.then(resolve, reject)
            } else {
                resolve(value)
            }
        }, time);
    })
}


/**
  * 返回一个promise对象,它在指定的时间后才确定失败结果
  * @param {*} reason 
  * @param {*} time 
  */
Promise.rejectDelay = function (reason, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(reason)
        }, time);
    })
}

// 向外暴露函数
window.Promise = Promise;

数据类型

javascript数据类型(八种)分为基本类型(七种)和引用类型(一种)。

基本类型(七种)

基本类型,即值类型。

基本特征

  • 基本数据类型的值是按值访问的,即值传递。
  • 值类型(基本类型):栈内存(不包含闭包中的变量)
  • 基本类型以栈的数据结构存储在内存中。
  • 基本类型的变量存储在栈内存中
  • 栈内存中包括了变量的标识符和变量的值
  • 基本类型(基本数值、基本数据类型)是一种既非对象也无方法的数据。

未定义(Undefined)

// undefined | void 0
console.log(undefined == void 0);   // true 
console.log(undefined === void 0);  // true

let name;  
console.log(name === undefined); //输出:true  
console.log(name == null); //输出:true  
console.log(name === null); //输出:false 

空(Null)

  • 为什么会出现typeof null === 'object'?
  • 不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。

布尔(Boolean)

字符串(String)

数字(Number)

BigInt 类型

Symbol

用来定义唯一值。

let sy = Symbol("KK");
console.log(sy); // 输出:Symbol(KK)
let sk = Symbol("KK");
console.log(sk); // 输出:Symbol(KK)
console.log(sy == sk); // 输出:false
console.log(sy === sk); // 输出:false

引用类型(一种)

Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型、Math类型、Map对象、Set对象。 引用类型的值是按引用访问的,即地址传递。

隐式类型转换

将类型转换到环境中应该使用的类型。

显式类型转换

转换为数字:Number(value)、parseInt(value)、parseFloat(value) 将对象转换成字符串:String(value)

  var arr = {"JavaScript","VBScript","Script"};  
  document.write(arr.toString()); 

常量

布尔常量:即true与false。
数组常量:data_name = {value1,value2,value3,...,valuen}

let a = 0;
let b = 0;
b++;

console.info(a);
let o = {};
o.a = 0;
let b = o;
b.a = 10;

console.info(o.a);
let a = { n: 1 };
let b = a;
a.x = a = { n: 2 };

console.info(a.x);
console.info(b);

引用类型(对象类型)

引用类型:堆内存(包含闭包中的变量) 栈内存中保存了变量标识符和指向堆内存中该对象的指针 堆内存中保存了对象的内容 引用类型的值是保存在堆内存(Heap)中的对象(Object)

  • 对象(Object)
  • 数组(Array)
  • 函数(Function)
  • 正则(RegExp)
  • 日期(Date)
  • Map(映射)
  • Set(集合)

引用

  • 值引用
    • 数字、字符串、布尔类型的为原始类型,是值引用
    • 值引用可以深拷贝
  • 地址引用
    • 数组、对象类型为地址引用
    • 地址引用循环到原始类型方可进行深拷贝

注意事项

引用类型以堆的数据结构存储在内存中。

包装类型

在基本数据类型中存在String、Number、Boolean三个基本数据类型,这三个基本类型都有自己对应的包装对象。在正常声明这三个基本数据类型的变量中,可以直接基于变量调用相关的方法,这是因为JS解析过程中默认为这三种基本数据类型添加了对象包装,即如下示例。

const str = 'Hello';
console.log(str.charAt(0));

// JS默认转换结果
const str = new String('Hello');

属性、实例与原型对象关系判断

  • constructor:返回创建对象的原型函数。
  • prototype:允许你向对象添加属性或方法。
  • instanceof:检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • isPrototypeOf:测试一个对象是否存在于另一个对象的原型链上。
  • hasOwnProperty:返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键),该方法会忽略掉那些从原型链上继承到的属性。
  • in:如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。
  • Reflect.has(target, propertyKey):判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
  • Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响)。

typeof 检测普通类型

可以检测变量:"string"、"number"、"bigint"、"boolean"、"symbol"、"undefined"、"object"、"function"等。

console.log(typeof 0);              //number  
console.log(typeof 'hello');        //string  
console.log(typeof false);          //boolean  
console.log(typeof undefined);      //undefined  
console.log(typeof function() {});  //function

console.log(typeof null);  //object  
console.log(typeof {});  //object  

typeof a === "string"
typeof a === "number"
typeof a === "bigint"
typeof a === "boolean"
typeof a === "symbol"
typeof a === "undefined"
typeof a === "object"
typeof a === "function"

检测数组

console.log([].constructor === Array);  //true
console.log(Array.isArray([]));         //true
console.log(arr instanceof Array);      //true

检测对象

console.log(typeof null); // 输出:object
console.log(typeof []); // 输出:object
console.log(typeof {}); // 输出:object

// 真实判断
console.log(({})?.constructor === Object); // true
console.log((null)?.constructor === Object); // false

constructor

返回创建原型实例的构造函数原型对象

console.log("".constructor === String);                 //true
console.log((1).constructor === Number);                //true
console.log((function() {}).constructor === Function);  //true
console.log(([]).constructor === Array);                // true
console.log(({}).constructor === Object);               //true
console.log((false).constructor === Boolean);           //true
console.log((new Map()).constructor === Map);           //Map
console.log((new Set()).constructor === Set);           //Set

instanceof运算符

检测某实例是否为某对象的实例,判断某个构造函数的 prototype 属性所指向的对象是否存在于另外一个要检测对象的原型链上

console.log(({}) instanceof Object);                //true
console.log(([]) instanceof Array);                 //true
console.log((/aa/g) instanceof RegExp );            //true
console.log((function(){}) instanceof Function);    //true

类型强制转换

  • 数字转换为字符串
console.log('' + 1);          // '1'
  • 字符串转换为数字
parseInt('10')
parseFloat('1')
console.log([] + 1);          // 1

console.log(null + 1);        // 1
console.log(undefined + 1);   // NaN

相同数据类型的子集判断

// JS判断字符串A是否为字符串B的子集?
StringB.includes(StringB);

// JS判断数组A是否为数组B的子集?
function judgeSubset(arrayA, arrayB) {
  
}

表达式

语句和语法

语句

条件语句

// if 语句

// if-else 语句

// if-else-if 语句

// switch 语句

if () {}语句

  let count = 0;
  if (count > 0) {
    console.log(0);
  }

if () {} else {}

  let count = 0;
  if (count == 0) {
    console.log(0);
  } else {
    console.log(1);
  }

if () {} else if {} else {}

  let count = 10;

  if (count > 20) {
    console.log("你好-20");
  } else if (count > 10) {
    console.log("你好-10");
  } else if (count > 0) {
    console.log("你好-0");
  } else {
    consle.log("你好");
  }

switch 语句

  let count = 0;
  switch (count) {
    case 0: console.log(0);
    break;
  case 1: {
    console.log(1);
    console.log('输出1');
  }
  break;
  default: console.log('bug');
  }

while 循环语句

  let count = 0;
  while (count < 10) {
    console.log(count);
    count ++;
  }

do {} while () 循环语句

  let count = 0;
  do {
    console.log(count);
    count ++;
  } while (count < 10);

for () {} 循环语句

  for (let count = 0; count < 10; count++) {
    console.log(count);
  }

for ( in ) { } 循环语句

  // 数组
  let arr = [0, 1, 2, 3, 4, 5];
  for (let x in arr) {
    console.log(arr[x]);
  }
  // 对象
  let obj = {a: 100, b: 200, c: 300};
  for (let i in obj) {
    console.log(i + ' = ' + obj[i]);
  }

break 语句:跳出整个循环

  for (let count = 0; count < 5; count ++) {
    if (count == 3) {
      break;
    }
    console.log(count);
  }

continue 语句:跳出本次循环

  for (let count = 0; count < 5; count ++) {
    if (count == 3) {
      continue;
    }
    console.log(count);
  }

return 语句


with 语句:设置代码在特定对象中的作用域

不推荐使用,性能不可预测 优

  let message = 'with';
  with (message) {
    console.log(toUpperCase());
  }

---------------------------------------

循环语句

---------------------------------------

for:在循环头部测试循环条件

  // for 语句
  for (let i = 0; i < Array.length; i++) {
    // 循环体 ...
  }

  // for-in 语句
  for (n in set) {
    //
  }

while:在循环头部测试循环条件

  let i = 0;
  while(i < 5) {
    // 循环体 ...
    i++;
  }

do...while:在循环的尾部检查它的条件,因此会确保至少执行一次循环

  let i = 0;
  do {
    // 循环体 ...
    i++;
  } while(i < 5);

循环语句

break:无条件跳出并结束当前的循环结构。 continue:忽略其后的语句并结束当前条件循环而重新开启新的一轮循环。 goto;

---------------------------------------

异常语句

---------------------------------------

  // try-catch 语句

  // try-catch-finally 语句

  // throw 语句

  // 

for...in与for...of差异

Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

console.log(iterable); // { 0: 3, 1: 5, 2: 7, foo: "hello", length: 3 }
console.log(iterable.length); // 3

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // 输出:0, 1, 2, "foo"
  }
}

for...in 语句

依次访问一个对象及其原型链中所有可枚举的属性。 以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

注意事项

for...in 语句输出值是迭代对象的属性,建议仅用于调试。

const studentConf = { name: "张三", sex: "男", age: 28, money: '100亿' };

for (let key in studentConf) {
  console.log(key); // name, sex, age, money
}


const studentList = [{ name: "张三" }, { name: "李四" }, { name: "王二" }];

for (let key in studentList) {
  console.log(key); // 0, 1, 2
}

for...of 语句

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

注意事项

for...of 语句输出值是迭代对象的内容数据

const studentList = [{ name: "张三" }, { name: "李四" }, { name: "王二" }];

for (item of studentList) {
  console.log(item); // { name: "张三" }, { name: "李四" }, { name: "王二" }
}

相关知识体系

浅拷贝

  • 仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变
  • 只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用
  • Array、Object的拷贝属于浅拷贝,因此需要处理因为浅拷贝带来的问题

解决方案:对象

const studentConf = {
  name: "Eshen",
  age: 21
};

const moreConf = {
  ...studentConf
};

moreConf.name = "张三";
studentConf.name = "李四";

console.log(studentConf); // { name: '李四', age: 21 }
console.log(moreConf); // { name: '张三', age: 21 }

解决方案:数组

// 推荐方法
const list = [];

const copyList = list;

const otherList = [];
otherList.push.apply(otherList, list);

list.push(1);

copyList.push(1);

console.log("list: ", list); // [ 1, 1 ]
console.log("copyList: ", copyList); // [ 1, 1 ]
console.log('otherList: ', otherList); // []






// 方法一
const studentList = [0, 2, 3];
const childList = [...studentList];

studentList.push(4);

console.log(studentList);  // [ 0, 2, 3, 4 ]
console.log(childList); // [ 0, 2, 3 ]

// 方法二
const studentList = [0, 2, 3];
const childList = studentList.concat();

studentList.push(4);

console.log(studentList);  // [ 0, 2, 3, 4 ]
console.log(childList); // [ 0, 2, 3 ]





// 案例
const fileDir = ERP_DATABASE_PATH + 'list';

const fileList = fs.readdirSync(fileDir);
const fileUrlList = fileList.map(file => path.join(fileDir, file));

const countLimit = 20;

// 复制数组,直到数组长度等于countLimit
const copyArrayFunc = function (arrayList = [], arraySize = 20) {
  const _arrayList = [...arrayList];

  if (_arrayList.length >= 20) {
    return _arrayList.slice(0, 20);
  }

  _arrayList.push.apply(_arrayList, _arrayList);
  return copyArrayFunc(_arrayList);
}

for (let i = 0; i < fileUrlList.length; i++) {
  const fileUrl = fileUrlList[i];
  const defaultList = await readFileUtil(fileUrl);

  const copyArray = copyArrayFunc(defaultList);
  console.log(defaultList.length, copyArray.length);
}

深拷贝

  • 在计算机中开辟了一块内存地址用于存放复制的对象
  • 可以利用递归的思想来做,既省性能,又不会发生引用
  • 值类型(基本类型)之间的拷贝属于深拷贝
let a = 0;
let b = a;

console.info('a=', a, ' | b=', b); // a= 0  | b= 0
a = 10;
console.info('a=', a, ' | b=', b); // a= 10  | b= 0


// 示例二
const students = { count: 0 };
const name = students.count;

students.count += 1;

console.info("name: ", name); // name: 0

DOMContentLoaded与onload

  • DOMContentLoaded:当所有的CSS脚本与JS脚本加载且执行完成,才触发DOMContentLoaded监听器
document.addEventListener('DOMContentLoaded', function() {
  console.log('document.onDOMContentLoaded');
}, false);
  • onload:当所有的资源加载且执行完成后,才触发onload监听器
window.onload = function() {  
  console.log('window.onload');
}

Object.create()、new object()和{}

创建null的区别: 创建非null的区别:

async与await的实现原理

迭代器(generator)的语法糖,将generator函数的*换成async,将yield替换成await。

函数声明和函数表达式

  • 函数表达式中的变量值不存在提升,所以前置调用时无法获取变量的函数
  • 用函数声明创建的函数可以在函数定义之前就进行调用,因为函数声明在JS解析时进行函数提升,在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用;
  • 用函数表达式创建的函数不能在表达式对象被赋值之前进行调用,因为函数表达式的值是在JS运行时确定,并且在表达式赋值完成后,该函数才能调用。
// 函数声明
function  add() {console.log(1);};    

// 函数声明式
const add = function() {console.log(2);};    

new操作符实例化过程

  • 创建一个空的简单JavaScript对象(即{});
  • 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 将步骤1新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。

null和undefined

本质上的区别:null代表不存在而为空,undefined代表存在但未定义。

DOM事件流的三个阶段

  • 事件捕获阶段:首先发生事件捕获,为截获事件提供机会;
  • 处于目标阶段:然后是实际目标接收到事件;
  • 事件冒泡阶段:可以在这个阶段对事件作出相应。

Object.defineProperty

Object.defineProperty()无法监听属性的添加与删除,数组索引和长度的变更。

Set、Map、WeakSet和WeakMap

  • Set是一种存储任何类型唯一值的集合,WeakSet仅仅存储对象的唯一值集合且没有被引用时自动被系统进行垃圾回收。
  • Map是存储任何键值对的字典,WeakMap要求其键必须是对象而其值为任何值的字典且没有被引用时自动被系统进行垃圾回收。

代码编程案例

节流 Throttle

  • 限制单位时间内的执行频率,例如500ms内仅仅执行一次。应用场景为表单提交、滚动、调整窗口等。
const throttle = (function() {  
  var timeout = null;    
  return function(fn, wait) {     
    console.log(timeout);
    if(timeout) clearTimeout(timeout); 
    timeout = setTimeout(fn, wait);    
  }
})();

document.querySelector('#text').addEventListener('input', (e) => {
  throttle(() => {
    console.log(e);
  }, 2000);
}, false);
  • 利用Lodash第三方库
const throttle = require('lodash/throttle');
document.querySelector('#text').addEventListener('resize', debounce(() => {
  // 节流任务程序
}, 150), false);

防抖 Debounce

限制有效时间内禁止执行的约束,例如执行操作停止后超过100ms才可以执行设定的相关任务。应用场景主要为搜索联想功能等。

function debounce() {
  //
}



// 新思路
function debounce(fn, wait) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), wait);
  };
}
  • 利用Lodash第三方库
const debounce = require('lodash/debounce');
window.addEventListener('resize', debounce(() => {
  // 防抖任务程序
}, 150), false);

浅拷贝与运算符优先级

var a = { k1: 1 };
var b = a;
console.log(a);           // {k1: 1}
console.log(b);           // {k1: 1}

a.k3 = a = { k2: 9 };

console.log(a);           // {k2: 9}
console.log(b);           // {k1: 1, k3: { k2: 9 }  }

非中间变量交换变量

let a = 1, b = 2;
console.log(`交换前数据:a=${a}; b=${b}`);
a = a + b;
b = a - b;
a = a - b;
console.log(`交换后数据:a=${a}; b=${b}`);

数组去重

  • 方法一
Array.prototype.filterRepeat = function() {
  let array = this;
  let _array = [];
  let i = array.length - 1;
  let obj = {};
  while(i >= 0) {
    if (!obj[array[i].key]) {
      _array.push(array[i]);
      obj[array[i].key] = true;
    }
    i--;
  }
  return _array;
}
let array = [{key: 0}, {key: 2}, {key: 0}, {key: 2}];
console.log(array.filterRepeat());  //[ { key: 2 }, { key: 0 } ]
  • 方法二
//遍历, 遍历数组,将不存在的数据添加至新队列中。
async function ergodic(array) {
  let queue = [];
  
  for (let i = 0; i < array.length; i++) {
    let value = array[i];
    if (queue.indexOf(value) === -1) {
      queue.push(value); 
    }
  }
  return queue;
}
  • 方法三
//数组下标
//利用Array.indexOf()的功能,如果值存在,则返回该值在数组中第一次出现的位置;那么如果数组中存在重复的数据,则返回的位置就不等于它在数组中的位置。
async function subscript(array) {
  let queue = [];
  
  array.forEach((value, index, arr)=> {
    if (arr.indexOf(value) === index) {
      queue.push(value);
    }
  });
  return queue;
}
  • 方法四
//排序后位置相邻去除, 先排序,遍历数组过滤相邻重复值
async function duplicate(array) {
  let queue = [];
  
  array.sort();
  array.forEach((value, index, arr)=> {
    if (index  > 0) {
      if (arr[index] != queue[queue.length - 1]) {
        queue.push(value);
      }
      return ;
    }
    queue.push(value);
  });

  return queue;
}
  • 方法五
//优化遍历数组 
async function duplicate(array) {
  let queue = [];
  
  for (let i = 0; i < array.length; i++) {
    for (let j = i+1; j < array.length; j++) {
      if (array[i] === array[j]) {
        ++i;
      } 
    }
    queue.push(array[i]);
  } 

  return queue;
}
  • 方法六
//ES6 Set()
async function duplicate(array) {
  let queue = new Set(array);
  
  return [...queue];
}

数组对象最小下标和

筛选两个数组对象中相同值的最小下标和

const list1 = ["Shogun", "Tapioca Express", "Burger King", "KFC"];
const list2 = ["KNN", "Burger King", "McDonalds", "Shogun", "Piatti"];

function calcLeastSubItem(list1, list2) {
  if (!Array.isArray(list1) || !Array.isArray(list2)) return [];

  let data = [];
  let subList = [];

  // 取出list1与list2具有相同值的下标值与值
  list1.forEach((item, index) => {

    list2.forEach((_item, _index) => {
      const i = index + _index;
      if (item === _item) {
        data.push({ index, total: i, value: _item });
        subList.push(i);
      }
    });

  });

  // 取出最小下标和的值
  let minVal = Math.min.apply(subList, subList);
  const result = [];
  data.forEach((item) => {
    if (item.total === minVal) {
      result.push(item.value);
    }
  });

  data = null;
  subList = null;
  minVal = null;

  return result;
}

const result = calcLeastSubItem(list1, list2);
console.log('输出结果:', result);

{}()[]匹配

给定一个字符串str,只会出现{}()[]6种字符,请实现一个函数isMatch(str)判断这个字符中的括号是否是匹配的?

Last Updated:
Contributors: 709992523, Eshen