JS的基础

  1. 数据类型

js的8种基本数据类型:

***Undefined***,***Null***,***Boolean***,***Number***,***String***,***Object***,***BigInt***,***Symbol***
其中 Symbol  和 BigInt  是 ES6 新增的数据类型,可能会被单独问:
    - ***Symbol*** 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
    - ***BigInt*** 可以表示任意大小的整数。

数据类型的判断:

(1) typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object 。

    
1
2
3
4
5
6
7
8
9
10
11
12
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function

// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object

(2) instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

    
1
2
3
4
5
6
7
class People {}
class Student extends People {}

const vortesnail = new Student();

console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype  即为 true 。比如这里的 vortesnail  作为实例,顺着原型链能找到 Student.prototype  及 People.prototype ,所以都为 true 。

(3) **Object.prototype.toString.call()**:所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。

1
2
3
4
5
6
7
8
9
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"

如何判断变量是否为数组?

1
2
3
4
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
  1. 原型和原型链

    关于原型和原型链最开始也是懵懵懂懂,直接去看下面这两篇文章吧,相信你会对原型和原型链有更深的理解。
    + JavaScript 深入之从原型到原型链
    + 轻松理解JS 原型原型链

  2. 作用域和作用域链

    • 作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
    • 作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)

    js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
    推荐阅读下面两篇文章:JavaScript深入之词法作用域和动态作用域,深入理解JavaScript作用域和作用域链

  3. 执行上下文

    古人云书读百遍,其义自见,所以直接gun去看掘金大佬的关于执行上下文的几篇文章吧:
    + JavaScript深入之执行上下文栈
    + JavaScript深入之变量对象
    + JavaScript深入之作用域链
    + JavaScript深入之执行上下文
    总结一下:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
    * 变量对象(Variable object,VO);
    * 作用域链(Scope chain);
    * this。(关于 this 指向问题,也是一个重要的问题可以看一下JS 中 this 指向问题这篇文章,你就有更深理解啦。

  4. 闭包

对闭包的定义:

MDN中文的定义:在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
也可以这样说:闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 闭包 = 函数 + 函数能够访问的自由变量。

闭包的实质:

在经过前文“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包
总结:
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

闭包的应用:

(1) 函数作为参数被传递:

1
2
3
4
5
6
7
8
9
10
11
function print(fn) {
const a = 200;
fn();
}

const a = 100;
function fn() {
console.log(a);
}

print(fn); // 100

(2) 函数作为返回值被返回:
1
2
3
4
5
6
7
8
9
10
11
function create() {
const a = 100;

return function () {
console.log(a);
};
}

const fn = create();
const a = 200;
fn(); // 100

(3) 自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
应用实例:比如缓存工具,隐藏数据,只提供 API 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createCache() {
const data = {}; // 闭包中被隐藏的数据,不被外界访问
return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
},
};
}

const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100

  1. call & apply & bind

call:

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

先直接看下面这个例子吧:

1
2
3
4
5
6
7
8
9
var obj = {
value: "vortesnail",
};

function fn() {
console.log(this.value);
}

fn.call(obj); // vortesnail

通过 call 方法我们做到了以下两点:
- call 改变了 this 的指向,指向到 obj 。
- fn 函数执行了。

如何来写call?

先考虑改造 obj 。

1
2
3
4
5
6
7
8
var obj = {
value: "vortesnail",
fn: function () {
console.log(this.value);
},
};

obj.fn(); // vortesnail

这时候 this 就指向了 obj ,但是这样做我们手动给 obj 增加了一个 fn 属性,这是不可行的,于是再使用对象属性的删除方法(delete):
1
2
3
obj.fn = fn;
obj.fn();
delete obj.fn;

最后我们综合一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this !== "function") {
throw new Error("Type error");
}

// 首先获取参数
let args = [...arguments].slice(1);
let result = null;

// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;

// 将被调用的方法设置为 context 的属性
// this 即为我们要调用的方法
context.fn = this;

// 执行要被调用的方法
result = context.fn(...args);

// 删除手动增加的属性方法
delete context.fn;

// 将执行结果返回
return result;
};

apply:

apply与call没有任何区别,除了传参方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new Error("Type error");
}
let result = null;
context = context || window;

// 与上面代码相比,我们使用 Symbol 来保证属性唯一
// 也就是保证不会重写用户自己原来定义在 context 中的同名属性
const fnSymbol = Symbol();
context[fnSymbol] = this;

// 执行要被调用的方法
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};

bind:

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

