JS模块
1.原型对象
- 基本类型 number string boolean object null undefined
- 引用类型 String Number Boolean Object Function Array Date RegExp Error
function Mother(name) {
this.name = name;
}
const son = new Mother("jack");
console.log(son.__proto__ === Mother.prototype);
// 在执行new过程时
1.创建一个新的对象 son
2. son.__proto__ 指向 Mother.prototype
3. 绑定this指向 Mother.call(son,'jack')
4.执行函数中的代码
5.默认return this
扩展问题
let F = function () {};
Object.prototype.a = function () {};
Function.prototype.b = function () {};
let f = new F();
f存在哪个函数???
console.log(f.__proto__ === F.prototype);
console.log(F.__proto__ === Function.prototype);
console.log(F.prototype.__proto__ === Object.prototype);
答案:只有a没有b
2.原型链
function SuperMarket() {}
SuperMarket.prototype.product = "money";
function Shop() {}
Shop.prototype = new SuperMarket();
let p = new Shop();
console.log(p.product);
当p在shop的原型链上找不到product时 会一直往上找 这样就形成了一条原型链
3.作用域
- 全局作用域
- 函数作用域
- eval(少用)
function books() {
const book = "shuben";
return function () {
console.log(book);
};
}
let b = books();
b();
匿名函数一层一层的往上找 形成一条作用域链
问题:请打印出下列结果
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i++);
}, 0);
}
console.log(i);
答案:5 5 6 7 8 9
函数先进行for循环 将异步宏任务弄进任务执行队列保存 等外部同步任务执行后拿到的i为5,再分别拿出任务队列里的函数
若要输出5 0 1 2 3 4
for (var i = 0; i < 5; i++) {
(function (x) {
setTimeout(function () {
console.log(x++);
}, 0);
})(i);
}
console.log(i);
4.this指向
-
对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
-
对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
-
在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
-
call、apply和bind:this 是第一个参数,第二个参数apply是传入一个数组,而其他两个则传入的是一个个参数
-
箭头函数this指向:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。
那么 call,apply,bind的原理是什么呢? call apply都是立即执行返回的结果 而bind返回的是一个函数
Function.prototype.myCall = function (context, ...args) {
const ctx = context || window;
ctx.fn = this;
const res = ctx.fn(...args);
delete ctx.fn;
return res;
}
Function.prototype.myApply = function (context, args) {
const ctx = context || window;
ctx.fn = this;
const res = ctx.fn(...args);
delete ctx.fn;
return res;
};
Function.prototype.myBind = function () {
let args = Array.from(arguments);
let thisObj = args.shift();
let thisFunc = this;
// 因为需要构造函数,所以不能是匿名函数了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisObj = this instanceof fBound ? this : thisObj;
return thisFunc.apply(thisObj, newArgs);
};
// Object.create拷贝原型对象
fBound.prototype = Object.create(this.prototype);
return fBound;
};
5.数据类型的判断
- typeof typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等
当然null 被判断为 object是历史遗留的一个bug问题
typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效
- instanceof instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测null 和 undefined
[] instanceof Array; //true
{} instanceof Object; //Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true
null instanceof Null//报错
undefined instanceof undefined//报错
原理
function myInstanceOf(a, b) {
let l = a.__proto__;
let r = b.prototype;
while (l !== null) {
if (l === r) return true;
l = l.__proto__;
}
return false;
}
6.闭包
闭包这个概念也是JavaScript中比较抽象的概念,闭包是就是函数中的函数,里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。
闭包的作用:
- 使用闭包可以访问函数中的变量。
- 可以使变量长期保存在内存中,生命周期比较长。
闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。
闭包主要有两个应用场景:
- 函数作为参数传递(见作用域部分例子)
- 函数作为返回值(如下例
function outer() {
let num = 0 //内部变量
return function add() {
//通过return返回add函数,就可以在outer函数外访问了。
num++ //内部函数有引用,作为add函数的一部分了
console.log(num)
}
}
let func1 = outer() //
func1() //实际上是调用add函数, 输出1
func1() //输出2
let func2 = outer()
func2() // 输出1
func2() // 输出2
7.async 和 defer
async 是异步加载。defer是延后加载。 借用网上的一张图
- async: 一般用于加载第三方脚本,而且不需要修改dom的情况下使用,不保证加载的顺序,加载后立即执行
- defer: 一般用于需要修改dom的情况下,会先进行加载,保证在html加载完成后再执行对应的脚步
8. Promise
1). Promise 是 JS 中进行异步编程的新解决方案,是为了解决回调地狱的嵌套写法
备注:旧方案是单纯使用回调函数
2). 具体来说
-
从语法上来说: Promise 是一个构造函数
-
从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值
3).状态变更 从pending ----> resolved。 pending ----> rejected
- 只有这 2 种, 且一个 promise 对象只能改变一次 无论变为成功还是失败, 都会有一个结果数据,而且无法逆转,成功的结果数据一般称为 value, 失败的结果数据一般称为 reason
4). promise.all 原理:返回一个新的 promise, 只有所有的 promise 都成功才成功, 只要有一个失败了就直接失败
const myAll = (promise) => {
return new Promise((resolve, reject) => {
let count = 0;
let res = [];
if (promise.length === 0) {
resolve(res);
} else {
function parsePromise(i, data) {
res[i] = data;
if (++count === promise.length) {
resolve(res);
}
}
for (let i = 0; i < promise.length; i++) {
Promise.resolve(promise[i]).then(
(data) => {
parsePromise(i, data);
},
(err) => reject(err)
);
}
}
});
}
5).promise.race原理:返回一个新的 promise, 第一个完成的 promise 的结果状态就是最终的结果状态
const myRace = (promises) => {
return new Promise((resolve, reject) => {
if(promises.length === 0) {
resolve([])
}else {
for(let i = 0; i < promises.length; i++) {
let current = promises[i];
if(isPromise(current)) {
current.then(resolve, reject)
}else {
resolve(current)
}
}
}
})
}
6).async await
- async await本质上是promise的语法糖,其原理是将generator的函数执行权交出来
// 确保拿到res1 交给getRes2函数
let res1 = await getRes1();
let res2 = await getRes2(res1);
9.防抖与节流
- 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
function debounce(fn, delay, immediate) {
let ctx, args, result, timeout;
return function () {
ctx = this;
args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, delay);
if (callNow) result = fn.apply(ctx, ...args);
} else {
timeout = setTimeout(() => {
fn.apply(ctx, ...args);
}, delay);
}
return result;
};
}
- 节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
function throttle(fn, delay) {
let ctx, args, timeout;
return function () {
ctx = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
fn.apply(ctx, ...args);
timeout = null;
}, delay);
}
};
}
10.ES模块和Commonjs模块
1). CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
2). CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
11.单线程 --- 宏任务和为任务
- 我们都知道,js是
单线程执行任务,那么在执行任务时,每个线程都会有它自己的event loop(事件循环),所以都能独立运行。然而所有同源窗口会共享一个event loop以同步通信。event loop会一直运行,来执行进入队列的宏任务。一个event loop有多种的宏任务源,这些宏任务源保证了在本任务源内的顺序。但是浏览器每次都会选择一个源中的一个宏任务去执行。这保证了浏览器给与一些宏任务(如用户输入)以更高的优先级。 macrotask(宏任务) 和microtask(微任务)表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。- 分类如下
借用两张图来描述
- 请快速输出下列代码的结果 (美团面试题)
setTimeout(function () {
console.log("1");
}, 0);
async function async1() {
console.log("2");
const data = await async2();
console.log("3");
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log("4");
resolve("async2的结果");
}).then((data) => {
console.log("5");
return data;
});
}
async1().then((data) => {
console.log("6");
console.log(data);
});
new Promise(function (resolve) {
console.log("7");
// resolve()
}).then(function () {
console.log("8");
});
输出结果:247536 async2 的结果 1