程序员备忘录

我是一名独立 blogger,有一个维护了很久的博客:峰间的云,里面有技术内容,也有非技术的内容,加上博客天然的按时间倒排序的特点,导致技术文章的组织缺少条理性,不方便汇总和回顾。因此,有了当前这个以类似书本的方式按章节撰写的博客。我将这本“书”叫做《程序员备忘录》。这本书记录了WEB程序员常用的知识点,方便温故知新,自我成长,书里有很多是自己的学习笔记,也有不少是对网上优质内容的“拿来主义”。

你可以通过以下方式阅读本书:

意见与讨论请到这里提交:https://github.com/Yakima-Teng/memo/issues

保护你的眼睛

本书提供单页HTML版本:https://www.orzzone.com/memo/single。读者可以直接利用浏览器的打印功能打印成 PDF 电子书放到水墨屏电纸书阅读器中阅读。以减少对眼睛的伤害。

版权说明

参考的书籍/文章已标注在正文当中或列于书本末处,但可能少列了。若您发现文字和图片有侵犯到您的权益,请务必联系我。

本书中引用的他人文章版权归原作者/平台所有,本人自己写的部分版权归本人所有。

本书仅用于个人私下学习。谢绝商用。

联系方式可在作者个人主页中找到:峰间的云

目录


前端知识

因为本书的目标读者并非零基础,所以本章不会对基础语法、特性进行详细讲解。本章的目的主要是帮助大家迅速回忆基础知识,看是否有明显的缺漏需要补齐。对于本章,推荐的学习方式是:

  • 快速浏览。
  • 碰到某个关键词感觉比较模糊时,利用搜索引擎搜一下详细内容进行了解。

基础教程

网上有很多写得很好的教程:


JavaScript

JavaScript数据类型

  • 基本数据类型(primitive data type):共 7 个,分别是:Undefined、Null、Boolean、Number、String、Symbol、BigInt。
  • 对象数据类型

参考:BigInt

BigInt

bigint 是基础数据类型。通过在整数末尾添加 n,或者通过调用 BigInt(传入整数或字符串),可以创建一个 bigint 类型的值。

javascript
const previouslyMaxSafeInteger = 9007199254740991n;

// 十进制
const alsoHuge = BigInt(9007199254740991);
// 9007199254740991n

// 十进制
const hugeString = BigInt("9007199254740991");
// 9007199254740991n

// 十六进制
const hugeHex = BigInt("0x1fffffffffffff");
// 9007199254740991n

// 八进制
const hugeOctal = BigInt("0o377777777777777777");
// 9007199254740991n

// 二进制
const hugeBin = BigInt(
  "0b11111111111111111111111111111111111111111111111111111",
);
// 9007199254740991n

注意:

  • 因为类型不同,10n === 10的值为 false10n == 10的值为 true)。
  • bigint 值不支持用 Math 对象的原生方法进行处理。
  • typeof 1n === "bigint";true
  • typeof Object(1n) === "object";true

callapplybind

callfunc.call(obj, param1, param2)func函数应用于obj对象上,此时func函数内部的this指向obj对象。

apply:与call类似,只是所有要传入的数据都是以数组的形式放到第二个参数里的,如func.apply(obj, [arg1, arg2])。一个经典用法是来求数组中的最大数:Math.max.apply(null, [1, 3, 5]),另一个经典用法是用数组方法去处理非数组对象:[].slice.call(arguments, 1)

bind:与call类似,但不会立即执行,而是生成了一个新函数,新函数的this指向的是我们传入的obj

关于第一个参数

callapplybind的第一个参数,如果传了 null 或者 undefined 会被替换为全局对象(浏览器环境下的话就是 window 对象),如果传的是其他基础类型(比如1'a'false 等)则会被转换成基础类型对应的对象。

javascript
function test (...args) {
    console.log(this)
    console.log(args)
}

console.log('call')
test.call('a', 1, true, 'example', [], { a: 1 })

console.log('apply')
test.apply(false, [1, true, 'example', [], { a: 1 }])

console.log('bind')
/**
 * 注意这里我们直接对 `bind` 生成的函数进行调用
 * 并且在这个调用中也传入了几个新参数
 */
test.bind(0, 1, true, 'example', [], { a: 1 })(
    'hello', 'world'
)

执行结果见下图:

实现 bind

我们自己实现一个,注意:

  • 调用 bind 后生成的是一个新函数(新函数名为 newFn)。
  • 调用 bind 时传的第二个及后续参数会和调用新函数 newFn 时传入的参数合并作为入参。
  • 调用 bind 时会指定 newFn 中的 this 指向。
javascript
Function.prototype.myBind = function () {
    // 这里的 `this` 就是被 `bind` 处理前的函数
    const func = this

    let obj = arguments[0]
    // 如果 `obj` 是 `undefined` 或 `null`,则替换成全局对象
    if (typeof obj === 'undefined' || obj === null) {
        obj = globalThis
    }
    // 如果不是对象类型,比如是 `false`、`1` 等,则包装成对象
    if (typeof obj !== 'object') {
        obj = new Object(obj)
    }

    let args = [].slice.call(arguments, 1)
    return function () {
        args = args.concat(Array.from(arguments))
        const uniqueKey = Symbol('避免污染对象上的其他属性')
        /**
         * 使用 `symbol`作为 key,
         * 可以避免影响其他人添加的可能同名的 key
         */
        obj[uniqueKey] = func
        const result = obj[uniqueKey](...args)
        // 删除临时添加的属性,避免污染对象
        delete obj[uniqueKey]
        return result
    }
}

function test (...args) {
    console.log(this)
    console.log(args)
    console.log(arguments)
}
test.bind(0, 1, true, 'example', [], { a: 1 })(
    'hello', 'world'
)
test.myBind(0, 1, true, 'example', [], { a: 1 })(
    'hello', 'world'
)

实现结果如下:

forEachfor-offor-in 循环

javascript
// `forEach` 循环无法通过 `break` 或 `return` 语句进行中断
arr.forEach(function (elem) {
    console.log(elem)
})

/**
 * for-in循环实际上是为循环对象的可枚举(enumerable)属性而设计的,
 * 也能循环数组,不过不建议,因为key变成了数字
 */
const obj = { a: 1, b: 2, c: 3 }
for (const p in obj) {
    console.log(`obj.${p} = ${obj[p]}`)
}
// 上面的代码依次输出内容如下:
// obj.a = 1
// obj.b = 2
// obj.c = 3

/**
 * for-of能循环很多东西,
 * 包括字符串、数组、map、set、DOM collection等等
 * (但是不能遍历对象,因为对象不是iterable可迭代的)
 */
const iterable = [1, 2, 3]
for (const value of iterable) {
    console.log(value)
}

基本上 for in 用于大部分常见的由 key-value 对构成的对象上以遍历对象内容。但是 for in 在遍历数组对象时并不方便,这时候用 for of 会很方便。

IIFE(Immediately-Invoked Function Expression)与分号

如果习惯写完一条语句后不加分号的写法,碰到需要写 IIFE(自执行函数)的时候容易踩到下面的坑:

javascript
const a = 1
(function () {})()

上述代码会报错,因为第一行的 1 会和第二行一起被程序解析成 const a = 1(function () {})(),然后报错:Uncaught TypeError: 1 is not a function

这时候可以这样写:

javascript
const a = 1
void function () {}()

// 或
const a = 1
void (function () {})()

// 或者下面这种方式,但据说会多一次逻辑运算
const a = 1
!function () {}()

JS中除了使用 new 关键字还有什么方法可以创建对象?

可以通过 Object.create(proto, [, propertiesObject]) 实现。详见:Object.create()

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象。

javascript
const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(
        `I am ${this.name}. Am I human? ${this.isHuman}`
    );
  },
};

const me = Object.create(person);

// `name` 是 `me` 的属性,不是 `person` 的属性
me.name = 'Matthew';
// 继承过来的属性值可以被重写
me.isHuman = true;

// 打印内容: "I am Matthew. Am I human? true"
me.printIntroduction();

Object.create() 实现类式继承

javascript
// Shape —— 父类
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类方法
Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info("Shape moved.");
};

// Rectangle —— 子类
function Rectangle() {
  Shape.call(this); // 调用父类构造函数。
}

// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype, {
    /**
     * 如果不将 `Rectangle.prototype.constructor` 
     * 设置为 `Rectangle`,
     * 它将采用 `Shape`(父类)的 `prototype.constructor`。
     * 为避免这种情况,
     * 我们将 `prototype.constructor` 设置为 `Rectangle`(子类)。
     */
    constructor: {
        value: Rectangle,
        enumerable: false,
        writable: true,
        configurable: true,
    },
});

const rect = new Rectangle();

// true
console.log(
    "rect 是 Rectangle 类的实例吗?",
    rect instanceof Rectangle
);

// true
console.log(
    "rect 是 Shape 类的实例吗?",
    rect instanceof Shape
);

// 打印 'Shape moved.'
rect.move(1, 1);

使用 Object.create()propertyObject 参数

Object.create() 方法允许对对象创建过程进行精细的控制。实际上,字面量初始化对象语法可视为 Object.create() 的一种语法糖。使用 Object.create(),我们可以创建具有指定原型和某些属性的对象。请注意,第二个参数将键映射到属性描述符,这意味着你还可以控制每个属性的可枚举性、可配置性等,而这在 {} 字面量初始化对象语法中是做不到的。

javascript
o = {};
// 等价于:
o = Object.create(Object.prototype);

o = Object.create(Object.prototype, {
  // foo 是一个常规数据属性
  foo: {
    writable: true,
    configurable: true,
    value: "hello",
  },
  // bar 是一个访问器属性
  bar: {
    configurable: false,
    get() {
      return 10;
    },
    set(value) {
      console.log("Setting `o.bar` to", value);
    },
  },
});

/**
 * 创建一个新对象
 * 它的原型是一个新的空对象
 * 并添加一个名为 'p',值为 42 的属性。
 */
o = Object.create({}, { p: { value: 42 } });

使用 Object.create(),我们可以创建一个原型为 null 的对象。在字面量初始化对象语法中,相当于将 __proto__ 键赋值为 null

javascript
o = Object.create(null);
// 等价于:
o = { __proto__: null };

你可以使用 Object.create() 来模仿 new 运算符的行为。

javascript
function Constructor() {}
o = new Constructor();
// 等价于:
o = Object.create(Constructor.prototype);

当然,如果 Constructor 函数中有实际的初始化代码,那么 Object.create() 方法就无法模仿它。

生成器函数与 yield 语句

javascript
function* hello (name) {
    yield `hello ${name}!`
    yield 'I am glad to meet you!'
    if (0.6 > 0.5) {
        yield `It is a good day!`
    }
    yield 'See you later!'
}