相关文章:解析 bind 原理,并手写 bind 实现
bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取参数
const args = [...arguments].slice(1),
const fn = this;
return function Fn() {
return fn.apply(
this instanceof Fn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
  1. new 关键字?

    我们需要知道执行new关键字的时候发生了什么:
    - 首先创一个新的空对象。
    - 根据原型链,设置空对象的 proto 为构造函数的 prototype
    - 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
    - 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。

    1
    2
    3
    4
    5
    6
    function myNew(context) {
    const obj = new Object();
    obj.__proto__ = context.prototype;
    const res = context.apply(obj, [...arguments].slice(1));
    return typeof res === "object" ? res : obj;
    }
  2. Promise & async/await & eventloop

eventloop它的执行顺序:

  • 一开始整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 执行浏览器UI线程的渲染工作
  • 检查是否有Web Worker任务,有则执行
  • 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

宏任务包括:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering。
微任务包括:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、Node独有的process.nextTick

注:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

关于async/await:

async 函数: 一个语法糖 是异步操作更简单,返回值是一个 promise 对象;
  • return 的值是 promise resolved 时候的 value
  • Throw 的值是 Promise rejected 时候的 reason
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    async function test() {
    return true
    }
    const p = test()
    console.log(p) // 打印出一个promise,状态是resolved,value是true

    // Promise {<fulfilled>: true}
    // [[Prototype]]: Promise
    // [[PromiseState]]: "fulfilled"
    // [[PromiseResult]]: true

    p.then((data) => {
    console.log(data) // true
    })
    async 函数的返回值是一个 promise
    1
    2
    3
    4
    5
    6
    7
    8
    async function test() {
    throw new Error('error')
    }
    const p = test()
    console.log(p) // 打印出一个promise,状态是rejected,value是error
    p.then((data) => {
    console.log(data) //打印出的promise的reason 是error
    })
await 函数
  • 只能出现在 async 函数内部或最外层
  • 等待一个 promise 对象的值
  • await 的 promise 的状态为 rejected,后续执行中断

await 为等待 promise 的状态是 resolved 的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function async1() {
console.log('async1 start')
await async2() // await为等待promise的状态,然后把值拿到
console.log('async1 end')
}
async function async2() {
return Promsie.resolve().then(_ => {
console.log('async2 promise')
})
}
async1()
/*
打印结果
async1 start
async2 promise
async1 end
*/

await 为等待 promise 的状态是 rejected 的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function f() {
await Promise.reject('error')
//后续代码不会执行
console.log(1)
await 100
}

// 解决方案1
async function f() {
await Promise.reject('error').catch(err => {
// 异常处理
})
console.log(1)
await 100
}

// 解决方案2
async function f() {
try {
await Promise.reject('error')
} catch (e) {
// 异常处理
} finally {
}
console.log(1)
await 100
}

关于Promise:

实现一个 Promise.all:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 参数可以不是数组,但必须具有 Iterator 接口
if (typeof promises[Symbol.iterator] !== "function") {
reject("Type error");
}
if (promises.length === 0) {
resolve([]);
} else {
const res = [];
let count = 0;
const len = promises.length;
for (let i = 0; i < len; i++) {
//考虑到 promises[i] 可能是 thenable 对象也可能是普通值
Promise.resolve(promises[i])
.then((data) => {
res[i] = data;
if (++count === len) {
resolve(res);
}
})
.catch((err) => {
reject(err);
});
}
}
});
};

直接上阮一峰的这篇文章吧:ECMAScript 6 入门-Promise 对象

  1. 让人欲哭无泪的的手撕代码

手写深拷贝

何为深/浅拷贝?
  • 浅:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
开撕吧^_^^_^^_^^_^

先来一个基础版本的浅拷贝吧:

1
2
3
4
5
6
7
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function clone(target) {
    if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
    cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
    } else {
    return target;
    }
    };

兼容数组之后的深拷贝:

1
2
3
4
5
6
7
8
9
10
11
module.exports = function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
- 检查map中有无克隆过的对象, 有 - 直接返回,没有 - 将当前对象作为key,克隆对象作为value进行存储
- 继续克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};

深拷贝还有诸多优化可以看一下关于手写深拷贝的大佬文章:如何写出一个惊艳面试官的深拷贝?

数组去重

  • 利用set关键字(ES6)
    1
    2
    3
    function unique(arr) {
    return [...new Set(arr)];
    }
  • 利用filter方法(ES5)
    1
    2
    3
    4
    5
    function unique(arr) {
    return arr.filter((item, index, array) => {
    return array.indexOf(item) === index;
    });
    }

数组扁平化

1
2
3
4
5
6
7
8
9
function flat(arr, depth = 1) {
if (depth > 0) {
// 以下代码还可以简化,不过为了可读性,还是....
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
}, []);
}
return arr.slice();
}

手写 reduce

先不考虑第二个参数初始值:

1
2
3
4
5
6
7
8
Array.prototype.reduce = function (cb) {
const arr = this; //this就是调用reduce方法的数组
let total = arr[0]; // 默认为数组的第一项
for (let i = 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};

考虑上初始值:

1
2
3
4
5
6
7
8
9
Array.prototype.reduce = function (cb, initialValue) {
const arr = this;
let total = initialValue || arr[0];
// 有初始值的话从0遍历,否则从1遍历
for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};

防抖和节流

防抖就是指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次;(回城,被打断了就要重新来)

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(fn, delay = 200) {
let timer = 0
return function() {
// 如果这个函数已经被触发了
if(timer){
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments); // 透传 this和参数
timer = 0
},delay)
}
}

节流就是是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。(技能CD,CD没有冷却好,就一直用不了技能)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 节流函数
function throttle(fn, delay = 200) {
let timer = 0
return function () {
if(timer){
return
}
timer = setTimeout(() =>{
fn.apply(this, arguments); // 透传 this和参数
timer = 0
},delay)
}
}

应用场景:滚动加载,下拉刷新,提交按钮点击,轮播图切换

Last…………………..持续学习更新中……………………