Appearance
原型链&作用域链
原型链:
构造函数、原型和实例的关系:
- 对象是通过函数创建的,函数是对象
Object.__proto__ === Function.prototype,Function.__proto__ === Function.prototype - 每个函数都有
prototype属性,除了用这个方法创建的函数let fun = Function.prototype.bind() - 每个对象都有一个隐藏属性
__proto__,这个属性指向了创建这个对象的构造函数的原型(这是一个非标准属性,不可以用于编程,是供浏览器自己使用的) prototype属性里面默认有一个constructor属性,该属性指向原型对象所属的构造函数
- 对象是通过函数创建的,函数是对象
prototype与__proto__的关系prototype是构造函数的属性__proto__是实例对象的属性 这两者都指向同一个对象
属性搜索
- 在访问对象的某个成员的时候会先在对象中查找是否存在
- 如果当前对象中没有就在构造函数的原型对象中找
- 如果构造函数的原型对象中没有找到就到原型对象的原型上找
- 直到 Object 的原型对象的原型是 null 为止
new 的实现过程:
- 声明一个中间对象(新生成一个对象)
- 将该中间对象的原型指向构造函数的原型(链接到原型)
- 将构造函数的
this,指向该中间对象(绑定this) - 返回该中间对象,即返回实例对象(返回新对象)
在调用 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 有什么区别
for..in是 ES5 语法,for..of是 ES6 语法for..in循环遍历对象的属性,遍历出自身和原型上的可枚举非Symbol属性,遍历不一定按照顺序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("我是匿名函数")}()- 不必为函数命名,避免污染全局变量
- 立即执行函数内部形成一个独立的作用域,可以封装一些外部无法读取的私有变量
数组问题
数组去重:
- 双重
for遍历去重,复杂度 O(n²) indexOf去重- 相邻元素去重(先排序
sort(),然后遍历对比相邻元素) - 对象属性去重
Set与数组解构赋值去重[...new Set(arr)]
数组常用方法
查找数组重复项
扁平化数组
jsfunction flatten(arr) { return arr.reduce((flat, item) => { return flat.concat(Array.isArray(item) ? flatten(item) : item); }, []); }按数组中各项和特定值差值排序
async/await
一个函数如果加上 async,那么该函数就会返回一个 Promise,await 只能在 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,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。
async 和 await 相比直接使用 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内部实现了generators,generators会保留堆栈中东西,所以这时候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();
});- 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行,
- CMD 推崇依赖就近,AMD 推崇依赖前置
- AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
- 其它细节上也有差异
CommonJS:
CommonJS 是 Nodejs 广泛使用的模块化机制。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。
根据这个规范,每个文件就是一个模块,有自己的作用域,文件中的变量、函数、类等都是对其他文件不可见的。
ES6 Module:
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export 和 import
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
CommonJS 和 ES6 Module 中模块引入的区别?
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 是单个值导出,ES6 Module 可以导出多个
- CommonJS 是动态语法,可以写在判断里,ES6 Module 是静态语法只能写在顶层
- CommonJS 的
this是当前模块,ES6 Module 的this是undefined
参考资料:
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)应用场景:
- 使用
Symbol来作为对象属性名 (key) - 使用
Symbol来替代常量 - 使用
Symbol定义类的私有属性/方法
参考资料:
JavaScript 新特性
类的私有变量
类的
static字段可选链操作符
空位合并操作符
BigInt
Top-level await
WeakRef
我能为字符串添加一个属性吗?
思考下面的代码,它会工作吗?会得到什么样的结果?
js
let str = 'Hello';
str.test = 5; // (*)
alert(str.test);根据你是否开启了严格模式 use strict,会得到如下结果:
undefined(非严格模式)- 报错(严格模式)
为什么?让我们看看在 (*) 那一行到底发生了什么:
- 当访问
str的属性时,一个「对象包装器」被创建了 - 在严格模式下,向其写入内容会报错。
- 否则,将继续执行带有属性的操作,该对象将获得
test属性,但是此后,「对象包装器」将消失,因此在最后一行,str并没有该属性的踪迹。
这个例子清楚地表明,原始类型不是对象,它们不能存储额外的数据。
参考资料:
