程序员备忘录

《程序员备忘录》记录了WEB程序员常用的知识点,方便温故知新,自我成长。

本书动笔最早是在2018年左右,起因是自己找工作准备面试,一边准备面试一边总结当时的常见面试题,于是有了本书的“初稿"。说是初稿,但其实内容并不成体系,东一块西一块的,只是简单按目录进行了归类,并不能称之为“书”。

现在,做前端已经有8年了,工作经验和阅历也比之前更丰富了,于是着手将之前的内容进行整理,从而有了本书。此次整理,并不是简单将之前的内容汇总,用行业的话来说,应该叫重构。与此同时,也总结了很多新的话题,以免书中部分内容过时。

这本书的目标读者不包括零基础的纯新手,主要是帮助初、中级开发进阶高级开发,或者帮助高级开发进一步进阶用的。不过光靠看书是不够的,工作经验也是很重要的,工作久了你会有些自己感悟。所以大家在平时工作中还是要多注意代码质量,想想怎么样写更好,平时多进行小范围的重构。这样不仅自己后面维护代码更轻松,而且也对自己的技能进行了实际意义上的打磨,对以后的职业发展更有助益。

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

保护你的眼睛

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

由于编写时间仓促,如您发现有错误或需要补充、删减的地方,或是有想讨论的内容,请到这里提交:https://github.com/Yakima-Teng/memo/issues

最后,希望本书的内容能给大家带来一些帮助。如读者有宝贵意见和建议,欢迎邮件指出,我会及时更新本书内容。

版权说明

说是书,其实当前版本也有不少内容是对网络上的优质内容进行了“拿来主义”,对部分内容进行了调整,希望有时间能慢慢磨好这本电子书的质量。 参考的文档已列与本书末处,但由于整理的部分笔记时间较早,当时的出处已不可考,可能少列了。 若您发现文字和图片有侵犯到您的权益,请务必联系我。

本书中引用的他人文章版权归原作者/平台所有,本人自己写的部分版权归本人所有。本书仅用于个人私下学习。谢绝商用。

作者:Yakima Teng

——2024年1月18日,于上海

目录

前端知识

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

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

基础教程

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

JavaScript

JavaScript数据类型

  • 基本数据类型(primitive data type):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
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

call、apply和bind

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

apply:与call类似,只是所有要传入的数据都是以数组的形式放到第二个参数里的,如func.apply(obj, [arg1, arg2])

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

关于第一个参数

callapplybind的第一个参数,如果传了null或者undefined会被替换为global对象(浏览器环境下的话就是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')
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,则替换成global对象
    if (typeof obj === 'undefined' || obj === null) {
        obj = global
    }
    // 如果不是对象类型,比如是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')
Function.prototype.myBind = function () {
    // 这里的this就是被bind处理前的函数
    const func = this

    let obj = arguments[0]
    // 如果obj是undefined或null,则替换成global对象
    if (typeof obj === 'undefined' || obj === null) {
        obj = global
    }
    // 如果不是对象类型,比如是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')

实现结果如下:

forEach、for-of、for-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)
}
// 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 () {})()
const a = 1
(function () {})()

上述代码会报错,因为上一行的1会和这一行一起被程序解析成const a = 1(function () {})(),然后报错说1不是函数。

这时候可以这样写:

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

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

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

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

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

JS中的new

使用常规的{}花括号可以创建一个对象,但是当我们想要创建相似的对象时,如果还使用{}就会产生很多冗余的代码,所以为了方便,js就设计了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
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;(隐式返回)
}
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
};
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
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);
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 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略 return语句,继续返回 this

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

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

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

console.log(new BigUser().name);  // 小王
function BigUser() {
    this.name = "小明";
    return { name: "小王" }; 
}

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

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

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

console.log(new SmallUser().name);  // 小小王
function SmallUser() {
    this.name = "小小王";
    return;
}

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

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

省略括号

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

javascript
const user = new User;
// 等同于
const user = new User();
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(); // 我的名字是李白
function User(name) {
    this.name = name;

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

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

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

手写一个new

实现

javascript
function myNew() {
    // 1、创建一个空对象
    const obj = {}
    // 获取构造方法
    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
}
function myNew() {
    // 1、创建一个空对象
    const obj = {}
    // 获取构造方法
    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);
function Person(name, age) {
    this.name = name
    this.age = age
}

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

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

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

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

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

const me = Object.create(person);

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

// 打印内容: "My name is Matthew. Am I human? true"
me.printIntroduction();
const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
};

const me = Object.create(person);

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

// 打印内容: "My name is 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();

console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle); // true
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape); // true
rect.move(1, 1); // 打印 'Shape moved.'
// 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();

console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle); // true
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape); // true
rect.move(1, 1); // 打印 'Shape moved.'

使用 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 } });
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__ 键。

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

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

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

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

判断JS全局变量是否存在

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

或者

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

注意二者的区别:

javascript
var a // 或 var a = undefined
'a' in self // true
typeof a // 'undefined'
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

判断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 && keys2.length === 0) {
        return true
    }

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

    return true
}
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 && keys2.length === 0) {
        return true
    }

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

    return true
}

生成器函数与 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}
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 有点像。不同点在于,所有函数(包括生成器函数)都只能返回一次,而在生成器函数中可以 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);
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);

普通函数的 this 指向

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

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

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

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

testObject()

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

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

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

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

默认绑定

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

上述代码输出 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
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();
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();
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
function Test() {
 this.x = 1;
}

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

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

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

new 过程遇到 return 一个对象,此时 this 指向为返回的对象:

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

如果 return 一个简单类型的值,则 this 指向实例对象:

javascript
function fn() {
    this.user = 'xxx';  
    return 1;
}
const a = new fn;
console.log(a.user); // xxx
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
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
const x = 0;
function test() {
 console.log(this.x);
}

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

箭头函数的 this 指向

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

下面是普通函数的列子:

javascript
var name = 'window'; // 其实是window.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();
var name = 'window'; // 其实是window.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();
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)
      return s//返回箭头函数s
   }
}

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

var B = {
   name: 'B'
}

// 输出 `A` 
sayHello.call(B);
// 输出 `A` 
sayHello.call();
var name = 'window'; 

var A = {
   name: 'A',
   sayHello: function(){
      var s = () => console.log(this.name)
      return s//返回箭头函数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 参数代替。
  • 不可以使用 yield 命令,因此箭头函数不能用作 生成器(Generator)函数。

变量提升

例子1

javascript
var msg = 'String A'
function test () {
  alert(msg)
  var msg = 'String A'
  alert(msg)
}
test()
var msg = 'String A'
function test () {
  alert(msg)
  var msg = 'String A'
  alert(msg)
}
test()

上题的分析与答案:

在函数内部声明的变量在函数内部会覆盖掉全局同名变量。在JS预解析时,定义变量的行为会在变量作用域内的顶部实现(hoisting),但是变量的赋值行为并不会提前,所以上述代码等价于如下代码,所以第一次alert弹出的是undefined,第二次alert弹出的是“String A”。

javascript
var msg = 'String A'
function test () {
  var msg
  alert(msg)
  msg = 'String A'
  alert(msg)
}
var msg = 'String A'
function test () {
  var msg
  alert(msg)
  msg = 'String A'
  alert(msg)
}

例子2

写出下面代码a、b、c三行的输出分别是什么?

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
// 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下面return的fun是同一个函数,标记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。

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", normal object
typeof o3 // "object", normal object
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", normal object
typeof o3 // "object", normal 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"
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)
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"
// 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
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
Person.prototype.constructor === Person // true
Function.prototype.constructor === Function // true
Object.prototype.constructor === Object // true

闭包

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

闭包的作用:

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

闭包案例1

javascript
function fn1() {
    var num = 10;
    function fn2() {
        console.log(num);
    }
    fn2();
}
fn1(); //输出结果10
function fn1() {
    var num = 10;
    function fn2() {
        console.log(num);
    }
    fn2();
}
fn1(); //输出结果10

fn2的作用域当中访问到了fn1函数中的num这个局部变量, 所以此时 fn1 就是一个闭包函数(被访问的变量所在的函数就是一个闭包函数)。

闭包案例2

另一个例子:

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

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

闭包案例3

javascript
var fn  =function(){
    var sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn()() //1
fn()() //1
//fn()进行sum变量申明并且返回一个匿名函数,第二个()意思是执行这个匿名函数
var fn  =function(){
    var sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn()() //1
fn()() //1
//fn()进行sum变量申明并且返回一个匿名函数,第二个()意思是执行这个匿名函数

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

那如何不被回收呢?

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

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

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

javascript
var fn  =function(){
    var sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn1=fn() 
fn1()   //1
fn1()   //2
fn1()   //3
fn1 = null // fn1的引用fn被手动释放了
fn1=fn()  //num再次归零
fn1() //1
var fn  =function(){
    var sum = 0
    return function(){
        sum++
        console.log(sum);
    }
}
fn1=fn() 
fn1()   //1
fn1()   //2
fn1()   //3
fn1 = null // fn1的引用fn被手动释放了
fn1=fn()  //num再次归零
fn1() //1

实现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
}
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 {string} array, object, function, null, undefined, string, number
 */
function typeOf(val) {
    return ({}).toString.call(val).slice(8, -1).toLowerCase()
}

function merge(target, obj) {
    for (const p in obj) {
        if (!obj.hasOwnProperty(p)) {
            return
        }
        if (typeOf(target[p]) === 'object' && typeOf(obj[p]) === 'object') {
            merge(target[p], obj[p])
            return
        }
        target[p] = obj[p]
    }
    return target
}
/**
 * 判断类型
 * @param val {any}
 * @returns {string} array, object, function, null, undefined, string, number
 */
function typeOf(val) {
    return ({}).toString.call(val).slice(8, -1).toLowerCase()
}

function merge(target, obj) {
    for (const p in obj) {
        if (!obj.hasOwnProperty(p)) {
            return
        }
        if (typeOf(target[p]) === 'object' && typeOf(obj[p]) === 'object') {
            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())
Array.from(Array(100).keys())

或者

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

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

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

注意,上面的例子里可以认为{ length: 100 }是一种对欺骗的方式,让Array.from认为这是一个类数组。

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方法可以调用多次
         * 比如:
         * 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的指向改变,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);
        }
    }
}
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方法可以调用多次
         * 比如:
         * 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的指向改变,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)
    })
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) {
        this.status = ChainablePromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        // 存储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)
    })
class ChainablePromise {
    static pending = 'pending';
    static fulfilled = 'fulfilled';
    static rejected = 'rejected';

    constructor(executor) {
        this.status = ChainablePromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        // 存储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)
    })

常用的异步处理方法

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

事件循环

微任务和宏任务

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

  • macro-task(宏任务):包括整体代码脚本,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同的任务会进入到不同的event queue。比如setTimeout和setInterval会进入相同的Event Queue。

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

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

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
setTimeout(function() {
    console.log('setTimeout');
})

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. ok,第一轮事件结束,进行第二轮,刚刚我们放在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 中得到同步。

setTimeout的时间误差

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

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

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

sleep(10000000)
setTimeout(() => {
    task()
},3000)

sleep(10000000)

这是为何?

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

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

setTimeout第二个参数最小值

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒, 如果低于这个值,就会自动增加。 在此之前,老版本的浏览器都将最短间隔设为10毫秒。

promise和process.nextTick

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

几个例子

例1

javascript
setTimeout(() => {
    console.log(1)
}, 0)
new Promise((resolve) => {
    resolve()
}).then(() => {
    console.log(2)
})
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)
})()
(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)
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

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

要求

实现一个函数,该函数入参为一个时间戳,返回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}`
}
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}`
}

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

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

柯里化之前的效果:

javascript
// 柯里化之前
function add(a, b, c, d, e) {
    console.log(a + b + c + d + e)
}
add(1, 2, 3, 4, 5)
// 柯里化之前
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
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)
}
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
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

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>
<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;
            }
        }
    }
}
.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;
}
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;
}
.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%);
    }
}
.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;
    }
}
.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;
    }
}
.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;
    }
}
.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;
    }
}
.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;
    }
}
.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;
}
.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>
<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;
    }
}
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:相对自身在文档流中的原始位置进行定位。

static:position默认值,即元素本身在文档流中的默认位置(忽略top、bottom、left、right和z-index声明)。

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

边距塌陷及其修复

竖直方向上相接触的margin-top、margin-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页面的渲染

todo。

常用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">

<!-- Sets whether a web application runs in full-screen mode for iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">

<!--
    Sets the style of the status bar for a web application for iOS.
    This meta tag has no effect unless you first specify full-screen mode
    as described in apple-apple-mobile-web-app-capable
-->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

<!--
    Enables or disables automatic detection of
    possible phone numbers in a webpage in Safari on iOS.
-->
<meta name="format-detection" content="telephone=no">

<!--
    用于设定禁止浏览器从本地机的缓存中调阅页面内容
    设定后一旦离开网页就无法从Cache中再调出
-->
<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/">

<!-- for SEO,其中页面描述应不超过150个字符 -->
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">

<!-- 强制页面在当前窗口以独立页面显示,用来防止别人在框架里调用自己的页面 -->
<meta http-equiv="Window-target" content="_top">
<!-- 设定页面使用的字符集 -->
<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">

<!-- Sets whether a web application runs in full-screen mode for iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">

<!--
    Sets the style of the status bar for a web application for iOS.
    This meta tag has no effect unless you first specify full-screen mode
    as described in apple-apple-mobile-web-app-capable
-->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

<!--
    Enables or disables automatic detection of
    possible phone numbers in a webpage in Safari on iOS.
-->
<meta name="format-detection" content="telephone=no">

<!--
    用于设定禁止浏览器从本地机的缓存中调阅页面内容
    设定后一旦离开网页就无法从Cache中再调出
-->
<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/">

<!-- for SEO,其中页面描述应不超过150个字符 -->
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">

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

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'
        }
    }
}
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 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)
// 还原题目真实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:from general to specific;
  • Bubbling:from specific to general.

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

addEventListener

element.addEventListener(type, listener, useCapture) element.addEventListener(type, listener, options)

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

addEventListener

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

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

removeEventListener

removeEventListener的入参和addEventListener一样。

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

事件代理/委托

事件代理/委托,是靠事件的冒泡机制实现的(所以,对于一些不具有冒泡特性的事件,比如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)
        }
    })
}

// 兼容写法
function delegate(element, targetClass, type, handler) {
    addEvent(element, type, function(e) {
        e = e || window.event
        var target = e.target || e.srcElement
        if (target.className.indexOf(targetClass) !== -1) {
            handler.apply(target, arguments)
        }
    })
}

function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}
// 只考虑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)
        }
    })
}

// 兼容写法
function delegate(element, targetClass, type, handler) {
    addEvent(element, type, function(e) {
        e = e || window.event
        var target = e.target || e.srcElement
        if (target.className.indexOf(targetClass) !== -1) {
            handler.apply(target, arguments)
        }
    })
}

function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}

说明:上面的实现方案中addEvent方法的最后一种实现方式,即target['on' + type]的方式会将之前绑定的事件覆盖掉,是有点问题的。但是考虑到兼容性,一般来说代码是走不到这个地方的,所以也没有问题。

阻止事件传播和默认行为

阻止事件的默认行为

javascript
e = e || window.event
if (e.preventDefault) {
    // none-IE, IE 9&+
    e.preventDefault()
} else {
    // IE 5-8
    e.returnValue = false
}
e = e || window.event
if (e.preventDefault) {
    // none-IE, IE 9&+
    e.preventDefault()
} else {
    // IE 5-8
    e.returnValue = false
}

阻止事件传播

javascript
e = e || window.event
if (e.stopPropagation) {
    e.stopPropagation()
} else {
    // IE 8&-
    e.cancelBubble = true
}
e = e || window.event
if (e.stopPropagation) {
    e.stopPropagation()
} else {
    // IE 8&-
    e.cancelBubble = true
}

stopImmediatePropagation

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

触发顺序

如果同类型事件的几个监听器函数被绑定到了同一个对象上,它们会按照添加的顺序被触发。

stopPropagation和stopImmediatePropagation的区别

  • 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
}
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('#d')
    const getHandler = (elem, useCapture) => {
        return (e) => {
            const target = e.target
            const currentTarget = e.currentTarget
            const payload = {
                elemId: elem.id,
                useCapture,
                targetId: target.id,
                currentTargetId: currentTarget.id,
            }
            console.log(JSON.stringify(payload))
        }
    }
    d.addEventListener('click', () => {
        console.log('冒泡 d1')
    }, { capture: false })
    const elems = [a, b, c, d]
    elems.forEach((elem) => {
        elem.addEventListener('click', getHandler(elem, false), { capture: false })
        elem.addEventListener('click', getHandler(elem, true), { capture: true })
    })
    d.addEventListener('click', () => {
        console.log('捕获 d2')
    }, { capture: true })
    d.addEventListener('click', () => {
        console.log('捕获 d3')
    }, { capture: true })
    d.addEventListener('click', () => {
        console.log('冒泡 d4')
    }, { capture: false })
</script>
</body>
</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('#d')
    const getHandler = (elem, useCapture) => {
        return (e) => {
            const target = e.target
            const currentTarget = e.currentTarget
            const payload = {
                elemId: elem.id,
                useCapture,
                targetId: target.id,
                currentTargetId: currentTarget.id,
            }
            console.log(JSON.stringify(payload))
        }
    }
    d.addEventListener('click', () => {
        console.log('冒泡 d1')
    }, { capture: false })
    const elems = [a, b, c, d]
    elems.forEach((elem) => {
        elem.addEventListener('click', getHandler(elem, false), { capture: false })
        elem.addEventListener('click', getHandler(elem, true), { capture: true })
    })
    d.addEventListener('click', () => {
        console.log('捕获 d2')
    }, { capture: true })
    d.addEventListener('click', () => {
        console.log('捕获 d3')
    }, { capture: true })
    d.addEventListener('click', () => {
        console.log('冒泡 d4')
    }, { capture: false })
</script>
</body>
</html>

点击#d元素,控制台打印内容如下:

text
{"elemId":"a","useCapture":true,"targetId":"d","currentTargetId":"a"}
{"elemId":"b","useCapture":true,"targetId":"d","currentTargetId":"b"}
{"elemId":"c","useCapture":true,"targetId":"d","currentTargetId":"c"}
{"elemId":"d","useCapture":true,"targetId":"d","currentTargetId":"d"}
捕获 d2
捕获 d3
冒泡 d1
{"elemId":"d","useCapture":false,"targetId":"d","currentTargetId":"d"}
冒泡 d4
{"elemId":"c","useCapture":false,"targetId":"d","currentTargetId":"c"}
{"elemId":"b","useCapture":false,"targetId":"d","currentTargetId":"b"}
{"elemId":"a","useCapture":false,"targetId":"d","currentTargetId":"a"}
{"elemId":"a","useCapture":true,"targetId":"d","currentTargetId":"a"}
{"elemId":"b","useCapture":true,"targetId":"d","currentTargetId":"b"}
{"elemId":"c","useCapture":true,"targetId":"d","currentTargetId":"c"}
{"elemId":"d","useCapture":true,"targetId":"d","currentTargetId":"d"}
捕获 d2
捕获 d3
冒泡 d1
{"elemId":"d","useCapture":false,"targetId":"d","currentTargetId":"d"}
冒泡 d4
{"elemId":"c","useCapture":false,"targetId":"d","currentTargetId":"c"}
{"elemId":"b","useCapture":false,"targetId":"d","currentTargetId":"b"}
{"elemId":"a","useCapture":false,"targetId":"d","currentTargetId":"a"}

可以看到:

  • 捕获阶段:先由外至内按捕获的顺序触发了事件回调。
  • 目标阶段:其实就是先捕获后冒泡,在此前提下各自按注册的先后顺序执行。
  • 冒泡阶段:最后由内而外按冒泡的顺序又触发了对应的事件回调。

移动端开发

响应式页面设计的原理

响应式页面设计的原理是让页面根据浏览器屏幕宽度/视口宽度自适应,较理想地呈现出页面内容。

较常见的做法是使用CSS media query, 而且通常会在meta标签中对viewport的宽度等进行设定(比如设定width: device-width)。 但即便不用这种方法,只要页面能根据屏幕宽度做出自适应的调整,那就是响应式设计。

rem布局原理

javascript
function fit () {
  // 750是设计稿的宽度
  const scale = $('body').width() / 750
  // 开发时,以100px对应1rem进行计算
  document.querySelector('html').style.fontSize = 100 * scale + 'px'
}

$(document).ready(() => {
  fit()
  $(window).resize(fit)
})
function fit () {
  // 750是设计稿的宽度
  const scale = $('body').width() / 750
  // 开发时,以100px对应1rem进行计算
  document.querySelector('html').style.fontSize = 100 * scale + 'px'
}

$(document).ready(() => {
  fit()
  $(window).resize(fit)
})

移动端click事件延时

在移动端使用click事件会产生300ms的延迟。

问题的产生:移动端存在双击放大的问题, 所以在移动端点击事件发生时,为了判断用户的行为(到底是要双击还是要点击),浏览器通常会等待300ms, 如果300ms之内,用户没有再次点击,则判定为点击事件,否则判定为双击缩放。

为什么要解决:现代web对性能的极致追求,对用户体验的高标准,让着300ms的卡顿变得难以接受

如何解决:

1、user-scalable:no 禁止缩放——没有缩放就不存在双击,也就没有点击延迟

2、指针事件:CSS:-ms-touch-action:none 点击后浏览器不会启用缩放操作,也就不存在延迟。然而这种方法兼容性很不好。

3、FastClick库:针对这个问题所开发的轻量级库。FastClick在检测到touchend事件后,会立即触发一个模拟的click事件,并把300ms后真正的click事件阻止掉

用法:

javascript
window.addEventListener('load', function () {
  // 虽然可以绑定到更具体的元素,但绑定到body上能使整个应用都受益
  FastClick.attach(document.body)
})
window.addEventListener('load', function () {
  // 虽然可以绑定到更具体的元素,但绑定到body上能使整个应用都受益
  FastClick.attach(document.body)
})

当FastClick检测到页面中使用了user-scalable:no或者touch-action:none方案时,会静默退出。

伪类:active失效

只需给document绑定touchstart或touchend事件即可, 如document.addEventListener('touchstart', function () {}, false)。

更简单的方法是直接在html中body标签上添加属性ontouchstart=""。

格式检测

不让安卓手机识别邮箱:

html
<meta content="email=no" name="format-detection">
<meta content="email=no" name="format-detection">

禁止IOS识别长串数字为电话:

html
<meta content="telephone=no" name="format-detection">
<meta content="telephone=no" name="format-detection">

交互限制

禁止iOS弹出各种操作窗口:-webkit-touch-callout: none;

禁止用户选中文字:-webkit-user-select: none;

input[type="date"]不支持placeholder

html
<input placeholder="占位符" type="text" onfocus="(this.type='date')">
<input placeholder="占位符" type="text" onfocus="(this.type='date')">

iOS部分版本Date构造函数不支持YYYY-MM-DD格式入参

iOS部分版本的Date构造函数不支持规范标准中定义的YYYY-MM-DD格式, 如new Date('2013-11-11')是Invalid Date, 但支持YYYY/MM/DD格式,可用new Date('2013/11/11'); 类似的,对于yyyy-mm-dd hh:mm:ss格式的日期, 可以通过类似下面的方法将其转换为Date对象实例(适用于所有设备):

javascript
// 将形如"yyyy-mm-dd hh:mm:ss"的日期字符串转换为日期对象(兼容IOS设备)
function longStringToDate (dateString) {
  if (dateString && dateString.length === 19) {
    // Attention: there is a space between regular expression
    const tempArr = dateString.split(/[- :]/)
    return new Date(
        tempArr[0],
        tempArr[1] - 1,
        tempArr[2],
        tempArr[3],
        tempArr[4],
        tempArr[5]
    )
  }
  return 'Invalid Date'
}
// 将形如"yyyy-mm-dd hh:mm:ss"的日期字符串转换为日期对象(兼容IOS设备)
function longStringToDate (dateString) {
  if (dateString && dateString.length === 19) {
    // Attention: there is a space between regular expression
    const tempArr = dateString.split(/[- :]/)
    return new Date(
        tempArr[0],
        tempArr[1] - 1,
        tempArr[2],
        tempArr[3],
        tempArr[4],
        tempArr[5]
    )
  }
  return 'Invalid Date'
}

HTTP

HTTP协议的主要特点

  • 简单快速:可以理解为每个资源的URI(统一资源定位符)都是固定的,所以在http协议处理起来比较容易
  • 灵活:每个http协议的头部都有一个类型,通过一个http协议就能完成不同类型的传输,所以比较灵活
  • 无连接(重):http协议连接一次之后就会断开,不会保持连接
  • 无状态(重):可以理解为服务端和客户端是两种身份,单从http协议中是无法区分两次协议者的身份

HTTP报文的组成部分

请求报文:

  • 请求行 --- 包含http方法,页面地址,http协议,http版本
  • 请求头 --- 包含一些key:value的值,eg: host、Cache-Control,Accept,Cookie等
  • 空行 --- 用来告诉服务端往下就是请求体的部分啦
  • 请求体 --- 就是正常的query/body参数

响应报文:

  • 状态行 --- 包含http方法,http协议,http版本,状态码
  • 响应头 --- 包含一些key:value的值,eg: Content-type,Set-cookie, Cache-Control, Date, Server等
  • 空行 --- 用来告诉客户端往下就是响应体的部分啦
  • 响应体 --- 就是服务端返回的数据

HTTP方法

  • GET -- 获取资源
  • POST -- 创建一个新的资源
  • PUT -- 更新资源,常用来做传输文件,更新整个资源对象
  • PATCH -- 更新资源,更新部分属性,例如只更新某个用户的 nickname 属性
  • DELETE -- 删除资源
  • HEAD -- 获取请求报文首部
  • OPTIONS -- 询问支持的方法,查询针对请求URI指定的资源支持的方法,在跨域请求中,由客户端(浏览器)发送

上述方法,只有 GET 和 POST 才能在

表单中使用,

但是现在也有很多公司使用 Only Post 的规则:接口只接收 POST 请求。

GET和POST请求的区别

  • GET产生的URL地址可以被收藏,而POST不可以
  • GET请求会被浏览器主动缓存,而POST不会,除非主动设置
  • GET请求参数会被完整的保留在浏览器历史里,而POST的参数不会被保留
  • GET请求在URL中传输参数有长度限制,而POST没有
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
  • POST比GET更安全,因为GET请求的参数直接暴露在URL上
  • GET参数通过URL传输,而POST参数放在request body中

注意:上面有些说法严格来说也是不对的,因为post请求你可以在url上加query参数,服务端也能获取。

两种方法除了自身的参数限制、缓存限制,通常情况下它们根本的区别:

GET 不会产生副作用,而 POST 会。

常见HTTP状态码

  • 1XX --- 指示信息,表示请求已接受,继续处理
  • 2XX --- 成功,表示请求已被成功接受
    • 200 --- OK,客户端请求成功
    • 206 --- 客户端发送了一个带有Range头的GET请求,视频/音频可能会用到
  • 3XX --- 重定向,要完成请求,必需进行近一步操作
    • 301 --- 重定向,所请求的界面转移到新的url,永久重定向
    • 302 --- 同上301,但是是临时重定向
    • 304 --- 缓存,服务端告诉客户端有缓存可用,不用重新请求
  • 4XX --- 客户端错误,请求有语法错误或请求无法实现
    • 400 --- Bad Request, 客户端请求有语法错误
    • 401 --- Unauthorized, 请求未授权
    • 403 --- Forbidden, 禁止页面访问
    • 404 --- Not found, 请求资源不存在
  • 5XX --- 服务端错误,服务器未能实现合法的请求
    • 500 --- Internal Server Error, 服务器错误
    • 503 --- Server Unavailable, 请求未完成,服务器临时过载或者宕机,一段时间后可恢复正常

持久连接

当使用Keep-alive模式(又称持久连接,连接重用 http1.1的版本才支持)时,Keep-alive功能使客户端到服务端的连接持续有效,当出现服务器的后续请求时,Keep-alive避免了建立或者重新建立连接。

管线化

在使用持久连接的情况下,某个连接上的消息传递类似于:

请求1 --> 响应1 --> 请求2 --> 响应2 --> 请求3 --> 响应3

管线化的连接消息传递是类似于:

请求1 --> 请求2 --> 请求3 --> 响应1 --> 响应2 --> 响应3

相当于客户端一次性把所有的请求打包发送给服务端,同时服务端也一次性打包将所有的返回回传回来

只有GET和HEAD请求可以进行管线化,而POST有所限制

管线化是通过持久连接完成的,且只有http/1.1版本支持

TCP三次握手四次挥手

TCP的特性

  • TCP提供一种面向连接的、可靠的字节流服务
  • 在一个TCP连接中,仅有两方进行彼此通信。广播和多播不能用于TCP
  • TCP使用校验和、确认和重传机制来保证可靠传输
  • TCP给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
  • TCP使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制

注意:TCP并不能保证数据一定会被对方接收到,因为这是不可能的。TCP能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此准确说TCP也不是100%可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。

三次握手与四次挥手

所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。

三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

在socket编程中,客户端执行connect()时。将触发三次握手。

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。

客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

TCP/IP的分层管理

TCP/IP最重要的一个特点就是分层管理,分别为:

  • 应用层:决定向用户提供应用服务时的通信活动,http、ftp、dns 都属于这一层
  • 安全层(TSL/SSL):如果是https的请求会存在这一层,http的请求则无此层,注意!
  • 传输层:传输层对上层应用层提供处于网络连接中两台计算机之间的数据传输
  • 网络层:网络层用来处理网络上流动的数据包,数据包是网络传输的最小数据单位
  • 链路层:用来处理连接网络的硬件部分,包括控制操作系统、硬件的设备驱动等物理可见部分

HTTP缓存控制

浏览器缓存控制分为强缓存和协商缓存,协商缓存必须配合强缓存使用。 首先浏览器第一次跟一个服务器请求一个资源,服务器在返回这个资源和response header的同时, 会根据开发者要求或者浏览器默认,在response的header加上相关字段的http response header。

浏览器在请求已经访问过的URL时,会判断是否使用缓存,判断是否使用缓存主要是判断缓存是否在有效期内。

1.1、当浏览器对某个资源的请求命中了强缓存时,利用[Expires]或者[Cache-Control]这两个http response header实现

  • [Expires]描述的是一个绝对时间,依据的是客户端的时间,用GMT格式的字符串表示,如Expires:Thu,31 Dec 2037 23:55:55 GMT,下次浏览器再次请求同一资源时,会先从客户端缓存中查找,找到这个资源后拿出它的[Expires]与当前时间做比较,如果请求时间在[Expires]规定的有效期内就能命中缓存,这样就不用再次到服务器上去缓存一遍,节省了资源。但正因为是绝对时间,如果客户端的时间被随意篡改,这个机制就失效了,所以我们需要[Cache-Control]。

  • [Cache-Control]描述的是一个相对时间,在进行缓存命中时都是利用浏览器时间判断。

[Expires]和[Cache-Control]这两个header可以只启用一个,也可以同时启用,同时启用时[Cache-Control]优先级高于[Expires]。

1.2、当浏览器对某个资源的请求没有命中强缓存(即缓存过期后),就会发起一个请求到服务器,验证协商缓存是否命中(即验证缓存是否有更新,虽然有效期过了,但我还能继续使用它吗?),如果命中则还是从客户端中加载。协商缓存利用的是[Last-Modified,If-Modified-Since]和[ETag,If-None-Match]这两对header来管理的。

  • [Last-Modified,If-Modified-Since]:原理和上面的[Expires]相同,服务器会响应一个Last-Modified字段,表示最近一次修改缓存的时间,当缓存过期后, 浏览器就会把这个时间放在If-Modified-Since去请求服务器,判断缓存是否有更新。区别是它是根据服务器时间返回的header来判断缓存是否存在,但有时候也会出现服务器上资源有变化但修改时间没有变化的情况,这种情况我们就需要[ETag,If-None-Match]。

  • [ETag,If-None-Match]:原理与上面相同,区别是浏览器向服务器请求一个资源,服务器在返回这个资源的同时,在response的header中加上一个Etag字段,这个字段是服务器根据当前请求的资源生成的唯一标识字符串,只有资源有变化这个串就会发生改动。当缓存过期后,浏览器会把这个字符串放在If-None-Match去请求服务器,比较字符串的值判断是否有更新,Etag的优先级比Last-Modified的更高, Etag的出现是为了解决一个缓存文件在短时间内被多次修改的问题,因为Last-Modified只能精确到秒

  • [ETag,If-None-Match]这么厉害我们为什么还需要[Last-Modified,If-Modified-Since]呢?有一个例子就是,分布式系统尽量关掉ETag,因为每台机器生成的ETag不一样,[Last-Modified,If-Modified-Since]和[ETag,If-None-Match]一般都是同时启用。

常用场景举例:

前端 SPA 应用发布后,为了保证客户端总能直接加载最新的静态资源文件,结合 nginx.conf 对缓存做出如下配置:

nginx
server {
  listen 80;
  server_name localhost;
  root /usr/share/app;
  index index.php index.html index.htm;

  location / {
    try_files $uri $uri/ /index.html;
  }

  # 静态资源文件 缓存30天;
  location ~ \.(css|js|gif|jpg|jpeg|png|bmp|swf|ttf|woff|otf|ttc|pfa)$ {
    expires 30d;
  }

  # `html` 不缓存
  location ~ \.(html|htm)$ {
    add_header Cache-Control "no-store, no-cache, must-relalidate";
  }
}
server {
  listen 80;
  server_name localhost;
  root /usr/share/app;
  index index.php index.html index.htm;

  location / {
    try_files $uri $uri/ /index.html;
  }

  # 静态资源文件 缓存30天;
  location ~ \.(css|js|gif|jpg|jpeg|png|bmp|swf|ttf|woff|otf|ttc|pfa)$ {
    expires 30d;
  }

  # `html` 不缓存
  location ~ \.(html|htm)$ {
    add_header Cache-Control "no-store, no-cache, must-relalidate";
  }
}

同时为 index.html 增加缓存相关的 <meta /> 标签:

html
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="expires" content="0">

303 See Other

303 状态码表示服务器要将浏览器重定向到另一个资源,这个资源的 URI 会被写在响应 Header 的 Location 字段。从语义上讲,重定向到的资源并不是你所请求的资源,而是对你所请求资源的一些描述。

303 常用于将 POST 请求重定向到 GET 请求,比如你上传了一份个人信息,服务器发回一个 303 响应,将你导向一个“上传成功”页面。

不管原请求是什么方法,重定向请求的方法都是 GET(或 HEAD,不常用)。

304 Not Modified

HTTP响应码 304 Not Modified 表明不需要传输所请求的资源。它会将请求重定向到已有的缓存资源上。

当请求是GET或者HEAD这种安全请求,会出现这种情况。

开发者本地开发时经常会看到很多304请求,它们实际就是去访问本地的缓存资源的。

Mixed Content

当用户访问一个HTTPS网页时,他们与服务端之间的连接是经过TLS加密的,相对更安全。

如果HTTPS网页中包含的部分内容触发了明文HTTP请求,就会导致部分内容未被加密,也就不安全了。这种页面称为mixed content page

REST接口设计的5条指导规则

  1. 不要使用物理地址作为url。比如http://www.acme.com/inventory/product003.xml应更换为http://www.acme.com/inventory/product/003

  2. 不要返回过多的数据。比如,产品列表查询接口应返回前几条产品数据,而非全部的产品列表数据。数据较多时,可以考虑分页。

  3. 接口文档应书写清晰,不要随意改动,避免影响已有的客户端。

  4. 应返回实际地址而非让客户端根据id等自行去拼接。

  5. GET请求不应导致服务端状态的变动。

JS内存回收机制

由于字符串、对象和数组没有固定大小,当他们的大小已知时,才能对他们进行动态的存储分配。 JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。 只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用, 否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。

标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境时,就标记这个变量为“进入环境”。 从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。 当变量离开环境时,则将其标记为“离开环境”。

引用计数

引用计数的含义是跟踪记录每个值被引用的次数。 当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。 相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。 当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。 这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

该策略容易在循环引用时出现问题。 因为计数记录的是被引用的次数,所以循环引用时计数并不会消除。导致无法释放内存。 IE 9 - 的问题是在环境中bom和dom不是原生的js对象,而是com对象,而com对象的垃圾收集机制是引用计数策略。 换句话说,只要ie中存在着com对象,就会存在循环引用的问题。比如

javascript
var element=document.getElementById("someElement");
var myobject=new Object();
myobject.element=element;
element.someObject=myobject;
var element=document.getElementById("someElement");
var myobject=new Object();
myobject.element=element;
element.someObject=myobject;

这个例子中js对象和dom对象之间建立了循环引用,由于存在这个循环引用,即使将com对象从页面移除,也永远不会被回收。为避免类似问题,应该在不使用它们的时候手动把js对象和com对象断开。

javascript
myobject.element = null;
element.someObject = null;
myobject.element = null;
element.someObject = null;

如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null即可。将变量设置为null意味着切断变量与它此前引用的值之间的联系。当垃圾收集器下次运行时,就会删除这些值并回收他们占用的内存。

内存溢出

内存溢出一般是指执行程序时,程序会向系统申请一定大小的内存,当系统现在的实际内存少于需要的内存时,就会造成内存溢出。

内存溢出造成的结果是先前保存的数据会被覆盖或者后来的数据会没地方存。

内存泄漏

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。 内存泄漏是指程序执行时,一些变量没有及时释放,一直占用着内存 而这种占用内存的行为就叫做内存泄漏。

作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积。

内存泄漏如果一直堆积,最终会导致内存溢出问题。

内存泄漏发生的原因

全局变量

除了常规设置了比较大的对象在全局变量中,还可能是意外导致的全局变量,如:

javascript
function foo(arg) {
    bar = "this is a hidden global variable";
}
function foo(arg) {
    bar = "this is a hidden global variable";
}

在函数中,没有使用 var/let/const 定义变量,这样实际上是定义在window上面,变成了window.bar。再比如由于this导致的全局变量:

javascript
function foo() {    
    this.bar = "this is a hidden global variable";
}
foo()
function foo() {    
    this.bar = "this is a hidden global variable";
}
foo()

这种函数,在window作用域下被调用时,函数里面的this指向了window,执行时实际上为window.bar=xxx,这样也产生了全局变量。

计时器中引用没有清除

先看如下代码:

javascript
var someData = getData();
setInterval(function() {    
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData);
    }
}, 1000);
var someData = getData();
setInterval(function() {    
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData);
    }
}, 1000);

这里定义了一个计时器,每隔1s把一些数据写到Node节点里面。但是当这个Node节点被删除后,这里的逻辑其实都不需要了,可是这样写,却导致了计时器里面的回调函数无法被回收,同时,someData里的数据也是无法被回收的。

闭包

javascript
var theThing = null;
var replaceThing = function () {  
    var originalThing = theThing;  
    var unused = function () {
       if (originalThing)  console.log("hi");
    };
    theThing = {
        longStr: new Array(1000000).join('*'), 
        someMethod: function () {
           console.log(originalThing);
        }
    };
};
setInterval(replaceThing, 1000);
var theThing = null;
var replaceThing = function () {  
    var originalThing = theThing;  
    var unused = function () {
       if (originalThing)  console.log("hi");
    };
    theThing = {
        longStr: new Array(1000000).join('*'), 
        someMethod: function () {
           console.log(originalThing);
        }
    };
};
setInterval(replaceThing, 1000);

当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。

闭包与内存泄漏

内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。闭包里面的变量就是我们需要的变量,不能说是内存泄露。

闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。

局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript的问题。在 IE浏览器中,由于BOM和DOM中的对象是使用 C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。

同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null即可。将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

内存泄漏的识别方法

利用chrome 时间轴记录可视化内存泄漏

Performance(时间轴)能够面板直观实时显示JS内存使用情况、节点数量、监听器数量等。

打开 chrome 浏览器,调出调试面板(DevTools),点击Performance选项(低版本是Timeline),勾选Memory复选框。一种比较好的做法是使用强制垃圾回收开始和结束记录。在记录时点击 Collect garbage 按钮 (强制垃圾回收按钮) 可以强制进行垃圾回收。所以录制顺序可以这样:开始录制前先点击垃圾回收-->点击开始录制-->点击垃圾回收-->点击结束录制。面板介绍如图:

录制结果如图:

首先,从图中我们可以看出不同颜色的曲线代表的含义,这里主要关注JS堆内存、节点数量、监听器数量。鼠标移到曲线上,可以在左下角显示具体数据。在实际使用过程中,如果您看到这种 JS 堆大小或节点大小不断增大的模式,则可能存在内存泄漏。

使用堆快照发现已分离 DOM 树的内存泄漏

只有页面的 DOM 树或 JavaScript 代码不再引用 DOM 节点时,DOM 节点才会被作为垃圾进行回收。如果某个节点已从 DOM 树移除,但某些 JavaScript 仍然引用它,我们称此节点为“已分离”,已分离的 DOM 节点是内存泄漏的常见原因。

同理,调出调试面板,点击Memory,然后选择Heap Snapshot,然后点击进行录制。录制完成后,选中录制结果,在 Class filter 文本框中键入 Detached,搜索已分离的 DOM 树。以这段代码为例:

html
<html>
    <head></head>
    <body>
        <button id="createBtn">增加节点</button>
        <script> 
            var detachedNodes;
            function create() {
                var ul = document.createElement('ul');
                for (var i = 0; i < 10; i++) {    
                    var li = document.createElement('li');
                    ul.appendChild(li);
                }
                detachedTree = ul;
            }
            document.getElementById('createBtn').addEventListener('click', create);
        </script>
    </body>
</html>
<html>
    <head></head>
    <body>
        <button id="createBtn">增加节点</button>
        <script> 
            var detachedNodes;
            function create() {
                var ul = document.createElement('ul');
                for (var i = 0; i < 10; i++) {    
                    var li = document.createElement('li');
                    ul.appendChild(li);
                }
                detachedTree = ul;
            }
            document.getElementById('createBtn').addEventListener('click', create);
        </script>
    </body>
</html>

点击几下,然后记录。可以得到以下信息:

如上图,点开节点,可以看到下面的引用信息,上面可以看出,有个HTMLUListElement(ul节点)被window.detachedTree 引用。再结合代码,原来是没有加var/let/const声明,导致其成了全局变量,所以DOM无法释放。

避免内存泄漏的方法

1、少用全局变量,避免意外产生全局变量

2、使用闭包要及时注意,有Dom元素的引用要及时清理。

3、计时器里的回调没用的时候要记得销毁。

4、为了避免疏忽导致的遗忘,我们可以使用 WeakSet 和 WeakMap结构,它们对于值的引用都是不计入垃圾回收机制的,表示这是弱引用。举个例子:

javascript
const wm = new WeakMap();
const element = document.getElementById('example');
 
wm.set(element, 'some information');
wm.get(element) // "some information"
const wm = new WeakMap();
const element = document.getElementById('example');
 
wm.set(element, 'some information');
wm.get(element) // "some information"

这种情况下,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

Vue2

Vue2的响应式原理

所谓响应式就是首先建立响应式数据和依赖之间的关系,当这些响应式数据发生变化的时候, 可以通知那些绑定这些数据的依赖进行相关操作,可以是 DOM 更新,也可以是执行一个回调函数。

我们知道 Vue2 的对象数据是通过 Object.defineProperty 对每个属性进行监听, 当对属性进行读取的时候,就会触发 getter,对属性进行设置的时候,就会触发 setter。

javascript
/**
 * 这里的函数 defineReactive 用来对 Object.defineProperty 进行封装。
 */
function defineReactive(data, key, val) {
   // 依赖存储的地方
   const dep = new Dep()
   Object.defineProperty(data, key, {
       enumerable: true,
       configurable: true,
       get: function () {
           // 在 getter 中收集依赖
           dep.depend()
           return val
       },
       set: function(newVal) {
           val = newVal
           // 在 setter 中触发依赖
           dep.notify()
       }
   }) 
}
/**
 * 这里的函数 defineReactive 用来对 Object.defineProperty 进行封装。
 */
function defineReactive(data, key, val) {
   // 依赖存储的地方
   const dep = new Dep()
   Object.defineProperty(data, key, {
       enumerable: true,
       configurable: true,
       get: function () {
           // 在 getter 中收集依赖
           dep.depend()
           return val
       },
       set: function(newVal) {
           val = newVal
           // 在 setter 中触发依赖
           dep.notify()
       }
   }) 
}

那么是什么地方进行属性读取呢?就是在 Watcher 里面,Watcher 也就是所谓的依赖。 在 Watcher 里面读取数据的时候,会把自己设置到一个全局的变量中。

javascript
/**
 * 我们所讲的依赖其实就是 Watcher,
 * 我们要通知用到数据的地方,而使用这个数据的地方有很多,
 * 类型也不一样,有可能是组件的,有可能是用户写的watch,
 * 所以我们需要抽象出一个能集中处理这些情况的类。
 */
class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm
        this.getter = exp
        this.cb = cb
        this.value = this.get()
    }

    get() {
        Dep.target = this
        let value = this.getter.call(this.vm, this.vm)
        Dep.target = undefined
        return value
    }

    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}
/**
 * 我们所讲的依赖其实就是 Watcher,
 * 我们要通知用到数据的地方,而使用这个数据的地方有很多,
 * 类型也不一样,有可能是组件的,有可能是用户写的watch,
 * 所以我们需要抽象出一个能集中处理这些情况的类。
 */
class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm
        this.getter = exp
        this.cb = cb
        this.value = this.get()
    }

    get() {
        Dep.target = this
        let value = this.getter.call(this.vm, this.vm)
        Dep.target = undefined
        return value
    }

    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

在 Watcher 读取数据的时候也就触发了这个属性的监听 getter, 在 getter 里面就需要进行依赖收集,这些依赖存储的地方就叫 Dep, 在 Dep 里面就可以把全局变量中的依赖进行收集,收集完毕就会把全局依赖变量设置为空。 将来数据发生变化的时候,就去 Dep 中把相关的 Watcher 拿出来执行一遍。

javascript
/**
* 我们把依赖收集的代码封装成一个 Dep 类,它专门帮助我们管理依赖。
* 使用这个类,我们可以收集依赖、删除依赖或者向依赖发送通知等。
**/
class Dep {
    constructor() {
        this.subs = []
    }
    
    addSub(sub) {
        this.subs.push(sub)
    }
    
    removeSub(sub) {
        remove(this.subs, sub)
    }

    depend() {
        if(Dep.target){
            this.addSub(Dep.target)
        }
    }

    notify() {
        const subs = this.subs.slice()
        for(let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

// 删除依赖
function remove(arr, item) {
    if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1){
            return arr.splice(index, 1)
        } 
    }
}
/**
* 我们把依赖收集的代码封装成一个 Dep 类,它专门帮助我们管理依赖。
* 使用这个类,我们可以收集依赖、删除依赖或者向依赖发送通知等。
**/
class Dep {
    constructor() {
        this.subs = []
    }
    
    addSub(sub) {
        this.subs.push(sub)
    }
    
    removeSub(sub) {
        remove(this.subs, sub)
    }

