Skip to content

原型链&作用域链

原型链:

  • 构造函数、原型和实例的关系:

    1. 对象是通过函数创建的,函数是对象 Object.__proto__ === Function.prototype, Function.__proto__ === Function.prototype
    2. 每个函数都有 prototype 属性,除了用这个方法创建的函数 let fun = Function.prototype.bind()
    3. 每个对象都有一个隐藏属性 __proto__,这个属性指向了创建这个对象的构造函数的原型(这是一个非标准属性,不可以用于编程,是供浏览器自己使用的)
    4. prototype 属性里面默认有一个 constructor 属性,该属性指向原型对象所属的构造函数
  • prototype__proto__ 的关系

    1. prototype 是构造函数的属性
    2. __proto__ 是实例对象的属性 这两者都指向同一个对象
  • 属性搜索

    1. 在访问对象的某个成员的时候会先在对象中查找是否存在
    2. 如果当前对象中没有就在构造函数的原型对象中找
    3. 如果构造函数的原型对象中没有找到就到原型对象的原型上找
    4. 直到 Object 的原型对象的原型是 null 为止

new 的实现过程:

  1. 声明一个中间对象(新生成一个对象)
  2. 将该中间对象的原型指向构造函数的原型(链接到原型)
  3. 将构造函数的 this,指向该中间对象(绑定 this
  4. 返回该中间对象,即返回实例对象(返回新对象)

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

js
function xnew() {
	// 创建一个空的对象
	let obj = new Object()
	// 获得构造函数
	let Con = [].shift.call(arguments)
	// 链接到原型
	obj.__proto__ = Con.prototype
	// 绑定 this,执行构造函数
	let result = Con.apply(obj, arguments)
	// 确保 new 出来的是个对象
	return typeof result === 'object' ? result : obj
}

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

js
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()

作用域链:

作用域链是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止。当函数中需要查询一个变量的值的时候,js 解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续,如果找到最后也没找到需要的变量,则解释器返回 undefined

Ajax 的原生写法

js
const ajax = {
    get: function (url, options) {
        const xhr = new XMLHttpRequest();
        let params = options.params || {};

        for (let key in params) {
            if (!params.hasOwnProperty(key)) return;
            url += (/\?/.test(url) ? '&' : '?') + key + '=' + params[key];
        }

        // 若需传参,则再 open 之前完成相关 data 序列化
        xhr.open('GET', url, true);

        // setRequestHeader 方法需在 open() 之后,send() 之前调用
        xhr.setRequestHeader('token', options.headers.token);

        xhr.onreadystatechange = function () {
            if (
                xhr.readyState === 4
                    && (xhr.status === 200 || xhr.status === 304)
            ) {
                options.callback && options.callback.call(this, xhr.responseText);
            }
        }

        xhr.send();
    },
    post: function (url, options) {
        const xhr = new XMLHttpRequest();

        // 语法:xhrReq.open(method, url, async, user, password);
        xhr.open('POST', url, true);

        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

        xhr.onreadystatechange = function () {
            if (
                xhr.readyState === 4
                    && (xhr.status === 200 || xhr.status === 304)
            ) {
                options.callback && options.callback.call(this, xhr.responseText);
            }
        }

        xhr.send(options.data || {});
    }
}

数据请求方法 ajax, fetch, axios 有什么区别

axios = (ES6 Promise) + (http in node || ajax in browser) + (json automation) + (polyfill for browser)

fetch = Not ready fully in H5, but it is future.

fetch 的优点和缺点

优点:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 同构方便,更加底层,提供的 API 丰富(request, response, body , headers)
  • 脱离了 XHR,是 ES 规范里新的实现方式

缺点:

  • fetch 只对网络请求报错,对 400/500 都当做成功的请求,服务器返回 400/500 错误码时并不会 reject
  • fetch 默认不会带 cookie,需要添加配置项 credentials: 'include'
  • fetch 不支持 abort,不支持超时控制,造成了流量的浪费
  • fetch 没有办法原生监测请求的进度,而 XHR 可以

参考资料

懒加载、懒执行、预加载、预渲染

懒加载:

懒加载就是将不关键的资源延后加载。

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。

懒执行:

懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。

预加载:

在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。

预加载其实是声明式的 fetch,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载:

html
<link rel="preload" href="http://example.com" />

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

预渲染:

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染:

html
<link rel="prerender" href="http://example.com" />

预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染

懒加载和预加载的对比:

概念:

  • 懒加载:也叫延迟加载,JS 延迟图片/视频等资源的加载(或符合某些条件时才加载)
  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染

区别: 两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载,懒加载对服务器前端有一定的缓解压力作用,预加载则是增加服务器前端压力来换取更好的用户体验。

实现方式:

  • 懒加载
    • setTimeout, setInterval 延迟加载
    • 符合某些条件,或者触发某个事件后开始加载
    • 滚动至可视区域加载
  • 预加载
    • 声明式 <link rel="preload" href="http://example.com" />
    • CSS 样式引入,并且隐藏到非可视区域
    • Javascript new Image(),定义其 src 来实现预加载

for...in 迭代和 for...of 有什么区别

  1. for..in 是 ES5 语法,for..of 是 ES6 语法
  2. for..in 循环遍历对象的属性,遍历出自身和原型上的可枚举非 Symbol 属性,遍历不一定按照顺序
  3. for..of 只可遍历可迭代对象的可迭代属性(包括 Array, Map, Set, String, TypedArray, arguments 对象等等),不可迭代属性在循环中会被忽略

栗子:

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

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

for (let i in iterable) {
    console.log(i); // 0, 1, 2, "foo", "arrCustom", "objCustom"
}

for (let i of iterable) {
    console.log(i); // 3, 5, 7
}

函数柯里化

**柯里化(Currying)**是把接受多个参数的函数变换成接受单一参数(原始函数的第一个参数)的函数,并且返回接受余下参数而且返回结果的新函数的技术。

柯里化的作用

  • 参数复用
  • 提前返回
  • 延迟计算/运行

实现一个 add 方法,使计算结果能够满足如下预期:

js
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
js
function add() {
    var args = Array.prototype.slice.call(arguments);
    var _add = function () {
        // 利用闭包收集所有参数值
        Array.prototype.push.apply(args, arguments);
        return _add;
    };

    // _add.valueOf = function () {
    //     return args.reduce(function (a, b) {
    //         return a + b;
    //     });
    // }

    // 利用 toString 隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _add.toString = function () {
        return args.reduce(function (a, b) {
            return a + b;
        });
    }

    return _add;
}