// Generator函数执行后会返回一个迭代器,通过调用next方法依次yield相应的值
const iterator = hello('Yakima')

iterator.next() // 返回{value: "hello Yakima!", done: false}
iterator.next() // 返回{value: "I am glad to meet you!", done: false}
iterator.next() // 返回{value: "It is a good day!", done: false}
iterator.next() // 返回{value: "See you later!", done: false}
iterator.next() // 返回{value: undefined, done: true}
iterator.next() // 返回{value: undefined, done: true}

生成器函数(Generator)与常见的函数的差异:

  • 通常的函数以 function 开始,而生成器函数以 function* 开始;
  • 在生成器函数内部,yield 是一个关键字,和 return 有点像。不同点在于,所有函数(包括生成器函数)都只能 return 一次,而在生成器函数中可以 yield 任意次。yield 表达式暂停了生成器函数的执行,然后可以从暂停的地方恢复执行。

常见的函数不能暂停执行,而生成器函数可以,这是两者最大的区别。

扩展运算符进行对象拷贝时是浅拷贝

Babeljs.io Try it out上转义的结果是:

javascript
function _typeof(o) {
    "@babel/helpers - typeof";
    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
        }
        : function (o) {
            return (
                o
                && "function" == typeof Symbol
                && o.constructor === Symbol
                && o !== Symbol.prototype
            )
                ? "symbol"
                : typeof o;
        },
        _typeof(o);
}

function ownKeys(e, r) {
    var t = Object.keys(e);
    if (Object.getOwnPropertySymbols) {
        var o = Object.getOwnPropertySymbols(e);
        r && (o = o.filter(function (r) {
            return Object.getOwnPropertyDescriptor(e, r).enumerable;
        })), t.push.apply(t, o);
    }
    return t;
}

function _objectSpread(e) {
    for (var r = 1; r < arguments.length; r++) {
        var t = null != arguments[r] ? arguments[r] : {};
        r % 2
            ? ownKeys(Object(t), !0).forEach(function (r) {
                _defineProperty(e, r, t[r]);
            })
            : Object.getOwnPropertyDescriptors
                ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
                : ownKeys(Object(t)).forEach(function (r) {
                    Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
                });
    }
    return e;
}

function _defineProperty(obj, key, value) {
    key = _toPropertyKey(key);
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

function _toPropertyKey(t) {
    var i = _toPrimitive(t, "string");
    return "symbol" == _typeof(i) ? i : String(i);
}

function _toPrimitive(t, r) {
    if ("object" != _typeof(t) || !t) return t;
    var e = t[Symbol.toPrimitive];
    if (void 0 !== e) {
        var i = e.call(t, r || "default");
        if ("object" != _typeof(i)) return i;
        throw new TypeError("@@toPrimitive must return a primitive value.");
    }
    return ("string" === r ? String : Number)(t);
}
var a = {
  a: 1,
  b: {
    c: 3
  },
  c: 5
};
var d = _objectSpread({}, a);

常用的异步处理方法

回调函数、事件监听、发布/订阅、Promise对象。

require, import

本文参考了以下文章:

一段时间以来,CommonJS 模块化方案一直是 Node.js 生态中的默认模块化方案。从 Node.js v8.5.0 开始,引入了 ES(ECMAScript) 模块化方案。这两种方案在执行时有一些差异。

  • ES 模块化方案是 ECMAScript 语言的官方正式模块化方案,也是大多数浏览器原生支持的方案。使用 importexport 来导入、导出模块。
  • Node.js 默认采用 CommonJS 模块化方案。使用 requiremodule.exports / exports.<keyName> 来导入和导出模块。
  • require() 函数可以在程序的任何地方被调用,import 则在文件头部被调用。
  • 一般用 require() 引入的文件名使用 .js 作为文件名后缀,用 import 引入的文件名使用 .mjs 作为文件名后缀。(不绝对)
  • require() 得到的内容可以视作一个对象,里面有我们需要的属性或者方法。
  • require() 得到的是原始内容的一个拷贝(如果是对象的话就是浅拷贝),也就是重新又自己声明了一份变量,比如在 b.js 文件中声明了 const { a } = require(./a.js) 后在 a.js 文件中修改 a 的值也不会影响 b.js 中的 a 的值,两个文件中的 a 是不一样的。
  • import 则不会重新声明变量,在上面所述的场景中,a.jsb.js 文件中的 a 一直都是同一个变量,值也始终相同。

require 引入 外部模块

require 除了支持通过传入一个本地文件路径来引用本地模块,也支持通过传入一个 web 地址来引入外部模块,比如这样:

javascript
const myVar = require('http://web-module.location');

JS 中的 new

直接使用 {} 花括号可以很方便地创建一个对象,但是当我们想要创建很多对象时,如果还采用直接使用 {} 的方式就需要写很多冗余代码。JavaScript 提供了 new 关键字,我们可以对构造函数使用 new 操作符来创建一类相似的对象。

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  • 它们的命名通常以大写字母开头(语法上并未限制,但这是普遍的共识、约定)。
  • 它们只能由 new 操作符来执行(如果直接调用,这时它就不是构造函数了,而是常规函数)。
javascript
function User(name) {
    this.name = name;
    this.isAdmin = false;
}

const user = new User("Jack");

console.log(user.name); // Jack
console.log(user.isAdmin); // false

当一个函数被使用 new 操作符执行时,它会经历以下步骤:

  • 一个新的空对象被创建并赋值给 this
  • 函数体执行。通常它会修改 this 对象,比如为其添加新的属性。
  • 返回 this 对象。

换句话说,执行 new User(...) 时,做的就是类似下面的事情:

javascript
function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

const user = new User("Jack") 可等价为以下代码:

javascript
const user = {
  name: "Jack",
  isAdmin: false
};

现在,如果我们想创建其他用户,我们可以调用 new User("Ann")new User("Alice") 等。代码量比每次都使用 {} 字面量的方式去创建要少,而且更易阅读。

这就是构造器的主要目的 —— 实现可重用的对象创建代码。

提示

从技术上讲,任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器。即可以通过 new 来运行。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new 来运行。

new.target

在一个函数内部,我们可以使用 new.target 属性来检查它是否被使用 new 关键字进行调用了。

常规调用时,它为 undefined。使用 new 调用时,则等于该函数:

javascript
function User() {
    console.log(new.target);
    console.log(new.target === User);
}

// 直接调用(不使用 `new` 关键字):
User(); // undefined, false

// 使用 `new` 关键字调用
new User(); // function User { ... }, true

我们也可以让常规调用和使用 new 关键字调用做相同的工作,像这样:

javascript
function User(name) {
    if (!new.target) {
        return new User(name);
    }

    this.name = name;
}

const john = User("John");
console.log(john.name);

这种方法有时被用在库中以使语法更加灵活。这样其他人在调用函数时,无论是否使用了 new,程序都能如期工作。

不过,到处都使用它并不是一件好事,因为省略了 new 使得很难直观地知道代码在干啥。而通过使用 new 关键字,我们都知道代码正在创建一个新对象。

构造函数的 return

通常,构造器函数内没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动返回 this

但是,如果构造器函数内有 return 语句,那么:

  • 如果 return 返回的是一个对象(不含 null),则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型(包括 null),则忽略 return 语句,继续返回 this

换句话说,带有对象的 return 语句返回该对象,其他情况下都返回 this

例如,这里 return 通过返回一个对象覆盖了 this

javascript
function BigUser() {
    this.name = "小明";
    return { name: "小王" }; 
}

console.log(new BigUser().name);  // 小王

这里有一个 returnundefined 的例子(或者我们可以在它之后放置一个原始类型,结果是一样的):

javascript
function SmallUser() {
    this.name = "小小王";
    return;
}

console.log(new SmallUser().name);  // 小小王

通常构造器函数里都是没有 return 语句的,这里只做了解即可。

省略括号

顺便说一下,如果没有参数,我们可以省略 new 后的括号:

javascript
const user = new User;
// 等同于
const user = new User();

这里省略括号不是一种好风格,但是语法规范上是允许的。

构造器中的方法

使用构造函数来创建对象有很大的灵活性。构造函数可能有一些函数入参,这些参数定义了如何构造对象。

当然,我们不仅可以在 this 上添加属性,还可以添加方法。

javascript
function User(name) {
    this.name = name;

    this.sayHi = function () {
        console.log(`我的名字是: ${this.name}`);
    };
}

const user = new User("李白");

user.sayHi(); // 我的名字是李白

手写一个 new

问题描述

由于 new 是 JavaScript 中的关键字,我们不可能另外实现一个自己的关键字。所以这里要求我们实现一个函数 myNew,要求:myNew(Person, '李白') 等价于 new Person('李白')

具体实现

javascript
function myNew() {
    // 1、创建一个空对象
    const obj = {}
    // 获取构造方法(并将其从 `arguments` 中移出)
    const constructor = [].shift.call(arguments)

    // 2、将新对象的原型指向构造方法的 `prototype` 对象上
    obj.__proto__ = constructor.prototype

    /**
     * 3、获取到构造方法的返回值
     * 如果原先构造方法有返回值,且是对象,
     * 那么原始的 `new` 会把这个对象返回出去
     * (基本类型会忽略)
     * 
     * 原始 `arguments` 的第一个参数已经在前面被 `shift` 了,
     * 所以剩下的参数全都是构造方法需要的值
     */
    const ret = constructor.apply(obj, arguments)

    // `(ret || obj)` 是为了判断 `null`,当为 `null` 时,也返回新对象
    return typeof ret === 'object' ? (ret || obj) : obj
}

使用

javascript
function Person(name, age) {
    this.name = name
    this.age = age
}

const p = myNew(Person, 'cheny', 28)
// true
console.log(p instanceof Person);

JavaScript 中的 this

普通函数的 this 指向

这里说的“普通函数”指箭头函数以外的函数。

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。

写出下面代码的执行结果:

javascript
// 当前位于全局作用域下
function testObject () {
  alert(this)
}

testObject()

上题的答案:在chrome中会弹出 [object Window]

this 关键字是函数运行时自动生成的一个内部独享对象,只能在函数内部使用,总指向调用它的对象。

根据不同的使用场合,this 有不同的值,主要分以下几种情况:

  • 默认绑定。
  • 隐式绑定。
  • new 绑定。
  • 显示绑定。

默认绑定

全局环境中定义 person 函数,内部使用 this 关键字。

javascript
var name = 'Jenny';
function person() {
    return this.name;
}

// Jenny
console.log(person());

上述代码输出 Jenny,原因是调用函数的对象在游览器中为 window,因此 this 指向 window,所以输出 Jenny

注意:

严格模式下,不能将全局对象用于默认绑定,this 会绑定到 undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。

隐式绑定

函数还可以作为某个对象的方法调用,这时 this 就指这个上级对象。

javascript
function test() {
  console.log(this.x);
}

const obj = {};
obj.x = 1;
obj.m = test;

obj.m(); // 1

下面这段代码中包含多级对象,注意 this 指向的只是它上一级的对象 b (由于 b 内部并没有属性 a 的定义,所以输出 undefined。)。

javascript
const o = {
    a: 10,
    b: {
        fn: function() {
            console.log(this.a); // undefined
        }
    }
}
o.b.fn();

再举一种特殊情况:

javascript
const o = {
    a: 10,
    b: {
        a: 12,
        fn: function() {
            console.log(this.a); // undefined
            console.log(this); // window
        }
    }
}
const j = o.b.fn;
j();

在上面的例子中,this 指向的是 window,这里的大家需要记住,this 永远指向的是最后调用它的对象,虽然fn 是对象 b 的方法,但是 fn 赋值给 j 时并没有执行,所以最终指向 window

new 绑定

通过构建函数 new 关键字生成一个实例对象时,this 指向这个实例对象。

javascript
function Test() {
 this.x = 1;
}

const obj = new Test();
obj.x // 1

上述代码之所以会输出 1,是因为 new 关键字改变了 this 的指向。

这里再列举一些特殊情况:

new 过程遇到 return 一个对象(不包括 null),此时 this 指向返回的对象:

javascript
function fn() {
    this.user = 'xxx';  
    return {};  
}
const a = new fn();
console.log(a.user); // undefined

如果 return 一个基础类型的值(包括 null),则 this 指向实例对象:

javascript
function fn() {
    this.user = 'xxx';  
    return 1;
}
const a = new fn;
console.log(a.user); // xxx

注意的是 null 虽然也是对象,但是此时 this 仍然指向实例对象:

javascript
function fn() {
    this.user = 'xxx';  
    return null;
}
const a = new fn;
console.log(a.user); // xxx

显示修改

applycallbind是函数的几个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时 this 指的就是这第一个参数。

javascript
const x = 0;
function test() {
 console.log(this.x);
}

const obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1

匿名函数的 this

匿名函数里的 this 指向 window

javascript
// 等价于 `window.name = 'The Window'`
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function() {
        return function() {
        return this.name;
     };
   }
};
// 输出为 `The Window`
alert(object.getNameFunc()());

箭头函数的 this 指向

大部分情况下,this 总是指向调用该函数的对象。但对箭头函数而言却不是这样的,箭头函数里的 this 是根据外层(函数或者全局)作用域(词法作用域)来决定的。

箭头函数体内的 this 对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。

下面是普通函数的列子:

javascript
// 其实是window.name = 'window'
var name = 'window';

var A = {
   name: 'A',
   sayHello: function() {
      console.log(this.name)
   }
}

// 输出 `A`
A.sayHello();

var B = {
  name: 'B'
}

// 输出 `B`
A.sayHello.call(B);

/**
 * 不传参数指向全局 `window` 对象,
 * 输出 `window.name` 也就是 `'window'`
 */
A.sayHello.call();

从上面可以看到,sayHello 这个方法是定义在 A 对象中的,但是当我们使用 call 方法,把其指向 B 对象后,最后输出了 B;可以得出,sayHellothis 只跟使用时的调用对象有关。

改造一下:

javascript
var name = 'window'; 

var A = {
   name: 'A',
   sayHello: () => {
      console.log(this.name)
   }
}

// 还是以为输出 `A`? 错啦,其实输出的是 `window`
A.sayHello();

我相信在这里,大部分同学都会出错,以为 sayHello 是绑定在 A 上的,但其实它绑定在 window 上的,那到底是为什么呢?

一开始,我重点标注了该函数所在的作用域指向的对象,作用域是指函数内部,这里的箭头函数(也就是 sayHello)所在的作用域其实是最外层的 js 环境,因为没有其他函数包裹;然后最外层的 js 环境指向的对象是 window 对象,所以这里的 this 指向的是 window 对象。

那如何改造成永远绑定A呢:

javascript
var name = 'window';

var A = {
    name: 'A',
    sayHello: function() {
        var s = () => console.log(this.name)
        // 返回箭头函数 `s`
        return s
    }
}

var sayHello = A.sayHello();
// 输出 `A` 
sayHello();

var B = {
    name: 'B'
}

// 输出 `A` 
sayHello.call(B);
// 输出 `A` 
sayHello.call();

OK,这样就做到了永远指向 A 对象了,我们再根据该函数所在的作用域指向的对象来分析一下:

  • 该函数所在的作用域:箭头函数 s 所在的作用域是 sayHello,因为 sayHello 是一个函数。
  • 作用域指向的对象:A.sayHello 指向的对象是 A

最后是使用箭头函数其他几点需要注意的地方

  • 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。可以用 ...rest 入参代替((...rest) => { console.log(rest); })。
  • 不可以使用 yield 命令,因此箭头函数不能用作 生成器(Generator)函数。

再来看一个例子:

javascript
var fullname = 'a'
var obj = {
    fullname: 'b',
    prop: {
        fullname: 'c',
        getFullname: () => {
            return this.fullname;
        }
    }
}
// 打印 'a'
console.log(obj.prop.getFullname())
var test = obj.prop.getFullname
// 打印 'a'
console.log(test())
javascript
var fullname = 'a'
function hello () {
    var fullname = 'd'
    var obj = {
        fullname: 'b',
        prop: {
            fullname: 'c',
            getFullname: () => {
                return this.fullname;
            }
        }
    }
    // 打印 'a'
    console.log(obj.prop.getFullname())
    var test = obj.prop.getFullname
    // 打印 'a'
    console.log(test())
}
hello()
javascript
var fullname = 'a'
function hello () {
    var fullname = 'd'
    var obj = {
        fullname: 'b',
        prop: {
            fullname: 'c',
            getFullname: () => {
                return this.fullname;
            }
        }
    }
    // 打印 undefined
    console.log(obj.prop.getFullname())
    var test = obj.prop.getFullname
    // 打印 undefined
    console.log(test())
}
const d = new hello()
javascript
var fullname = 'a'
function hello () {
    this.fullname = 'f'
    var fullname = 'd'
    var obj = {
        fullname: 'b',
        prop: {
            fullname: 'c',
            getFullname: () => {
                return this.fullname;
            }
        }
    }
    // 打印 'f'
    console.log(obj.prop.getFullname())
    var test = obj.prop.getFullname
    // 打印 'f'
    console.log(test())
}
const d = new hello()

看一个普通函数的例子:

javascript
const obj = {
    count: 10,
    doSomethingLater() {
        setTimeout(function () {
            // 这是一个匿名函数,是在 window 作用域下执行的
            this.count++;
            console.log(this.count);
        }, 300);
    },
};

// 打印 `NaN`,因为 window 对象上没有 `count` 属性
obj.doSomethingLater();

如果改成箭头函数:

javascript
const obj = {
    count: 10,
    doSomethingLater() {
        // 该方法将 `this` 绑定到 `obj` 上下文中
        setTimeout(() => {
            /**
             * 由于箭头函数内部不会自己绑定 `this`,
             * `setTimeout` 函数也没有创建 `this` 绑定,
             * 所以外部的 `obj` 上下文会被用作 `this`
             */
            this.count++;
            console.log(this.count);
        }, 300);
    },
};

// 打印 `11`
obj.doSomethingLater();
  • 参考:

JavaScript 中常用函数的实现

判断 JavaScript 全局变量是否存在

javascript
if (typeof localStorage !== 'undefined') {
  // 此时访问localStorage不会出现引用错误
}

或者

javascript
// 浏览器端全局处 `window`/`this`/`self` 三者彼此全等
if ('localStorage' in self) {
  // 此时访问 `localStorage` 绝对不会出现引用错误
}

注意二者的区别:

javascript
var a // 或 var a = undefined
'a' in self // true
typeof a // 'undefined'
  • 在全局作用域下定义 var a = undefined 或者 var a 相当于是给 window 对象添加了 a 属性,但是未赋值,即 window.a === undefinedtrue
  • typeof a 就是返回其变量类型,未赋值或者声明类型为 undefined 的变量,其类型就是 undefined

constletvar

全局作用域下通过 constlet 定义一个变量时,并不会在 window 上挂载该对象,这是与 var 表现不同之处。

判断 2 个对象是否相等(不是相同

前提假设

不是只根据引用地址来判断,只要两个对象的键完全一致且同名键对应的值也“相等”,就认为这两个对象是相等的,比如分开创建的 { a: 1 }{ a: 1 },被认为是相等的两个对象。

具体实现

javascript
function isObjectEqual (obj1, obj2) {
    if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
        return obj1 === obj2
    }

    // 如果两个对象指向的是同一个引用地址,则为相同对象
    if (obj1 === obj2) {
        return true
    }

    const keys1 = Object.keys(obj1)
    const keys2 = Object.keys(obj2)

    if (keys1.length !== keys2.length) {
        return false
    }

    if (keys1.length === 0) {
        return true
    }

    for (let i = 0, len = keys1.length; i < len; i++) {
        const key = keys1[i]
        if (!isObjectEqual(obj1[key], obj2[key])) {
            return false
        }
    }

    return true
}

实现 assign

javascript
function assign () {
    const args = Array.from(arguments)
    const target = args.shift()

    if (!target || typeof target !== 'object') {
        throw new TypeError('入参错误')
    }

    const objects = args.filter((obj) => typeof obj === 'object')

    objects.forEach((obj) => {
        for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
                target[key] = obj[key]
            }
        }
    })

    return target
}

merge 合并 2 个对象的可枚举属性

合并对象的可枚举的属性/方法到指定对象

javascript
/**
 * 判断是否是非空对象
 * @param val {any}
 * @returns {boolean}
 */
function isObject(val) {
    return (
        typeof val !== 'null' &&
        ({}).toString.call(val).slice(8, -1).toLowerCase() === 'object'
    )
}

function merge(target, obj) {
    for (const p in obj) {
        if (!obj.hasOwnProperty(p)) {
            return
        }
        if (isObject(target[p]) && isObject(obj[p])) {
            merge(target[p], obj[p])
            return
        }
        target[p] = obj[p]
    }
    return target
}

快速填充数字数组

假设你需要一个数组,数据长度为100,数组元素依次为0、1、2、3、4...98、99。该如何实现呢?直接写个for循环当然是可以的。不过这里有更方便的方法。

javascript
Array.from(Array(100).keys())

或者

javascript
[...Array(100).keys()]

如果你想要直接从 1 开始到 100,可以用 Array.from 方法实现(下面这种传参方法不太常见,第二个参数是一个 map function,可以对第一个参数传进去的类数组对象或者可迭代对象进行处理):