    depend() {
        if(Dep.target){
            this.addSub(Dep.target)
        }
    }

    notify() {
        const subs = this.subs.slice()
        for(let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

// 删除依赖
function remove(arr, item) {
    if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1){
            return arr.splice(index, 1)
        } 
    }
}

总的来说就是通过 Object.defineProperty 监听对象的每一个属性,当读取数据时会触发 getter,修改数据时会触发 setter。

然后我们在 getter 中进行依赖收集,当 setter 被触发的时候,就去把在 getter 中收集到的依赖拿出来进行相关操作,通常是执行一个回调函数。

我们收集依赖需要进行存储,对此 Vue2 中设置了一个 Dep 类,相当于一个管家,负责添加或删除相关的依赖和通知相关的依赖进行相关操作。

在 Vue2 中所谓的依赖就是 Watcher。 值得注意的是,只有 Watcher 触发的 getter 才会进行依赖收集,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中。 当响应式数据发生改变的时候,就会把收集到的 Watcher 都进行通知。

由于 Object.defineProperty 无法监听对象的变化, 所以 Vue2 中设置了一个 Observer 类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。

为什么 Vue2 新增响应式属性要通过额外的 API?

这是因为 Object.defineProperty 只会对属性进行监测,而不会对对象进行监测, 为了可以监测对象 Vue2 创建了一个 Observer 类。 Observer 类的作用就是把一个对象全部转换成响应式对象,包括子属性数据, 当对象新增或删除属性的时候负债通知对应的 Watcher 进行更新操作。

javascript
// 定义一个属性
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

class Observer {
    constructor(value) {
        this.value = value
        // 添加一个对象依赖收集的选项
        this.dep = new Dep()
        // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
        def(value, '__ob__', this)
        if(Array.isArray(value)) {
           
        } else {
            this.walk(value)
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj)
        // 遍历对象的属性进行响应式设置
        for(let i = 0; i < keys.length; i ++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
// 定义一个属性
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

class Observer {
    constructor(value) {
        this.value = value
        // 添加一个对象依赖收集的选项
        this.dep = new Dep()
        // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
        def(value, '__ob__', this)
        if(Array.isArray(value)) {
           
        } else {
            this.walk(value)
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj)
        // 遍历对象的属性进行响应式设置
        for(let i = 0; i < keys.length; i ++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

vm.$set 的实现原理

javascript
function set(target, key, val) {
    const ob = target.__ob__
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}
function set(target, key, val) {
    const ob = target.__ob__
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}

当向一个响应式对象新增属性的时候,需要对这个属性重新进行响应式的设置, 即使用 defineReactive 将新增的属性转换成 getter/setter。

我们在前面讲过每一个对象是会通过 Observer 类型进行包装的, 并在 Observer 类里面创建一个属于这个对象的依赖收集存储对象 dep, 最后在新增属性的时候就通过这个依赖对象进行通知相关 Watcher 进行变化更新。

vm.$delete 的实现原理

javascript
function del(target, key) {
    const ob = target.__ob__
    delete target[key]
    ob.dep.notify()
}
function del(target, key) {
    const ob = target.__ob__
    delete target[key]
    ob.dep.notify()
}

我们可以看到 vm.$delete 的实现原理和 vm.$set 的实现原理是非常相似的。

通过 vm.$delete 和 vm.$set 的实现原理,我们可以更加清晰地理解到 Observer 类的作用, Observer 类就是给一个对象也进行一个监测,因为 Object.defineProperty 是无法实现对对象的监测的, 但这个监测是手动,不是自动的。 获得授权,非商业转载请注明出处。

Object.defineProperty 真的不能监听数组的变化吗?

面试官一上来可能先问你 Vue2 中数组的响应式原理是怎么样的,这个问题你也许会觉得很容易回答, Vue2 对数组的监测是通过重写数组原型上的 7 个方法来实现,然后你会说具体的实现, 接下来面试官可能会问你,为什么要改写数组原型上的 7 个方法,而不使用 Object.defineProperty, 是因为 Object.defineProperty 真的不能监听数组的变化吗?

其实 Object.defineProperty 是可以监听数组的变化的。

javascript
const arr = [1, 2, 3]
arr.forEach((val, index) => {
    Object.defineProperty(arr, index, {
        get() {
            console.log('监听到了')
            return val
        },
        set(newVal) {
            console.log('变化了:', val, newVal)
            val = newVal
        }
    })
})
const arr = [1, 2, 3]
arr.forEach((val, index) => {
    Object.defineProperty(arr, index, {
        get() {
            console.log('监听到了')
            return val
        },
        set(newVal) {
            console.log('变化了:', val, newVal)
            val = newVal
        }
    })
})

其实数组就是一个特殊的对象,它的下标就可以看作是它的 key。

所以 Object.defineProperty 也能监听数组变化,那么为什么 Vue2 弃用了这个方案呢?

首先这种直接通过下标获取数组元素的场景就比较少, 其次即便通过了 Object.defineProperty 对数组进行监听,但也监听不了 push、pop、shift 等对数组进行操作的方法, 所以还是需要通过对数组原型上的那 7 个方法进行重写监听。 所以为了性能考虑 Vue2 直接弃用了使用 Object.defineProperty 对数组进行监听的方案。

Vue2 中是怎么监测数组的变化的?

通过上文我们知道如果使用 Object.defineProperty 对数组进行监听, 当通过 Array 原型上的方法改变数组内容的时候是无发触发 getter/setter 的, Vue2 中是放弃了使用 Object.defineProperty 对数组进行监听的方案, 而是通过对数组原型上的 7 个方法进行重写进行监听的。

原理就是使用拦截器覆盖 Array.prototype,之后再去使用 Array 原型上的方法的时候, 其实使用的是拦截器提供的方法,在拦截器里面才真正使用原生 Array 原型上的方法去操作数组。

拦截器

javascript
// 拦截器其实就是一个和 Array.prototype 一样的对象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 缓存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最终还是使用原生的 Array 原型方法去操作数组
            return original.apply(this, args)
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})
// 拦截器其实就是一个和 Array.prototype 一样的对象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 缓存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最终还是使用原生的 Array 原型方法去操作数组
            return original.apply(this, args)
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})

所以通过拦截器之后,我们就可以追踪到数组的变化了,然后就可以在拦截器里面进行依赖收集和触发依赖了。

接下来我们就使用拦截器覆盖那些进行了响应式处理的 Array 原型,数组也是一个对象, 通过上文我们可以知道 Vue2 是在 Observer 类里面对对象的进行响应式处理,并且给对象也进行一个依赖收集。 所以对数组的依赖处理也是在 Observer 类里面。

javascript
class Observer {
    constructor(value) {
        this.value = value
        // 添加一个对象依赖收集的选项
        this.dep = new Dep()
        // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
        def(value, '__ob__', this)
        // 如果是数组则通过覆盖数组的原型方法进行拦截操作
        if(Array.isArray(value)) {
          value.__proto__ = arrayMethods 
        } else {
            this.walk(value)
        }
    }
    // ...
}
class Observer {
    constructor(value) {
        this.value = value
        // 添加一个对象依赖收集的选项
        this.dep = new Dep()
        // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
        def(value, '__ob__', this)
        // 如果是数组则通过覆盖数组的原型方法进行拦截操作
        if(Array.isArray(value)) {
          value.__proto__ = arrayMethods 
        } else {
            this.walk(value)
        }
    }
    // ...
}

在这个地方 Vue2 会进行一些兼容性的处理, 如果能使用 __proto__ 就覆盖原型, 如果不能使用,则直接把那 7 个操作数组的方法直接挂载到需要被进行响应式处理的数组上, 因为当访问一个对象的方法时,只有这个对象自身不存在这个方法,才会去它的原型上查找这个方法。

数组如何收集依赖呢?

我们知道在数组进行响应式初始化的时候会在 Observer 类里面给这个数组对象的添加一个 __ob__ 的属性, 这个属性的值就是 Observer 这个类的实例对象,而这个 Observer 类里面有存在一个收集依赖的属性 dep, 所以在对数组里的内容通过那 7 个方法进行操作的时候,会触发数组的拦截器, 那么在拦截器里面就可以访问到这个数组的 Observer 类的实例对象,从而可以向这些数组的依赖发送变更通知。

javascript
// 拦截器其实就是一个和 Array.prototype 一样的对象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 缓存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最终还是使用原生的 Array 原型方法去操作数组
            const result = original.apply(this, args)
            // 获取 Observer 对象实例
            const ob = this.__ob__
            // 通过 Observer 对象实例上 Dep 实例对象去通知依赖进行更新
            ob.dep.notify()
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})
// 拦截器其实就是一个和 Array.prototype 一样的对象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 缓存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最终还是使用原生的 Array 原型方法去操作数组
            const result = original.apply(this, args)
            // 获取 Observer 对象实例
            const ob = this.__ob__
            // 通过 Observer 对象实例上 Dep 实例对象去通知依赖进行更新
            ob.dep.notify()
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})

因为 Vue2 的实现方法决定了在 Vue2 中对数组的一些操作无法实现响应式操作,例如:

javascript
this.list[0] = xxx
this.list[0] = xxx

由于 Vue2 放弃了 Object.defineProperty 对数组进行监听的方案,所以通过下标操作数组是无法实现响应式操作的。

又例如:

javascript
this.list.length = 0
this.list.length = 0

这个动作在 Vue2 中也是无法实现响应式操作的。

数组依赖的收集

其实不管是对象还是数组的依赖都是在 getter 中进行依赖收集的。

例如:

text
{ list: [1,2,3,4] }
{ list: [1,2,3,4] }

你要获取到 list 数组的内容,首先是通过 list 这个 key 进行获取的, 所以当通过 list 这个 key 进行获取数组内容的时候,就触发了 list 这个 key 的 getter。

在 getter 中会进行 key 的依赖收集,收集到的依赖保存在对应 key 的 Dep 对象中, 同时也会判断 key 的值,如果是一个对象,还会对这个对象进行依赖收集, 收集到的依赖则保存在这个对象的 __ob__ 属性对象上的 Dep 上, 而这个 __ob__ 属性对象就是上文中提到的 Observer 类, 在 Observer 类中也有一个 Dep 属性用于专门保存响应式对象的依赖的。 这样无论是对象还是数组数据都可以通过 __ob__ 属性拿到 Observer 实例, 然后拿到 Observer 实例中的 dep。然后就可以进行相关的响应式依赖通知操作了。

实现Vue的数据双向绑定

要求

html
<div id="app">
    <h3>数据的双向绑定</h3>
    <div class="cell">
        <div class="text" v-text="myHello"></div>
        <input class="input" type="text" v-model="myHello" >
        <div class="text" v-text="myWorld"></div>
        <input class="input" type="text" v-model="myWorld" >
    </div>
</div>
<div id="app">
    <h3>数据的双向绑定</h3>
    <div class="cell">
        <div class="text" v-text="myHello"></div>
        <input class="input" type="text" v-model="myHello" >
        <div class="text" v-text="myWorld"></div>
        <input class="input" type="text" v-model="myWorld" >
    </div>
</div>

要求实现一个SimpleVue类,使得执行下面的代码可以实现双向数据绑定效果。

javascript
// 创建SimpleVue实例
const app = new SimpleVue({
    el : '#app' ,
    data : {
        myHello : 'hello',
        myWorld : 'world',
    }
})
// 创建SimpleVue实例
const app = new SimpleVue({
    el : '#app' ,
    data : {
        myHello : 'hello',
        myWorld : 'world',
    }
})

实现

javascript
class SimpleVue {
    constructor (options){
        // 传入的配置参数
        this.options = options;
        // 根元素
        this.$el = document.querySelector(options.el);
        // 数据域
        this.$data = options.data;

        /**
         * 保存数据model与view相关的指令,
         * 当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
         */
        this._directives = {};
        // 数据劫持,重新定义数据的 set 和 get 方法
        this._obverse(this.$data);
        /**
         * 解析器,解析模板指令,
         * 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,
         * 一旦数据有变动,收到通知,更新视图
         */
        this._compile(this.$el);
    }

    // _obverse 函数,对data进行处理,重写data的set和get函数
    _obverse(data) {
        // 遍历数据
        for (const key in data) {
            // 判断是不是属于自己本身的属性
            if (!data.hasOwnProperty(key)) {
                continue
            }

            /**
             * 这里的key是一种简单处理,
             * 其实在后续递归调用this._obverse时,是可能会出现重复的key的
             */
            this._directives[key] = [];

            let val = data[key];

            // 如果是对象,则继续递归遍历(这里简化了,只考虑对象这种单一场景)
            if (typeof val === 'object') {
                this._obverse(val);
            }

            // 初始当前数据的执行队列
            let _dir = this._directives[key];

            // 重新定义数据的 get 和 set 方法
            Object.defineProperty(this.$data, key, {
                enumerable: true,
                configurable: true,
                get: function () {
                    return val;
                },
                set: function (newVal) {
                    if (val !== newVal) {
                        val = newVal;
                        /**
                         * 当 myHello等指令值改变时,
                         * 触发 _directives 中的绑定的Watcher类的更新
                         */
                        _dir.forEach((item) => {
                            //调用自身指令的更新操作
                            item._update();
                        })
                    }
                }
            })
        }
    }

    /**
     * 接着我们来看看 _compile 这个方法,它实际上是一个解析器,
     * 其功能就是解析模板指令,并将每个指令对应的节点绑定更新函数,
     * 添加监听数据的订阅者,一旦数据有变动,就收到通知,
     * 然后去更新视图变化,具体实现如下:
     *
     * 我们从根元素 #app 开始递归遍历每个节点,
     * 并判断每个节点是否有对应的指令,这里我们只针对 v-text 和 v-model,
     * 我们对 v-text 进行了一次 new Watcher(),
     * 并把它放到了 myHello 的指令集里面,对 v-model 也进行了解析,
     * 对其所在的 input 绑定了 input 事件,并将其通过 new Watcher() 与 myHello 关联起来
     */
    _compile(el){
        // 子元素
        const nodes = el.children;
        for (let i = 0, len = nodes.length;  i < len; i++){
            const node = nodes[i];

            // 递归:对所有子元素进行遍历,并进行处理
            if (node.children.length) {
                this._compile(node);
            }

            // 如果有 v-text 指令 , 监控 node的值 并及时更新
            if (node.hasAttribute('v-text')) {
                const attrValue = node.getAttribute('v-text');
                // 将指令对应的执行方法放入指令集
                this._directives[attrValue].push(
                    new Watcher('text', node, this, attrValue, 'innerHTML')
                )
            }

            /**
             * 如果有 v-model属性,
             * 并且元素是INPUT或者TEXTAREA,
             * 我们监听它的input事件
             */
            if (
                node.hasAttribute('v-model') &&
                ['INPUT', 'TEXTAREA'].includes(node.tagName)
            ){
                const _this = this;
                const attrValue = node.getAttribute('v-model');
                // 初始化赋值
                _this._directives[attrValue].push(
                    new Watcher('input', node, _this, attrValue, 'value')
                );
                // 添加input事件监听
                node.addEventListener('input', () => {
                    // 后面每次都会更新
                    _this.$data[attrValue] = node.value;
                }, { capture: false })
            }
        }
    }
}

/**
 * Watcher 其实就是订阅者,
 * 是 _observer 和 _compile 之间通信的桥梁,
 * 用来绑定更新函数,
 * 实现对 DOM 元素的更新。
 *
 * 每次创建 Watcher 的实例,都会传入相应的参数,
 * 也会进行一次 _update 操作,上述的 _compile 中,
 * 我们创建了两个 Watcher 实例,不过这两个对应的 _update 操作不同而已,
 * 对于 div.text 的操作其实相当于 div.innerHTML=h3.innerHTML = this.data.myHello,
 * 对于 input 相当于 input.value=this.data.myHello,
 * 这样每次数据 set 的时候,我们会触发两个 _update 操作,
 * 分别更新 div 和 input 中的内容~
 */
class