参考资料:

call, apply 区别,原生实现 bind

this 关键字 - call, apply, bind 调用

立即执行函数和使用场景

声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行。

为了避免解析上的歧义,JS 引擎规定,如果 function 出现在行首,一律解析成语句。因此 JS 引擎看到行首是 function 关键字以后,认为这一段都是函数定义,不应该以原括号结尾,所以就报错了。解决方法就是不要让 function 出现在行首,让 JS 引擎将其理解为一个表达式,写法:

js
(function () {alert("我是匿名函数")}())   //用括号把整个表达式包起来
(function () {alert("我是匿名函数")})()  //用括号把函数包起来
!function () {alert("我是匿名函数")}()  //求反,我们不在意值是多少,只想通过语法检查
+function () {alert("我是匿名函数")}()
-function () {alert("我是匿名函数")}()
~function () {alert("我是匿名函数")}()
void function () {alert("我是匿名函数")}()
new function () {alert("我是匿名函数")}()
  1. 不必为函数命名,避免污染全局变量
  2. 立即执行函数内部形成一个独立的作用域,可以封装一些外部无法读取的私有变量

数组问题

数组去重:

  1. 双重 for 遍历去重,复杂度 O(n²)
  2. indexOf 去重
  3. 相邻元素去重(先排序 sort(),然后遍历对比相邻元素)
  4. 对象属性去重
  5. Set 与数组解构赋值去重 [...new Set(arr)]

前端实现数组去重,如何高效快捷?

  • 数组常用方法

  • 查找数组重复项

  • 扁平化数组

    js
    function flatten(arr) {
        return arr.reduce((flat, item) => {
            return flat.concat(Array.isArray(item) ? flatten(item) : item);
        }, []);
    }
  • 按数组中各项和特定值差值排序

async/await

一个函数如果加上 async,那么该函数就会返回一个 Promiseawait 只能在 async 函数中使用:

js
function sleep() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('finish')
            resolve("sleep");
        }, 2000);
    });
}

async function test() {
    let value = await sleep();
    console.log("object");
}

test();

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

下面来看一个使用 await 的代码:

js
var a = 0
var b = async () => {
    a = a + await 10
    console.log('2', a) // -> '2' 10
    a = (await 10) + a
    console.log('3', a) // -> '3' 20
}

b();
a++
console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,这里说明下原理:

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 然后后面就是常规执行代码了

