function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return value + "";
}
// 判断数据是引用类型的情况
if (typeof value === "object") {
let valueClass = Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split("");
type.pop();
return type.join("").toLowerCase();
} else {
// 判断数据是基本数据类型的情况和函数的情况
return typeof value;
}
}
利用
<script>
标签不受跨域限制的特点,缺点是只能支持get
请求
script
标签script
标签的src
属性,以问号传递参数,设置好回调函数callback
名称html
文本中res
参数就是获取的数据function jsonp({url,params,callback}) {
return new Promise((resolve,reject)=>{
let script = document.createElement('script')
window[callback] = function (data) {
resolve(data)
document.body.removeChild(script)
}
var arr = []
for(var key in params) {
arr.push(`${key}=${params[key]}`)
}
script.type = 'text/javascript'
script.src = `${url}?callback=${callback}&${arr.join('&')}`
document.body.appendChild(script)
})
}
// 测试用例
jsonp({
url: 'http://suggest.taobao.com/sug',
callback: 'getData',
params: {
q: 'iphone手机',
code: 'utf-8'
},
}).then(data=>{console.log(data)})
CORS: Access-Control-Allow-Origin:*
postMessage
基于Promise.all实现Ajax的串行和并行
HTTP
请求可以同时进行,但是JS的操作都是一步步的来的,因为JS是单线程」,等待所有请求都成功,我们再去做什么事情?Promise.all([
axios.get('/user/list'),
axios.get('/user/list'),
axios.get('/user/list')
]).then(results => {
console.log(results);
}).catch(reason => {
});
Promise.all并发限制及async-pool的应用
并发限制指的是,每个时刻并发执行的promise数量是固定的,最终的执行结果还是保持与原来的
const delay = function delay(interval) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// if (interval === 1003) reject('xxx');
resolve(interval);
}, interval);
});
};
let tasks = [() => {
return delay(1000);
}, () => {
return delay(1003);
}, () => {
return delay(1005);
}, () => {
return delay(1002);
}, () => {
return delay(1004);
}, () => {
return delay(1006);
}];
/* Promise.all(tasks.map(task => task())).then(results => {
console.log(results);
}); */
let results = [];
asyncPool(2, tasks, (task, next) => {
task().then(result => {
results.push(result);
next();
});
}, () => {
console.log(results);
});
const delay = function delay(interval) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(interval);
}, interval);
});
};
let tasks = [() => {
return delay(1000);
}, () => {
return delay(1003);
}, () => {
return delay(1005);
}, () => {
return delay(1002);
}, () => {
return delay(1004);
}, () => {
return delay(1006);
}];
JS实现Ajax并发请求控制的两大解决方案
tasks
:数组,数组包含很多方法,每一个方法执行就是发送一个请求「基于Promise
管理」
function createRequest(tasks, pool) {
pool = pool || 5;
let results = [],
together = new Array(pool).fill(null),
index = 0;
together = together.map(() => {
return new Promise((resolve, reject) => {
const run = function run() {
if (index >= tasks.length) {
resolve();
return;
};
let old_index = index,
task = tasks[index++];
task().then(result => {
results[old_index] = result;
run();
}).catch(reason => {
reject(reason);
});
};
run();
});
});
return Promise.all(together).then(() => results);
}
/* createRequest(tasks, 2).then(results => {
// 都成功,整体才是成功,按顺序存储结果
console.log('成功-->', results);
}).catch(reason => {
// 只要有也给失败,整体就是失败
console.log('失败-->', reason);
}); */
function createRequest(tasks, pool, callback) {
if (typeof pool === "function") {
callback = pool;
pool = 5;
}
if (typeof pool !== "number") pool = 5;
if (typeof callback !== "function") callback = function () {};
//------
class TaskQueue {
running = 0;
queue = [];
results = [];
pushTask(task) {
let self = this;
self.queue.push(task);
self.next();
}
next() {
let self = this;
while (self.running < pool && self.queue.length) {
self.running++;
let task = self.queue.shift();
task().then(result => {
self.results.push(result);
}).finally(() => {
self.running--;
self.next();
});
}
if (self.running === 0) callback(self.results);
}
}
let TQ = new TaskQueue;
tasks.forEach(task => TQ.pushTask(task));
}
createRequest(tasks, 2, results => {
console.log(results);
});
require 基本原理
require 查找路径
require
和module.exports
干的事情并不复杂,我们先假设有一个全局对象{}
,初始情况下是空的,当你require
某个文件时,就将这个文件拿出来执行,如果这个文件里面存在module.exports
,当运行到这行代码时将module.exports
的值加入这个对象,键为对应的文件名,最终这个对象就长这样:
{
"a.js": "hello world",
"b.js": function add(){},
"c.js": 2,
"d.js": { num: 2 }
}
当你再次
require
某个文件时,如果这个对象里面有对应的值,就直接返回给你,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports
加入这个全局对象,并返回给调用者。这个全局对象其实就是我们经常听说的缓存。所以require
和module.exports
并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行
手写实现一个require
const path = require('path'); // 路径操作
const fs = require('fs'); // 文件读取
const vm = require('vm'); // 文件执行
// node模块化的实现
// node中是自带模块化机制的,每个文件就是一个单独的模块,并且它遵循的是CommonJS规范,也就是使用require的方式导入模块,通过module.export的方式导出模块。
// node模块的运行机制也很简单,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就可以实现代码间的作用域隔离
// require加载模块
// require依赖node中的fs模块来加载模块文件,fs.readFile读取到的是一个字符串。
// 在javascrpt中我们可以通过eval或者new Function的方式来将一个字符串转换成js代码来运行。
// eval
// const name = 'poetry';
// const str = 'const a = 123; console.log(name)';
// eval(str); // poetry;
// new Function
// new Function接收的是一个要执行的字符串,返回的是一个新的函数,调用这个新的函数字符串就会执行了。如果这个函数需要传递参数,可以在new Function的时候依次传入参数,最后传入的是要执行的字符串。比如这里传入参数b,要执行的字符串str
// const b = 3;
// const str = 'let a = 1; return a + b';
// const fun = new Function('b', str);
// console.log(fun(b, str)); // 4
// 可以看到eval和Function实例化都可以用来执行javascript字符串,似乎他们都可以来实现require模块加载。不过在node中并没有选用他们来实现模块化,原因也很简单因为他们都有一个致命的问题,就是都容易被不属于他们的变量所影响。
// 如下str字符串中并没有定义a,但是确可以使用上面定义的a变量,这显然是不对的,在模块化机制中,str字符串应该具有自身独立的运行空间,自身不存在的变量是不可以直接使用的
// const a = 1;
// const str = 'console.log(a)';
// eval(str);
// const func = new Function(str);
// func();
// node存在一个vm虚拟环境的概念,用来运行额外的js文件,他可以保证javascript执行的独立性,不会被外部所影响
// vm 内置模块
// 虽然我们在外部定义了hello,但是str是一个独立的模块,并不在村hello变量,所以会直接报错。
// 引入vm模块, 不需要安装,node 自建模块
// const vm = require('vm');
// const hello = 'poetry';
// const str = 'console.log(hello)';
// wm.runInThisContext(str); // 报错
// 所以node执行javascript模块时可以采用vm来实现。就可以保证模块的独立性了
// 分析实现步骤
// 1.导入相关模块,创建一个Require方法。
// 2.抽离通过Module._load方法,用于加载模块。
// 3.Module.resolveFilename 根据相对路径,转换成绝对路径。
// 4.缓存模块 Module._cache,同一个模块不要重复加载,提升性能。
// 5.创建模块 id: 保存的内容是 exports = {}相当于this。
// 6.利用tryModuleLoad(module, filename) 尝试加载模块。
// 7.Module._extensions使用读取文件。
// 8.Module.wrap: 把读取到的js包裹一个函数。
// 9.将拿到的字符串使用runInThisContext运行字符串。
// 10.让字符串执行并将this改编成exports
// 定义导入类,参数为模块路径
function Require(modulePath) {
// 获取当前要加载的绝对路径
let absPathname = path.resolve(__dirname, modulePath);
// 自动给模块添加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在
// 获取所有后缀名
const extNames = Object.keys(Module._extensions);
let index = 0;
// 存储原始文件路径
const oldPath = absPathname;
function findExt(absPathname) {
if (index === extNames.length) {
throw new Error('文件不存在');
}
try {
fs.accessSync(absPathname);
return absPathname;
} catch(e) {
const ext = extNames[index++];
findExt(oldPath + ext);
}
}
// 递归追加后缀名,判断文件是否存在
absPathname = findExt(absPathname);
// 从缓存中读取,如果存在,直接返回结果
if (Module._cache[absPathname]) {
return Module._cache[absPathname].exports;
}
// 创建模块,新建Module实例
const module = new Module(absPathname);
// 添加缓存
Module._cache[absPathname] = module;
// 加载当前模块
tryModuleLoad(module);
// 返回exports对象
return module.exports;
}
// Module的实现很简单,就是给模块创建一个exports对象,tryModuleLoad执行的时候将内容加入到exports中,id就是模块的绝对路径
// 定义模块, 添加文件id标识和exports属性
function Module(id) {
this.id = id;
// 读取到的文件内容会放在exports中
this.exports = {};
}
Module._cache = {};
// 我们给Module挂载静态属性wrapper,里面定义一下这个函数的字符串,wrapper是一个数组,数组的第一个元素就是函数的参数部分,其中有exports,module. Require,__dirname, __filename, 都是我们模块中常用的全局变量。注意这里传入的Require参数是我们自己定义的Require
// 第二个参数就是函数的结束部分。两部分都是字符串,使用的时候我们将他们包裹在模块的字符串外部就可以了
Module.wrapper = [
"(function(exports, module, Require, __dirname, __filename) {",
"})"
]
// _extensions用于针对不同的模块扩展名使用不同的加载方式,比如JSON和javascript加载方式肯定是不同的。JSON使用JSON.parse来运行。
// javascript使用vm.runInThisContext来运行,可以看到fs.readFileSync传入的是module.id也就是我们Module定义时候id存储的是模块的绝对路径,读取到的content是一个字符串,我们使用Module.wrapper来包裹一下就相当于在这个模块外部又包裹了一个函数,也就实现了私有作用域。
// 使用call来执行fn函数,第一个参数改变运行的this我们传入module.exports,后面的参数就是函数外面包裹参数exports, module, Require, __dirname, __filename
Module._extensions = {
'.js'(module) {
const content = fs.readFileSync(module.id, 'utf8');
const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
const fn = vm.runInThisContext(fnStr);
fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
},
'.json'(module) {
const json = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(json); // 把文件的结果放在exports属性上
}
}
// tryModuleLoad函数接收的是模块对象,通过path.extname来获取模块的后缀名,然后使用Module._extensions来加载模块
// 定义模块加载方法
function tryModuleLoad(module) {
// 获取扩展名
const extension = path.extname(module.id);
// 通过后缀加载当前模块
Module._extensions[extension](module);
}
// 至此Require加载机制我们基本就写完了,我们来重新看一下。Require加载模块的时候传入模块名称,在Require方法中使用path.resolve(__dirname, modulePath)获取到文件的绝对路径。然后通过new Module实例化的方式创建module对象,将模块的绝对路径存储在module的id属性中,在module中创建exports属性为一个json对象
// 使用tryModuleLoad方法去加载模块,tryModuleLoad中使用path.extname获取到文件的扩展名,然后根据扩展名来执行对应的模块加载机制
// 最终将加载到的模块挂载module.exports中。tryModuleLoad执行完毕之后module.exports已经存在了,直接返回就可以了
// 给模块添加缓存
// 添加缓存也比较简单,就是文件加载的时候将文件放入缓存中,再去加载模块时先看缓存中是否存在,如果存在直接使用,如果不存在再去重新,加载之后再放入缓存
// 测试
let json = Require('./test.json');
let test2 = Require('./test2.js');
console.log(json);
console.log(test2);
参考:前端手写面试题详细解答
Array.prototype.myForEach = function(callback, context=window) {
// this=>arr
let self = this,
i = 0,
len = self.length;
for(;i<len;i++) {
typeof callback == 'function' && callback.call(context,self[i], i)
}
}
Object.freeze
冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象
function myFreeze(obj){
// 判断参数是否为Object类型,如果是就封闭对象,循环遍历对象。去掉原型属性,将其writable特性设置为false
if(obj instanceof Object){
Object.seal(obj); // 封闭对象
for(let key in obj){
if(obj.hasOwnProperty(key)){
Object.defineProperty(obj,key,{
writable:false // 设置只读
})
// 如果属性值依然为对象,要通过递归来进行进一步的冻结
myFreeze(obj[key]);
}
}
}
}
Array.prototype.mySome=function(callback, context = window){
var len = this.length,
flag=false,
i = 0;
for(;i < len; i++){
if(callback.apply(context, [this[i], i , this])){
flag=true;
break;
}
}
return flag;
}
// var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)
// console.log(flag);
步骤
XMLHttpRequest
实例了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
组合多个函数,从右到左,比如:
compose(f, g, h)
最终得到这个结果(...args) => f(g(h(...args))).
题目描述:实现一个 compose
函数
// 用法如下:
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
实现代码如下
function compose(...funcs) {
if (!funcs.length) return (v) => v;
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => {
return (...args) => a(b(...args)))
}
}
compose
创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose
的部分代码即可实现
Api
接口:把reduce
改为reduceRight
a(b(...args))
改为b(a(...args))
核心:传递给我一个
Generator
函数,把函数中的内容基于Iterator
迭代器的特点一步步的执行
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
};
function asyncFunc(generator) {
const iterator = generator(); // 接下来要执行next
// data为第一次执行之后的返回结果,用于传给第二次执行
const next = (data) => {
let { value, done } = iterator.next(data); // 第二次执行,并接收第一次的请求结果 data
if (done) return; // 执行完毕(到第三次)直接返回
// 第一次执行next时,yield返回的 promise实例 赋值给了 value
value.then(data => {
next(data); // 当第一次value 执行完毕且成功时,执行下一步(并把第一次的结果传递下一步)
});
}
next();
};
asyncFunc(function* () {
// 生成器函数:控制代码一步步执行
let data = yield readFile('a.js'); // 等这一步骤执行执行成功之后,再往下走,没执行完的时候,直接返回
data = yield readFile(data + 'b.js');
return data;
})
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
巧妙的利用两个数的和、差:
a = a + b
b = a - b
a = a - b
call做了什么:
this
到函数并传入给定参数执行函数window
// 模拟 call bar.mycall(null);
//实现一个call方法:
// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()
Function.prototype.myCall = function(context = window, ...args) {
if (typeof this !== "function") {
throw new Error('type error')
}
// this-->func context--> obj args--> 传递过来的参数
// 在context上加一个唯一值不影响context上的属性
let key = Symbol('key')
context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的方法
// let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组
// 绑定参数 并执行函数
let result = context[key](...args);
// 清除定义的this 不删除会导致context属性越来越多
delete context[key];
// 返回结果
return result;
};
//用法:f.call(obj,arg1)
function f(a,b){
console.log(a+b)
console.log(this.name)
}
let obj={
name:1
}
f.myCall(obj,1,2) //否则this指向window
思路:
proto = proto.__proto__
,沿着原型链一直向上查找)__proto__
上找到了当前类的原型prototype
,则返回 true
Object.prototype.__proto__ == null
,Object
的基类(null
)上面都没找到,则返回 false
// 实例.__ptoto__ === 类.prototype
function _instanceof(example, classFunc) {
// 由于instance要检测的是某对象,需要有一个前置判断条件
//基本数据类型直接返回false
if(typeof example !== 'object' || example === null) return false;
let proto = Object.getPrototypeOf(example);
while(true) {
if(proto == null) return false;
// 在当前实例对象的原型链上,找到了当前类
if(proto == classFunc.prototype) return true;
// 沿着原型链__ptoto__一层一层向上查
proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__
}
}
console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true
defineProperty 版本
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 --> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
proxy 版本
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 --> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
redux-thunk
可以利用redux
中间件让redux
支持异步的action
// 如果 action 是个函数,就调用这个函数
// 如果 action 不是函数,就传给下一个中间件
// 发现 action 是函数就调用
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
export default thunk
apply原理与call很相似,不多赘述
// 模拟 apply
Function.prototype.myapply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
then
方法返回一个新的 promise
实例,为了在 promise
状态发生变化时(resolve
/ reject
被调用时)再执行 then
里的函数,我们使用一个 callbacks
数组先把传给then的函数暂存起来,等状态改变时再调用。
那么,怎么保证后一个 **then**
里的方法在前一个 **then**
(可能是异步)结束之后再执行呢? 我们可以将传给 then
的函数和新 promise
的 resolve
一起 push
到前一个 promise
的 callbacks
数组中,达到承前启后的效果:
promise
完成后,调用其 resolve
变更状态,在这个 resolve
里会依次调用 callbacks
里的回调,这样就执行了 then
里的方法了then
里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promise
的 resolve
,让其状态变更,这又会依次调用新 promise
的 callbacks
数组里的方法,循环往复。。如果返回的结果是个 promise
,则需要等它完成之后再触发新 promise
的 resolve
,所以可以在其结果的 then
里调用新 promise
的 resolve
then(onFulfilled, onReject){
// 保存前一个promise的this
const self = this;
return new MyPromise((resolve, reject) => {
// 封装前一个promise成功时执行的函数
let fulfilled = () => {
try{
const result = onFulfilled(self.value); // 承前
return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后
}catch(err){
reject(err)
}
}
// 封装前一个promise失败时执行的函数
let rejected = () => {
try{
const result = onReject(self.reason);
return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
}catch(err){
reject(err)
}
}
switch(self.status){
case PENDING:
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);
break;
case FULFILLED:
fulfilled();
break;
case REJECT:
rejected();
break;
}
})
}
注意:
then
里的回调方法是同步注册的,但注册到了不同的 callbacks
数组中,因为每次 then
都返回新的 promise
实例(参考上面的例子和图)callbacks
数组中提前注册的回调AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
bind 函数的实现步骤:
// bind 函数实现
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。