Array.from的语法如下:

javascript
Array.from(arrayLike)
Array.from(arrayLike, mapFn)
Array.from(arrayLike, mapFn, thisArg)

所以,可以这么写:

javascript
Array.from({ length: 100 }, (_, i) => i + 1)

注意,上面的例子里可以认为 { length: 100 } 是一个类数组

[原创]不使用内置函数处理时间戳

要求

实现一个函数,该函数入参为一个时间戳,返回 YYYY:MM:DD HH:mm:ss 格式的字符串。不允许使用 Date 对象的内置方法。

已知:

  • 时间戳是指格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数。
  • 普通闰年:公历年份是 4 的倍数,且不是 100 的倍数的,为闰年(如 2004 年、2020 年等就是闰年)。
  • 世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年(如 1900 年不是闰年,2000 年是闰年)。
  • 1、3、5、7、8、10、12 月每月 31 天。
  • 4、6、9、11 月每月 30 天。

实现

javascript
function toDateStr (ts) {
    // 1 天的毫秒数
    const tsDay = 24 * 60 * 60 * 1000
    // 1 小时的毫秒数
    const tsHour = 60 * 60 * 1000
    // 1 分钟的毫秒数
    const tsMin = 60 * 1000
    // 1 秒的毫秒数
    const tsSecond = 1000

    let remaining = ts
    // 天数
    const days = Math.floor(remaining / tsDay)
    remaining -= days * tsDay

    // 小时数
    const hours = Math.floor(remaining / tsHour)
    remaining -= hours * tsHour

    // 分钟数
    const mins = Math.floor(remaining / tsMin)
    remaining -= mins * tsMin

    // 秒数
    const seconds = Math.floor(remaining / tsSecond)

    // 将天数转换成年和月
    let years = 1970
    let months = 0
    let daysInLastMonth = 0
    
    // 统计过的天数
    let numOfDays = 0
    while (numOfDays < days) {
        // 2 月份
        let daysInFebruary = 28
        if (years % 400 === 0) {
            daysInFebruary = 29
        }
        if (years % 100 !== 0 && years % 4 === 0) {
            daysInFebruary = 29
        }

        // 各个月份的天数
        const arrMonthAndDays = [
            31, daysInFebruary, 31, 30, 31,
            30, 31, 31, 30, 31,
            30, 31,
        ]
        for (const daysInMonth of arrMonthAndDays) {
            /**
             * 注意这里是小于,不是小于等于,
             * 因为是从 1970 年 1 月【1 日】开始计算的,
             * 过 30 天就是 1 月 31 日,过 31 天已经是 2 月份了
             */
            if (days - numOfDays < daysInMonth) {
                daysInLastMonth = days - numOfDays
                numOfDays += days - numOfDays
                break
            }
            months++
            numOfDays += daysInMonth
        }
        
        if (numOfDays < days) {
            // 下一个年份
            years++
            months = 0
        }
    }

    /**
     * 因为时间戳是从 1970 年 1 月 1 日开始计时的,
     * 所以误差 1 天实际对应的是 2 号,
     * 所以我们最终算出来的误差天数需要加 1
     */
    return [
        `${years}:${months + 1}:${daysInLastMonth + 1}`,
        `${hours}:${mins}:${seconds}`
    ].join( )
}

[原创]三行代码实现函数柯里化

实现一个函数,用于将目标函数柯里化

柯里化之前的效果:

javascript
// 柯里化之前
function add(a, b, c, d, e) {
    console.log(a + b + c + d + e)
}
add(1, 2, 3, 4, 5)

实现一个柯里化函数 curry,使得下面 curryAdd(1)(2)(3)(4)(5) 的计算结果与上方 add(1, 2, 3, 4, 5) 一致:

javascript
function curry() {}
const curryAdd = curry(add)
console.log(curryAdd(1)(2)(3)(4)(5)) // 输出:15

实现方案

判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) :

  • 如果是,则执行当前函数;
  • 如果否,则返回一个新函数,用于继续接收更多的参数。

注意,这里我们的原函数是指如下这个函数(fn.length为 5,因为有 abcde 一共 5 个参数):

javascript
function add(a, b, c, d, e) {
    console.log(a + b + c + d + e)
}

实现方案如下:

javascript
function curry(fn, ...args) {
    // 函数的参数个数可以直接通过函数的 `.length` 属性来访问
    return args.length === fn.length
        ? fn(...args)
        : (...newArgs) => curry(fn, ...args, ...newArgs)
}

// 使用
function add(a, b, c, d, e) {
    console.log(a + b + c + d + e)
}
const curryAdd = curry(add)
console.log(curryAdd(1)(2)(3)(4)(5)) // 输出:15

Closure 闭包

闭包(closure)指有权访问另一个函数作用域中变量的函数。

闭包的作用:

  • 延伸变量作用域范围,读取函数内部的变量。
  • 让这些变量的值始终保持在内存中。

闭包案例1

javascript
function fn1() {
    var num = 10;
    function fn2() {
        console.log(num);
    }
    fn2();
}

//输出结果:10
fn1();

fn2 的作用域当中访问到了 fn1 函数中的 num 这个局部变量。

闭包案例2

另一个例子:

javascript
function fn() {
    var num = 10;
    return function() {
        console.log(num);
    }
}
var f = fn();
// 上面这步类似于
// var f = function() {
//     console.log(num);
// }

//输出结果:10
f();

在上例中我们做到了在 fn() 函数外面访问 fn() 中的局部变量 num。闭包延伸了变量作用域范围,读取了函数内部的变量。

闭包案例3

javascript
const fn = function() {
    let sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}

/**
 * `fn()` 进行 `sum` 变量申明并且返回一个匿名函数,
 * 第二个 `()` 意思是执行这个匿名函数
 */
fn()() // 1
fn()() // 1

我这里直接简单解释一下,执行 fn()() 后,fn()() 已经执行完毕,没有其他资源在引用 fn,此时内存回收机制会认为 fn 不需要了,就会在内存中释放它。

那如何不被回收呢?