垃圾回收机制

什么是垃圾

一般来说没有被引用的对象就是垃圾,就是要被清除。有个例外:如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。

如何清除垃圾

V8 引擎帮助我们实现了自动的垃圾回收管理,回收策略是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法

  • 新生代的垃圾回收过程中主要采用了 Scavenge 算法,牺牲空间换取时间。
  • 老生代中采用 Mark-Sweep(标记清除)Mark-Compact(标记整理) 算法

在早前还有一种算法叫做引用计数,该算法的原理比较简单,就是看对象是否还有其他引用指向它,如果没有指向该对象的引用,则该对象会被视为垃圾并被垃圾回收器回收。但该算法一旦碰到循环引用的场景,就会出现问题,导致内存泄露。

模块化(CommonJS, ES6 Module, CMD, AMD)

如何理解前端模块化:

模块化体现的是分治的思想,合理的模块化和组件化有利于我们开发维护及代码复用。

AMD 与 CMD 区别:

最明显的区别就是在模块定义时对依赖的处理不同

  • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块(RequireJS 在推广过程中对模块定义的规范化产出)
  • CMD 推崇就近依赖,只有在用到某个模块的时候再去 require(SeaJS 在推广过程中对模块定义的规范化产出)
js
// CMD
define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();

    // 此处略去 100 行

    var b = require('./b'); // 依赖可以就近书写
    b.doSomething();
    // ...
});

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) {
    // 依赖必须一开始就写好
    a.doSomething();

    // 此处略去 100 行

    b.doSomething();
});
  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行,
  2. CMD 推崇依赖就近,AMD 推崇依赖前置
  3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
  4. 其它细节上也有差异

CommonJS:

CommonJS 是 Nodejs 广泛使用的模块化机制。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

根据这个规范,每个文件就是一个模块,有自己的作用域,文件中的变量、函数、类等都是对其他文件不可见的。

ES6 Module:

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:exportimport

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

CommonJS 和 ES6 Module 中模块引入的区别?

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  3. CommonJS 是单个值导出,ES6 Module 可以导出多个
  4. CommonJS 是动态语法,可以写在判断里,ES6 Module 是静态语法只能写在顶层
  5. CommonJS 的 this 是当前模块,ES6 Module 的 thisundefined

参考资料:

ES6 Class 与 ES5 Function 区别及联系

ES5 及之前版本只有对象,没有类的概念。JS 是基于原型的面向对象语言。

ES6 引入了 Class 的概念,通过 class 关键字可以定义类,该关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言。

js
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    say() {
        console.log(`我的名字叫 ${this.name},今年 ${this.age} 岁了!`);
    }
}

const jimco = new Person('jimco', 18);

jimco.say(); // 我的名字叫 jimco,今年 18 岁了!
console.log(typeof Person); // function
console.log(Person === Person.prototype.constructor); // true

由以上代码可以看出类实质上就是一个函数,可以认为 ES6 中的类其实就是构造函数的另一种写法。

参考资料:

对 ES6 的 Symbol 有了解吗?

Symbol 是由 ES6 规范引入的基础数据类型,功能类似于一种标识唯一性的 ID。通过 Symbol() 函数返回 symbol 类型的值,任何一个 symbol 值都是唯一的。

js
const s1 = Symbol();
const s2 = Symbol('foo');
const s3 = Symbol('foo');

console.log(typeof s1); // symbol
console.log(s2 === s3); // false
console.log(s3.toString()); // Symbol(foo)

应用场景:

  1. 使用 Symbol 来作为对象属性名 (key)
  2. 使用 Symbol 来替代常量
  3. 使用 Symbol 定义类的私有属性/方法

参考资料:

JavaScript 新特性

我能为字符串添加一个属性吗?

思考下面的代码,它会工作吗?会得到什么样的结果?

js
let str = 'Hello';

str.test = 5; // (*)

alert(str.test);

根据你是否开启了严格模式 use strict,会得到如下结果:

  1. undefined(非严格模式)
  2. 报错(严格模式)

为什么?让我们看看在 (*) 那一行到底发生了什么:

  1. 当访问 str 的属性时,一个「对象包装器」被创建了
  2. 在严格模式下,向其写入内容会报错。
  3. 否则,将继续执行带有属性的操作,该对象将获得 test 属性,但是此后,「对象包装器」将消失,因此在最后一行,str 并没有该属性的踪迹。

这个例子清楚地表明,原始类型不是对象,它们不能存储额外的数据。

参考资料