javascript
const fn = function() {
    let sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn1 = fn()
// 1
fn1()
// 2
fn1()
// 3
fn1()

这种情况下,fn1 一直在引用 fn(),此时内存就不会被释放,就能实现值的累加。那么问题又来了,这样的函数如果太多,就会造成内存泄漏。

内存泄漏了怎么办呢?我们可以手动释放一下。

javascript
const fn = function() {
    let sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn1 = fn()

// 1
fn1()
// 2
fn1()
// 3
fn1()

// `fn1` 的引用 `fn` 被手动释放了
fn1 = null
// `num` 再次归零
fn1 = fn()
// 1
fn1()

事件循环

微任务和宏任务

除了广义的同步任务和异步任务,我们可以分的更加精细一点:

  • macro task(宏任务):包括整体代码脚本、setTimeoutsetInterval
  • micro task(微任务):Promiseprocess.nextTickMutationObserver

不同类型的任务会进入到不同的 事件队列(event queue)。相同类型的任务会进入相同的事件队列。比如 setTimeoutsetInterval 会进入相同的事件队列。

事件循环,宏任务,微任务的关系图

先看一个例子:

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

new Promise(function(resolve) {
    console.log('promise');
    resolve(true)
    console.log('after resolve')
}).then(function() {
    console.log('then');
})

console.log('console');

// promise
// after resolve
// console
// then
// setTimeout
  1. 首先会遇到 setTimeout,将其放到宏任务 event queue 里面。
  2. 然后回到 promise,new Promise 会立即执行,then 会分发到微任务。
  3. 遇到 console 立即执行。
  4. 整体宏任务执行完成,接下来判断是否有微任务,刚刚放到微任务里面的 then,执行。
  5. 至此第一轮事件结束,进行第二轮,刚刚我们放在 event queue 的 setTimeout 函数内的语句进入到宏任务,立即执行。
  6. 结束。

为何要区分微任务和宏任务

区分微任务和宏任务是为了将异步队列任务划分优先级,通俗的理解就是为了插队。

一个事件队列(Event Loop)中,microtask 是在 macrotask 之后被调用的,microtask 会在下一个 Event Loop 之前执行完,并且会将 microtask 执行当中新注册的 microtask 一并调用执行完,然后才开始下一次 Event Loop,所以如果有新的 Macrotask 就需要一直等待,等到上一个 Event Loop 当中 Microtask 被清空为止。由此可见,我们可以在下一次 Event Loop 之前进行插队。

如果不区分 Microtask 和 Macrotask,那就无法在下一次 Event Loop 之前进行插队,其中新注册的任务得等到下一个 Macrotask 完成之后才能进行,这中间可能你需要的状态就无法在下一个 Macrotask 前得到同步。

一个利用任务执行顺序(宏任务 => 微任务 => 浏览器渲染)的案例:

html
<html>
<head>
    <style>
        div {
            padding: 0;
            margin: 0;
            display: inline-block;
            widtH: 100px;
            height: 100px;
            background: blue;
        }
        #microTaskDom {
            margin-left: 10px;
        }
    </style>
</head>
<body>
<div id="taskDom"></div>
<div id="microTaskDom"></div>
<script>
    window.onload = () => {
        setTimeout(() => {
            taskDom.style.background = 'red'
            setTimeout(() => {
                /**
                 * 使用 `setTimeout` 立马修改背景色,
                 * 会闪现一次红色背景
                 */
                taskDom.style.background = 'black'
            }, 0);

            microTaskDom.style.background = 'red'
            Promise.resolve().then(() => {
                /**
                 * 使用 `Promise` 不会闪现红色背景。
                 * 因为微任务会在渲染之前完成对背景色的修改,
                 * 等到渲染时就只需要渲染黑色
                 */
                microTaskDom.style.background = 'black'
            })
        }, 1000);
    }
</script>
</body>
</html>

由此我们可以联想到一个的应用场景:使用主流的 MVVM 或类似框架每次修改数据后,并不是马上就会同步触发视图更新的。我们自己开发渲染库的话,可以在业务开发者多次进行数据修改和页面 DOM 渲染之间,做一个数据改动的汇总的操作,然后根据实际最终得到的变更差异,去渲染 DOM 更新页面视图。如果每次修改数据都同步去更新 DOM,那 DOM 更新的频率就太高了。

  • TODO:上面说的可能不对,因为渲染库也可以在所有修改数据动作结束之后,走同步方法做数据改动的汇总操作,然后再去渲染 DOM。

setTimeout 的时间误差

在使用 setTimeout 的时候,经常会发现设定的时间与自己设定的时间有差异。

如果改成下面这段会发现执行时间远远超过预定的时间:

javascript
setTimeout(() => {
    task()
},3000)

sleep(10000000)

这是为何?

我们来看一下是怎么执行的:

  1. task() 进入到 event table 里面注册计时。
  2. 然后主线程执行 sleep 函数,但是非常慢。计时任然在继续。
  3. 3 秒到了。task() 进入 event queue,但是主线程依旧没有走完。
  4. 终于过了 10000000ms 之后主线程走完了,task() 进入到主线程。
  5. 所以 task() 被调用前的真实延迟时间是远远大于 3 秒的。

setTimeout 第二个参数最小值

HTML5标准规定了 setTimeout() 中第二个参数如果传入小于 0 的值会被修改为 0,如果 setTimeout 或者 setInterval 嵌套层级超过 5 层,并且第二个参数传入的数值小于 4,那这个数值会被设置为 4。(本段参考了https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout

如果第二个参数如果传入 0,就表示要尽快调用回调函数,更准确地说 —— 就是要在下一个事件循环(event cycle)中调用回调函数。(本段参考了setTimeout() global function

promiseprocess.nextTick

process.nextTick(callback) 类似 Node.js 版的 setTimeout,在事件循环的下一次循环中调用 callback 回调函数。

几个例子

例1

javascript
setTimeout(() => {
    console.log(1)
}, 0)
new Promise((resolve) => {
    resolve()
}).then(() => {
    console.log(2)
})

上面的执行结果是2,1。

从规范上来讲,setTimeout有一个4ms的最短时间,也就是说不管你设定多少,反正最少都要间隔4ms(不是精确时间间隔)才运行里面的回调。 而Promise的异步没有这个问题。

从具体实现上来说,这两个的异步队列不一样,Promise所在的那个异步队列优先级要高一些。

例2

javascript
(function () {
    setTimeout(() => {
        console.log(4)
    }, 0)
    new Promise((resolve) => {
        console.log(1)
        for (let i = 0; i < 10000; i++) {
            i === 9999 && resolve(null)
        }
        console.log(2)
    }).then(() => {
        console.log(5)
    })
    console.log(3)
})()

执行结果1,2,3,5,4。为什么执行这样的结果?

  1. 创建Promise实例是同步执行的。所以先输出1,2,3,这三行代码都是同步执行。
  2. promise.then和setTimeout都是异步执行,会先执行谁呢?

setTimeout 异步会放到异步队列中等待执行。promise.then 异步会放到 microtask queue 中。microtask 队列中的内容经常是为了需要直接在当前脚本执行完后立即发生的事,所以当同步脚本执行完之后,就调用 microtask 队列中的内容,然后把异步队列中的 setTimeout 的回调函数放入执行栈中执行,所以最终结果是先执行 promise.then 异步,然后再执行 setTimeout 中的异步回调。

这是由于:

Promise 的回调函数属于异步任务,会在同步任务之后执行。但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。

注意:目前 microtask 队列中常用的就是 promise.then。

例3

javascript
setTimeout(() => {
    console.log(7)
}, 0)
new Promise((resolve, reject) => {
    console.log(3);
    resolve();
    console.log(4);
}).then(() => {
    console.log(6)
})
console.log(5)

执行结果3,4,5,6,7。

例4

javascript
console.log(1)
const promise = new Promise((resolve, reject) => {
    console.log(2)
    setTimeout(() => {
        resolve(3)
        reject(4)
    }, 0)
})

promise.then((data) => {
    console.log(data)
}).catch((err) => {
    console.log('6')
    console.log(err)
})

promise.then((data) => {
    console.log(data)
}).catch((err) => {
    console.log('7')
    console.log(err)
})

console.log(5)

上面这段代码会输出:1、2、5、3、3。

promise 的 resolve、reject

这里需要注意,promise 被 resolve 后再触发 reject 是无效的,不会触发 promise.catch 回调。

Vue3 的 nextTick()

A utility for waiting for the next DOM update flush.

When you mutate reactive state in Vue, the resulting DOM updates are not applied synchronously. Instead, Vue buffers them until the "next tick" to ensure that each component updates only once no matter how many state changes you have made.

nextTick() can be used immediately after a state change to wait for the DOM updates to complete. You can either pass a callback as an argument, or await the returned Promise.

vue
<script setup>
    import { ref, nextTick } from 'vue'

    const count = ref(0)

    async function increment() {
        count.value++

        // DOM not yet updated
        console.log(document.getElementById('counter').textContent) // 0

        await nextTick()
        // DOM is now updated
        console.log(document.getElementById('counter').textContent) // 1
    }
</script>

<template>
    <button id="counter" @click="increment">{{ count }}</button>
</template>

实现原理

原理部分参考了:面试官:Vue中的$nextTick有什么作用?

javascript
export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve;

    // cb 回调函数会经统一处理压入 callbacks 数组
    callbacks.push(() => {
        if (cb) {
            // 给 cb 回调函数执行加上了 try-catch 错误处理
            try {
                cb.call(ctx);
            } catch (e) {
                handleError(e, ctx, 'nextTick');
            }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });

    // 执行异步延迟函数 timerFunc
    if (!pending) {
        pending = true;
        timerFunc();
    }

    // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve;
        });
    }
}

上例中,timerFunc 函数会根据当前环境支持什么方法则确定调用哪个,分别有:Promise.thenMutationObserversetImmediatesetTimeout


JavaScript 函数与变量声明提升

函数作用域与变量提升

for 循环的例子:

javascript
// 打印:4、5、6
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(++i), 0)
}

// 打印:1、2、3
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(++i), 0)
}

变量声明在函数内部提升至顶部的例子:

javascript
var foo = 1;
function bar () {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
// 会 alert 10
bar();

变量声明提升在作用域最顶部,其次是函数声明,最后是赋值语句:

javascript
var a = 1;
function b () {
    a = 10;
    return;
    function a() {}
}
b();
// 会 alert 1
alert(a);
javascript
function a () {
    var b = 1;
    function b () {};
    console.log(b);
}
// 输出 1
a();
javascript
function a () {
    var b;
    function b () {};
    console.log(typeof b);
}
// 输出 'function'
a();
javascript
function a () {
    function b () {};
    var b;
    console.log(typeof b);
}
// 也是输出 'function'
a();
javascript
function a () {
    function b () {};
    var b = 2;
    console.log(b);
}
// 输出 2
a();

例子1

javascript
var msg = 'String A'
function test () {
    // 弹出 `undefined`
    alert(msg)
    var msg = 'String A'
    // 弹出 'String A'
    alert(msg)
}
test()

// 上面的代码等价于下面的写法:
var msg = 'String A'
function test () {
    var msg
    alert(msg)
    msg = 'String A'
    alert(msg)
}

来一个复杂的例子:

写出下面代码 abc 三行的输出分别是什么?

javascript
// mark A
function fun (n, o) {
    console.log(o)
    return {
        // mark B
        fun: function (m) {
            // mark C
            return fun(m, n)
        }
    }
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3);

// 答案:
// undefined, 0, 0, 0
// undefined, 0, 1, 2
// undefined, 0, 1, 1

首先,可以分析得到的结论:标记 A 下面的 fun 函数和标记 C 下面 returnfun 是同一个函数,标记 B 下面的 fun 属性对应的函数不同于标记 A 和标记 C 下方的函数。下文为了行文方便,将各个标记处下方的函数方便叫做 A、B、C 函数。

a 行的分析:

  • a = fun(0):即 a = fun (0) {console.log(undefined) return { // ... } },故输出 undefined
  • a.fun(1):相当于给 B 函数传了一个参数 1,返回了 C 函数传参 (1, 0) 执行后的结果,即 A 函数传参 (1, 0) 后执行的结果,故输出 0;
  • a.fun(2)a.fun(2) 同上,因为一开始 a = fun(0) 已经将 n 的值定为 0 了,后面 console.log 出来的就都是0了;

b 行的分析:

  • fun(0):毫无疑问输出 undefined
  • fun(0).fun(1):参考 a 行的分析,可知这里输出的是 0;
  • fun(0).fun(1).fun(2):类似的,输出 1;
  • fun(0).fun(1).fun(2).fun(3):类似的,输出 2;

c 行的分析:

  • fun(0).fun(1):参见上面的分析,输出 undefined、0;
  • c.fun(2)、c.fun(3):参见之前的分析,输出 1、1。

Promise的实现

简单版Promise

javascript
class SimplePromise {
    /**
     * pending 初始状态,既不是成功,也不是失败状态。
     * 等待 resolve 或者 reject 调用更新状态。
     */
    static pending = 'pending'
    /**
     * fulfilled 意味着操作成功完成。
     * 状态从 pending 转换为 fulfilled,只能由 resolve 方法完成转换
     */
    static fulfilled = 'fulfilled'
    /**
     * rejected 意味着操作失败。
     * 状态从 pending 转换为 rejected,只能由 reject 方法完成转换
     */
    static rejected = 'rejected'

    callbacks = []

    constructor(executor) {
        // 初始化状态为pending
        this.status = SimplePromise.pending
        // 存储 this._resolve 即操作成功 返回的值
        this.value = undefined
        // 存储 this._reject 即操作失败 返回的值
        this.reason = undefined
        /**
         * 存储 `then` 中传入的参数
         * 至于为什么是数组呢?
         * 因为同一个 Promise 的 `then` 方法可以调用多次。
         * 比如:
         * ```javascript
         * const p = new Promise(
         *     (resolve, reject) => resolve('3')
         * );
         * p.then(console.log);
         * p.then(console.log);
         * ```
         * 上面后面的两句 `p.then(console.log)` 都会打印 '3',
         * 都是基于 p 的结果 3 进行处理的(两个 `p.then` 互相无关)
         */
        this.callbacks = [];
        // 这里绑定 this 是为了防止执行时 this 的指向被改
        executor(
            this._resolve.bind(this),
            this._reject.bind(this)
        );
    }

    // onFulfilled 是成功时执行的函数
    // onRejected 是失败时执行的函数
    then(onFulfilled, onRejected) {
        // 这里可以理解为在注册事件
        // 也就是将需要执行的回调函数存储起来
        this.callbacks.push({
            onFulfilled,
            onRejected,
        });
    }

    _resolve(value) {
        this.value = value
        // 将状态设置为成功
        this.status = SimplePromise.fulfilled
        
        // 通知事件执行
        this.callbacks.forEach((cb) => this._handler(cb))
    }

    _reject(reason) {
        this.reason = reason
        // 将状态设置为失败
        this.status = SimplePromise.rejected

        // 通知事件执行
        this.callbacks.forEach((cb) => this._handler(cb))
    }

    _handler(callback) {
        const { onFulfilled, onRejected } = callback;

        if (
            this.status === SimplePromise.fulfilled &&
            onFulfilled
        ) {
            // 传入存储的值
            onFulfilled(this.value);
        }

        if (
            this.status === SimplePromise.rejected &&
            onRejected
        ) {
            // 传入存储的错误信息
            onRejected(this.reason);
        }
    }
}

支持链式调用的 Promise

要求下面打印出来1、3

javascript
const p = new ChainablePromise((resolve) => {
    resolve(1)
})
p
    .then((val) => {
        console.log(val)
        return 3
    }, (reason) => {
        console.log(reason)
    })
    .then((val) => {
        console.log(val)
    }, (reason) => {
        console.log(reason)
    })

实现方式

javascript
class ChainablePromise {
    static pending = 'pending';
    static fulfilled = 'fulfilled';
    static rejected = 'rejected';

    constructor(executor) {
        // 初始化状态为pending
        this.status = ChainablePromise.pending;
        // 存储 this._resolve 即操作成功 返回的值
        this.value = undefined;
        // 存储 this._reject 即操作失败 返回的值
        this.reason = undefined;
        /**
         * 存储 then 中传入的参数
         * 至于为什么是数组呢?
         * 因为同一个 Promise 的 then 方法可以调用多次
         */
        this.callbacks = [];
        executor(
            this._resolve.bind(this),
            this._reject.bind(this)
        );
    }

    /**
     * onFulfilled 是成功时执行的函数
     * onRejected 是失败时执行的函数
     */
    then(onFulfilled, onRejected) {
        // 返回一个新的Promise
        return new ChainablePromise((
            nextResolve,
            nextReject
        ) => {
            /**
             * 这里之所以把下一个 Promise 的 resolve 函数
             * + 和 reject 函数也存在 callback 中
             * 是为了将 onFulfilled 的执行结果
             * 通过 nextResolve 传入到下一个 Promise
             * + 作为它的 value 值
             */
            this._handler({
                nextResolve,
                nextReject,
                onFulfilled,
                onRejected
            });
        });
    }

    _resolve(value) {
        /**
         * 处理 onFulfilled 执行结果是一个 Promise 时的情况
         * 这里可能理解起来有点困难
         * 当 value instaneof ChainablePromise 时,
         * 说明当前 Promise 肯定不会是第一个 Promise
         * 而是后续 then 方法返回的 Promise(第二个 Promise)
         *
         * 我们要获取的是 value 中的 value 值
         * (
         * 有点绕,value 是个 promise 时,
         * 那么内部存有个 value 的变量
         * )
         *
         * 怎样将 value 的 value 值获取到呢,
         * 可以将传递一个函数作为 value.then 的 onFulfilled 参数
         *
         * 那么在 value 的内部则会执行这个函数,
         * 我们只需要将当前 Promise 的 value 值
         * + 赋值为 value 的 value 即可
         */
        if (value instanceof ChainablePromise) {
            value.then(
                this._resolve.bind(this),
                this._reject.bind(this)
            );
            return;
        }

        this.value = value;
        // 将状态设置为成功
        this.status = ChainablePromise.fulfilled;

        // 通知事件执行
        this.callbacks.forEach(cb => this._handler(cb));
    }

    _reject(reason) {
        if (reason instanceof ChainablePromise) {
            reason.then(
                this._resolve.bind(this),
                this._reject.bind(this)
            );
            return;
        }

        this.reason = reason;
        // 将状态设置为失败
        this.status = ChainablePromise.rejected;

        this.callbacks.forEach(cb => this._handler(cb));
    }

    _handler(callback) {
        const {
            onFulfilled,
            onRejected,
            nextResolve,
            nextReject
        } = callback;

        if (this.status === ChainablePromise.pending) {
            this.callbacks.push(callback);
            return;
        }

        if (this.status === ChainablePromise.fulfilled) {
            // 传入存储的值
            // 未传入 onFulfilled 时,传入 value
            const nextValue = onFulfilled
                ? onFulfilled(this.value)
                : this.value;
            nextResolve(nextValue);
            return;
        }

        if (this.status === ChainablePromise.rejected) {
            // 传入存储的错误信息
            // 同样的处理
            const nextReason = onRejected
                ? onRejected(this.reason)
                : this.reason;
            nextReject(nextReason);
        }
    }
}

const p = new ChainablePromise((resolve) => {
    resolve(1)
})
p
    .then((val) => {
        console.log(val)
        return 3
    }, (reason) => {
        console.log(reason)
    })
    .then((val) => {
        console.log(val)
    }, (reason) => {
        console.log(reason)
    })

JS原型与原型链

普通对象、函数对象、原型对象

普通对象和函数对象

JS中,对象分普通对象函数对象,Object、Function是JS自带的函数对象。凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。

javascript
typeof Object // "function", 函数对象
typeof Function // "function", 函数对象

function f1 () {}
var f2 = function () {}
var f3 = new Function('str', 'console.log(str)')

var o1 = new f1()
var o2 = {}
var o3 = new Object()

typeof f1 // "function", 函数对象
typeof f2 // "function", 函数对象
typeof f3 // "function", 函数对象

typeof o1 // "object", 普通对象
typeof o2 // "object", 普通对象
typeof o3 // "object", 普通对象

原型对象

每当定义一个对象(函数)时,对象中都会包含一些预定义的属性。

其中,函数对象会有一个 prototype 属性,就是我们所说的原型对象(普通对象没有 prototype,但有 __proto__ 属性;函数对象同时含有 prototype__proto__ 属性)。

原型对象其实就是普通对象(Function.prototype 除外,它是函数对象,但同时它又没有 prototype 属性)。

javascript
function f1 () {}

// Object{} with two properties: `constructor` and `__proto__`
console.log(f1.prototype)

// "object"
typeof f1.prototype

// 'function'
typeof Object.__proto__

// 特例,没必要记住,平常根本用不到
typeof Function.prototype // "function"
typeof Function.prototype.prototype // "undefined"
typeof Object.prototype // "object"

原型对象的主要作用是用于继承:

javascript
var Person = function (name) {
  this.name = name
}

Person.prototype.getName = function () {
  return this.name
}

var yakima = new Person('yakima')
yakima.getName() // "yakima"

// true
console.log(yakima.__proto__ === Person.prototype)

原型链

上面提到原型对象的主要作用是用于继承,其具体的实现就是通过原型链实现的。创建对象(不论是普通对象还是函数对象)时,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象(即函数对象的prototype属性)

javascript
// true,对象的内置__proto__对象指向创建该对象的函数对象的prototype
yakima.__proto__ === Person.prototype

// true
Person.prototype.__proto__ === Object.prototype

// 继续,Object.prototype对象也有__proto__属性,但它比较特殊,为null
Object.prototype.__proto__ === null // true

typeof null // "object"

这个由__proto__串起来的直到Object.prototype.__proto__ ==> null对象的链称为原型链。

  1. yakima的__proto__属性指向Person.prototype对象;
  2. Person.prototype对象的__proto__属性指向Object.prototype对象;
  3. Object.prototype对象的__proto__属性指向null对象

原型和原型链是JS实现继承的一种模型。

看个例子

javascript
var Animal = function () {}
var Dog = function () {}

Animal.price = 2000
Dog.prototype = Animal

var tidy = new Dog()

console.log(Dog.price) // undefined
console.log(tidy.price) // 2000

对上例的分析:

  • Dog自身没有price属性,沿着__proto__属性往上找,因为Dog赋值时的Dog = function () {}其实使用new Function ()创建的Dog,所以,Dog.__proto__ ==> Function.prototype, Function.prototype.__proto__ ===> Object.prototype,而Object.prototype.__proto__ ==> null。很明显,整条链上都找不到price属性,只能返回undefined了;
  • tidy自身没有price属性,沿着__proto__属性往上找,因为tidy对象是Dog函数对象的实例,tidy.__proto__ ==> Dog.prototype ==> Animal,从而tidy.price获取到了Animal.price的值。

constructor

原型对象中都有个 constructor 属性,用来引用它的函数对象。这是一种循环引用。

javascript
Person.prototype.constructor === Person // true
Function.prototype.constructor === Function // true
Object.prototype.constructor === Object // true

CSS

三栏布局

要求

分为左、中、右三部分,高度均为屏幕高度,左边部分宽度为200px,另外两部分等分剩下的页面宽度。

实现

html
<html>
<head></head>
<body>
<div class="container">
    <aside class="left">Left</aside>
    <div class="wrapper">
        <article class="middle">Middle</article>
        <article class="right">Right</article>
    </div>
</div>
</body>
</html>
less
.clearfix() {
  &:after {
    content: '';
    clear: both;
    display: block;
    height: 0;
    opacity: 0;
    visibility: hidden;
  }
}
html, body, div, aside, article {
    margin: 0;
    padding: 0;
}
html, body, .container, .left, .wrapper, .middle, .right {
    height: 100%;
}
.container {
    padding-left: 200px;
    .clearfix();
    
    .left {
        float: left;
        width: 200px;
        margin-left: -200px;
        background-color: skyblue;
    }
    
    .wrapper {
        float: left;
        width: 100%;
        
        .middle, .right {
            float: left;
            width: 50%;
        }
        .middle {
            background-color: gray;
        }
        .right {
            background-color: yellow;
        }
    }
}

input 框加入 disabled 属性后字体颜色变淡

css
input[disabled] {
    opacity: 1;
}

z-index

建议使用CSS预处理器语言的情况下,对所有涉及z-index的属性的值放在一个文件中统一进行管理。这个主意是从饿了么前端团队代码风格指南中看到的。另外补充一下,应该将同一条直系链里同一层级的元素的z-index分类到一起进行管理。因为不同层级或者非直系链里的同一层级的元素是无法直接根据z-index来判断元素前后排列顺序的。

图片在父元素中水平、垂直居中

方案 1:(flex 布局)

less
.parent {
    display: flex;
    align-items: center;
    justify-content: center;
}

方法 2(使用 absolute 绝对定位)

less
.parent {
    position: relative;
    display: block;

    .img {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
}

方法 3(使用 table-cell

less
.parent {
    display: table-cell;
    // width要写得大一点,以撑满容器之外部容器的宽度
    width: 3000px;
    text-align: center;
    vertical-align: middle;
    
    .img {
        display: inline-block;
        vertical-align: middle;
    }
}

方法 4(如果父元素的高度为已知的定值,使用 line-height 实现)

less
.parent {
    display: block;
    text-align: center;
    height: 300px;
    line-height: 300px;

    .img {
        display: inline-block;
    }
}

方法5(写死间距)

less
.parent {
    display: block;
    
    .img {
        display: block;
        height: 100px;
        margin: 150px auto 0;
    }
}

方案6(写死定位)

less
.parent {
    position: relative;
    display: block;
    width: 600px;
    height: 400px;

    .img {
        position: absolute;
        width: 100px;
        height: 300px;
        top: 50px;
        left: 250px;
    }
}

方案7(撑开外部容器)

less
.parent {
    // 包围内部元素
    display: inline-block;
    
    .img {
        // 用来撑开父元素
        padding: 30px 20px;
    }
}

方案8(作为背景图)

less
.parent {
    display: block;
    height: 300px;
    background:
        transparent url('./example.png') scroll no-repeat center center;
    background-size: 100px 200px;
}

弹性盒(Flexible Box)模型

justify-content

  • flex-start:默认值,伸缩项目向一行的起始位置靠齐;
  • flex-end:伸缩项目向一行的结束位置靠齐;
  • center:项伸缩项目向一行的中间位置靠齐;
  • space-between:伸缩项目会平均地分布在行里。第一个伸缩项目一行中的最开始位置,最后一个伸缩项目在一行中最终点位置;
  • space-around:伸缩项目会平均地分布在行里,两端保留一半的空间;
  • initial:设置该属性为它的默认值;
  • inherit:从父元素继承该属性。

align-items

  • stretch:默认值,项目被拉伸以适应容器;
  • center:项目位于容器的中心;
  • flex-start:项目位于容器的开头;
  • flex-end:项目位于容器的结尾;
  • baseline:项目位于容器的基线上;
  • initial:设置该属性为它的默认值;
  • inherit:从父元素继承该属性。

弹性盒实现竖向九宫格

要求

使用 flexbox 布局将 9 个格子排列成 3*3 的九宫格,且第一列排完才排第二列。

html
<html>
<head></head>
<body>
    <section class="boxes-wrapper">
        <div class="box">1</div>
        <div class="box">2</div>
        <div class="box">3</div>
        <div class="box">4</div>
        <div class="box">5</div>
        <div class="box">6</div>
        <div class="box">7</div>
        <div class="box">8</div>
        <div class="box">9</div>
    </section>
</body>
</html>

实现

less
body {
    margin: 0;
}
.boxes-wrapper {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 10px;
    width: 320px;
    height: 320px;

    .box {
        background-color: aqua;
        width: 100px;
        height: 100px;
        text-align: center;
        line-height: 100px;
    }
}

清除浮动的原理

清除浮动使用 clear: left/right/both。业界常用的 .clearfix 也是这么做的,只不过是把该样式写进了父元素的 :after 伪元素中,并加了 opacity: 0; display: block; height: 0; visibility: hidden; 等使伪元素不可见。

不清除浮动但包围浮动元素的方法有:

为浮动元素的父元素添加 overflow: hidden、或将父元素也浮动起来等使父元素形成 BFC(Block Formatting Context) 的方式,但这些方式在应用上没有 .clearfix 这种方式理想。

简述 position 属性各个值的区别

fixed:类似 absolute,但是是相对浏览器窗口而非网页页面进行定位。

absolute:相对最近的 position 值非 static 的外层元素进行定位。

relative:相对自身在文档流中的原始位置进行定位。

staticposition 默认值,即元素本身在文档流中的默认位置(忽略 topbottomleftrightz-index 声明)。

inherit:继承父元素 position 属性的值。

边距塌陷及其修复

竖直方向上相接触的 margin-topmargin-bottom 会塌陷:

  • 若二者均为正值,或均为负值,取其绝对值大者;
  • 若二者中一正一负,则取二者之和。

高性能动画

CSS 动画会比 JS 动画的性能更好,JS 动画的优势主要在于:

  • 更具定制性(毕竟 JS 比 CSS 更可编程);
  • 更易实现对低端浏览器的兼容。

当然,大部分业务中,主要还是使用 CSS 动画的,对低端浏览器进行降级就可以了(保证页面可读可操作就可以了,不建议增加老旧设备的性能负担)。

几个注意点:

  • 利用 transform: translate3d(x, y, z); 可借助 3D 变形开启 GPU 加速(这会消耗更多内存与功耗,确有性能问题时再考虑)。
  • 若动画开始时有闪烁,可尝试:backface-visibility: hidden; perspective: 1000;
  • 尽可能少用 box-shadowsgradients 这两页面性能杀手。
  • CSS 动画属性可能会触发整个页面的重排(reflow/relayout)、重绘(repaint)和重组(recomposite)。其中 paint 通常是最花费性能的,尽可能避免使用触发 paint 的 CSS 动画属性。所以要尽可能通过修改 translate 代替修改 top/left/bottom/right 来实现动画效果,可以减少页面重绘(repaint),前者只触发页面的重组,而后者会额外触发页面的重排和重绘。
  • 尽量让动画元素脱离文档流(document flow)中,以减少重排(reflow)。
  • 操作 DOM 的 js 语句能连着写尽量连着写,这样可借助浏览器的优化策略,将可触发重排的操作放于一个队列中,然后一次性进行一次重排;如果操作 DOM 的语句中间被其他诸如赋值语句之类的间断了,页面可能就会发生多次重排了。

HTML

HTML 页面的渲染

本文参考:html+CSS+js解析全过程

主流程

从浏览器请求html文件,到屏幕上出现渲染的实际像素等,可以分为以下部分:

  1. 构建 DOM Tree
  2. 构建 CSSOM Tree
  3. 合成 Render Tree
  4. layout 回流:主要涉及到字体大小、元素长宽等 CSS 属性,计算元素的相对位置
  5. repaint 重绘:颜色等 CSS 属性,显示在屏幕上

解析HTML文件的细节

  1. 解析 DOM 元素
  2. 遇 script,DOM 解析暂停
    • 包含 JS 代码
    • 外联:加载 JS
  3. 执行 JS(该 JS 代码在 CSS 前,则不受 CSSOM 的阻塞)
  4. 遇 link CSS
  5. 加载 CSS 文件,不阻塞 DOM 解析
    • 遇到 script:JS 加载,等待 CSSOM Tree 构建完成后再运行
      • 防止 JS 代码执行时 获取旧的 CSS 属性
  6. 构建 CSSOM Tree,DOM 继续解析
  7. 若有 JS,则运行
  8. 构建 DOM Tree
  9. 渲染

DOMContentLoaded 与 load 事件

  • DOMContentLoaded 事件:在完成 DOM 解析完成,JS 执行完毕后触发。大多数浏览器也会等到 CSS 文件加载并解析完成。
  • load 事件:所有外部资源与文件下载完毕后触发。

关于阻塞/非阻塞

  • 内联 JS 会阻塞 DOM 的解析。
  • 内联 CSS 会阻塞 DOM 解析。
  • 外联 CSS 加载不阻塞 DOM 解析,阻塞 DOM 渲染。
  • 外联 CSS 加载阻塞后续 script 内的 JS 代码执行(不阻塞前面的)
  • JS 文件的普通加载与执行(非 async defer)会阻塞 DOM 的解析
    • 因此实际上,后面有 script 的外联 CSS 会阻塞DOM的解析
  • <script defer> 不会阻塞 DOM 的解析
  • <script async> 加载不阻塞,执行阻塞
  • iframe 内的 image 等资源不阻塞 DOM 解析

其他注意事项

  • DOM Tree 增量构建,而 CSSOM Tree 非渐进。

常用 meta 标签

html
<!-- 设定页面使用的字符集 -->
<meta charset="utf-8">
<meta
    http-equiv="Content-Type"
    content="text/html; charset=UTF-8"
>

<!-- 优先使用 IE 最新版本和 Chrome -->
<meta
    http-equiv="X-UA-Compatible"
    content="IE=edge,chrome=1"
>

<!-- 国产360浏览器默认采用高速模式渲染页面 -->
<meta name="renderer" content="webkit">

<meta
    name="viewport"
    content="
        width=device-width,
        initial-scale=1,
        user-scalable=no
    "
>

<!-- 禁止设备检测手机号和邮箱 -->
<meta
    name="format-detection"
    content="telephone=no,email=no"
>

<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">

<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">

<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">

<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">

<!-- UC应用模式 -->
<meta name="browsermode" content="application">

<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">

<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">

<!--
    针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,
    比如黑莓
-->
<meta name="HandheldFriendly" content="true">

<!-- 设置 web 应用在 iOS 设备中是否启用全屏模式 -->
<meta name="apple-mobile-web-app-capable" content="yes">

<!--
    设置 iOS 设备中 web 应用的状态栏样式。
    只在用 `apple-apple-mobile-web-app-capable`
    + 开启全局模式后生效
-->
<meta
    name="apple-mobile-web-app-status-bar-style"
    content="black"
>

<!-- 启用/禁用 iOS Safari 浏览器中自动检测手机号的功能 -->
<meta name="format-detection" content="telephone=no">

<!--
    用于设定禁止浏览器从本机的缓存中读取页面内容。
    设定后一旦离开网页就无法从缓冲中再读取
-->
<meta http-equiv="pragma" content="no-cache">

<!-- 禁用缓存(再次访问需重新下载页面) -->
<meta http-equiv="cache-control" content="no-cache">

<!--
    可以用于设定网页的到期时间。
    一旦网页过期,必须到服务器上重新传输
-->
<meta http-equiv="expires" content="0">

<!-- 停留 2 秒钟后自动刷新到 URL 网址 -->
<meta
    http-equiv="Refresh"
    content="2;URL=http://www.example.com/"
>

<!-- 用于 SEO,其中 description 的内容应不超过 150 个字符 -->
<meta
    http-equiv="keywords"
    content="keyword1,keyword2,keyword3"
>
<meta
    http-equiv="description"
    content="This is my page"
>

<!--
    强制页面在当前窗口以独立页面显示,
    用来防止别人在 iframe 里调用自己的页面
-->
<meta http-equiv="Window-target" content="_top">

script 标签

本文参考资料如下:

script 标签用于嵌入可执行脚本或数据,一般用于嵌入一段 JavaScript 脚本,或者指向一个 JavaScript 文件。script 标签也可用于其他语言,比如 WebGL 的 GLSL shader 编程语言脚本,或者 JSON 数据等。

用 script 标签载入数据

注意,如果用 script 标签来载入数据(而非脚本):

  • 这些数据必须是以内联的方式嵌入的,并且需要通过 type 属性指定数据的格式。
  • 同时需要禁止使用以下属性:srcasyncnomoduledefercrossoriginintegrityreferrerpolicyfetchpriority

比如这样是可以的:

html
<script id="data" type="application/json">
    {"a": "123"}
</script>
<script>
    const jsonData = JSON.stringify(
        document.querySelector('#data').textContent
    )
</script>

但是下面这样是不可以的:

html
<!-- 这样直接通过 src 属性去指向一个数据文件的方式是不可以的 -->
<script
    id="data"
    type="application/json"
    src="https://bla.bla.com/blabla.json">
</script>

模板语言

script 标签有个特点是其中的内容不会直接展现在页面上,所以有很多前端模板语言会使用 script 标签来存放 html 模板。我们可以自己写个简单的,像下面这样:

html
<div class="userInfo"></div>

<script id="template" type="text/template">
    <div>
        <div class="name">{{ userName }}</div>
        <div class="address">{{ userAddress }}</div>
    </div>
</script>

<script type="application/javascript">
    const templateContent = document
        .querySelector('#template')
        .textContent
    const templateData = {
        userName: 'name',
        address: 'address'
    }
    // 一般模版语言的渲染函数实际干的内容类似下面这样
    document
        .querySelector('.userInfo')
        .innerHTML = templateContent
        .replace(/\{\{ userName \}\}/m, templateData.userName)
        .replate(/\{\{ address \}\}/m, templateData.address)
</script>

<script defer>、<script async> 脚本的下载、解析动作与 HTML 解析的时序关系

从下图可以看书,除了 <script> 会暂停 HTML 的解析,其他比如 <script defer>、<script async>、<script type="module"> 脚本的下载与 HTML 的解析都是并行不会暂停 HTML 的解析的。

<script async>:

  • 只对外部脚本有效。
  • 请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML。
  • 多个 async script 之间的执行顺序是不确定的,取决于谁先被下载完毕。
  • 多个 async script 之间的下载开始时间与书写顺序一致,他们是可以并行下载的,下载结束的时间顺序是不确定的。
  • 下载完毕后会被直接解析执行,如果此时 HTML 还未被解析完,浏览器会暂停解析 HTML,先执行 JS 脚本。
  • async script 一定会在页面的 load 事件之前执行,但与 DOMContentLoaded 事件则没有确定的先后关系。

<script defer>:

  • 只对外部脚本有效。
  • 请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML。
  • 多个 defer script 之间的执行书序和书写顺序一致。
  • 多个 async script 之间的下载开始时间与书写顺序一致,他们是可以并行下载的,下载结束的时间顺序是不确定的。
  • 下载完成后 JS 脚本不会立即执行,会等到浏览器解析完 HTML 后再执行 JS 脚本。
  • 赋予 defer 属性的 script 脚本会在 HTML 文档解析完成后被执行,在此之后才会触发 DOMContentLoaded 事件。

DOM

获取元素

书写原生js脚本将body下的第二个div隐藏

javascript
var oBody = document.getElementsByTagName('body')[0]
var oChildren = oBody.childNodes
var nDivCounter = 0

for (var i = 0, len = oChildren.length; i < len; i++) {
    if (oChildren[i].nodeName === 'DIV') {
        nDivCounter++
        if (nDivCounter === 2) {
            oChildren[i].style.display = 'none'
        }
    }
}

创建元素

问题

现有:

html
<ul id="list" class="foo">
    <li>#0</li>
    <li><span>#1</span></li>
    <li>#2</li>
    <li>#3</li>
    <li>
        <ul>
            <li>#4</li>
        </ul>
    </li>
    <!-- ... -->
    <li><a href="//v2ex.com">#99998</a></li>
    <li>#99999</li>
    <li>#100000</li>
</ul>

要求:

  • 为ul元素添加一个类.bar
  • 删除第10个li
  • 在第500个li后面添加一个li,其文字内容为“<v2ex.com />”
  • 点击任意li弹框显示其为当前列表中的第几项

解答

javascript
// 还原题目真实DOM结构
var list = document.getElementById('list')
void function() {
    var html = ''
    for (var i = 0; i <= 10000; i++) {
        if (i === 1) {
            html += '<li><span>#1</span></li>'
        } else if (i === 4) {
            html += '<li><ul><li>#4</li></ul></li>'
        } else if (i === 9998) {
            html += '<li><a href="//v2ex.com">#9998</a></li>'
        } else {
            html += '<li>#' + i + '</li>'
        }
    }
    list.innerHTML = html
}()

// or, list.className += ' bar'
list.classList.add('bar')

var li10 = document.querySelector('#list > li:nth-of-type(10)')
li10.parentNode.removeChild(li10)

var newItem = document.createElement('LI')
var textNode = document.createTextNode('<v2ex.com />')
newItem.appendChild(textNode)

// index for css nth-of-type is 1-based
var li501 = document.querySelector('#list > li:nth-of-type(501)')
list.insertBefore(newItem, li501)

list.addEventListener('click', function(e) {
    var target = e.target || e.srcElement
    if (target.id === 'list') {
        alert('你点到最外层的ul上了,叫我怎么判断?')
        return
    }
    while (target.nodeName !== 'LI') {
        target = target.parentNode
    }

    var parentUl = target.parentNode
    var children = parentUl.childNodes
    var count = 0
    for (var i = 0, len = children.length; i < len; i++) {
        var node = children[i]
        if (node.nodeName === 'LI') {
            count++
        }
        if (node === target) {
            alert('是当前第' + count + '项')
            break
        }
    }
}, false)

// PS: if querySelector method is not available, the following can be changed.
var li10 = document.querySelector('#list > li:nth-of-type(10)')
var li501 = document.querySelector('#list > li:nth-of-type(501)')

// As below:
function getLiByIndex(index /* 0-based index */ ) {
    var count = -1
    for (var i = 0, len = list.childNodes.length; i < len; i++) {
        if (list.childNodes[i].nodeName === 'LI') {
            count++
            if (count === index) {
                return list.childNodes[i]
            }
        }
    }
}
var li10 = getLiByIndex(9)
var li501 = getLiByIndex(500)

事件的冒泡和捕获

JS 中事件流的三个阶段:捕获(低版本 IE 不支持)==> 目标 ==> 冒泡。

  • 捕获(Capture):从外到内。
  • 冒泡(Bubbling):从内到外。

如果不同层的元素使用 useCapture 不同,会先从最外层元素往目标元素寻找设定为 捕获(capture)模式的事件,到达目标元素后执行目标元素的事件后,在循原路往外寻找设定为冒泡(bubbling)模式的事件。

addEventListener

语法如下:

javascript
element.addEventListener(type, listener, useCapture)
element.addEventListener(type, listener, options)
  • element: 要绑定事件的对象,或 HTML 节点;
  • type:事件名称(不带“on”),如 “click”、“mouseover”;
  • listener:要绑定的事件监听函数;
  • useCapture:事件监听方式,只能是 truefalsetrue,采用捕获(capture)模式;false,采用冒泡(bubbling)模式。若无特殊要求,一般是 false
  • options
    • options.capture:一个布尔值,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
    • options.once:一个布尔值,表示 listener 在添加之后最多只调用一次。如果为 truelistener 会在其被调用之后自动移除。
    • options.passive:一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

addEventListener

addEventListener() 的工作原理是将实现 EventListener 的函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件侦听器列表中。如果要绑定的函数或对象已经被添加到列表中,该函数或对象不会被再次添加

addEventListener 允许对同一个 target 同时绑定多个事件,且可以控制是在冒泡阶段还是捕获阶段触发。onclickonmouseover 这种方式只能绑定一个事件监听回调(最后绑定的生效),且只能在冒泡阶段触发

removeEventListener

removeEventListener 的入参和 addEventListener 一样。

警告:如果同一个事件监听器分别为“事件捕获(capturetrue)”和“事件冒泡(capturefalse)”各注册了一次,这两个版本的监听器需要分别移除。移除捕获监听器不会影响非捕获版本的相同监听器,反之亦然。

使用 passive 改善滚屏性能

passive 设为 true 可以启用性能优化,并可大幅改善应用性能(副作用是不能 preventDefault 了),正如下面这个例子:

javascript
/* 检测浏览器是否支持该特性 */
let passiveIfSupported = false;

try {
  window.addEventListener(
    "test",
    null,
    Object.defineProperty({}, "passive", {
      get() {
        passiveIfSupported = { passive: true };
      },
    }),
  );
} catch (err) {}

window.addEventListener(
  "scroll",
  (event) => {
    /* do something */
    // 不能使用 event.preventDefault();
  },
  passiveIfSupported,
);

根据规范,addEventListener()passive 默认值始终为 false。然而,这会导致触摸事件和滚轮事件(如 wheelmousewheeltouchstarttouchmove)的事件监听器在浏览器尝试滚动页面时可能会阻塞浏览器主线程——这可能会大大降低浏览器处理页面滚动时的性能

事件代理/委托

事件代理/委托,是靠事件的冒泡机制实现的(所以,对于一些不具有冒泡特性的事件,比如focus、blur,就没有事件代理/委托这种说法了)。

优缺点

优点有:

  • 可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒;
  • 可以实现当新增子孙节点时无需再次对其绑定事件,对于动态内容部分尤为合适。

缺点有:

  • 如果把所有事件都代理到一个比较顶层的 DOM 节点上的话,比较容易出现误判,给不需要执行逻辑的节点执行了逻辑。比如把页面中所有事件都绑定到 document 上进行委托,就不是很合适;
  • 事件逐级冒泡到外层 DOM 上再执行肯定没有直接执行快。

实现

javascript
// 只考虑IE 9&+
function delegate(element, targetSelector, type, handler) {
    element.addEventListener(type, function(e) {
        var targets = Array.prototype.slice.call(
            element.querySelectorAll(targetSelector)
        )
        var target = e.target
        if (targets.indexOf(target) !== -1) {
            return handler.apply(target, arguments)
        }
    })
}

阻止事件传播和默认行为

阻止事件的默认行为

javascript
e = e || window.event
e.preventDefault()

阻止事件传播

javascript
e = e || window.event
e.stopPropagation()

stopImmediatePropagation

stopImmediatePropagation 方法可阻止同元素或外层元素上相同事件上绑定的其他监听器函数被触发。

触发顺序

如果同类型事件的几个监听器函数被绑定到了同一个对象上,且它们处于相同的阶段(冒泡、捕获),它们会按照添加的顺序被触发。

stopPropagationstopImmediatePropagation的区别

  • stopPropagation will prevent any parent handlers from being executed;
  • stopImmediatePropagation will prevent any parent handlers and also any other handlers from executing.

事件的几种 target

target

触发事件的对象,也就是用户实际操作(比如点击)的对象。

获取事件对象和目标对象:

javascript
function (e) {
  e = e ? e : window.event
  var target = e.target || e.srcElement
  // do some things here
}

currentTarget

绑定事件的对象。对应的就是element.addEventListener(eventName, handler, options)里的element。

currentTarget 和 target的比较

  • target指向事件直接作用的对象,而currentTarget指向绑定该事件的对象;
  • 当处于捕获或冒泡阶段时,两者指向不一致;当处于目标阶段时,两者指向一致。
html
<html>
<body>
<div id="a">
    <div id="b">
        <div id="c">
            <div id="d">最里层</div>
        </div>
    </div>
</div>
<script>
    const a = document.querySelector('#a')
    const b = document.querySelector('#b')
    const c = document.querySelector('#c')
    const d = document.querySelector