跳到主要内容

10 篇博文 含有标签「JavaScript」

查看所有标签

· 阅读需 22 分钟

[[toc]]

JS 源码实现

原型链

instanceof

const instanceOf = (instance, constructor) => {
let p = instance.__proto__
while(p) {
if(p === constructor.prototype) return true
p = p.__proto__
}
return false
}

console.log(instanceOf(1, Number));

new

function NEW(className, ...args) {
const obj = {};
obj.__proto__ = className.prototype;
const res = className.call(obj, ...args);
return res instanceof Object
? res
: obj;
}

function classA(age) {
this.name = "start";
this.age = age;
}
classA.prototype.sayName = function() {
console.log(this.name);
};
classA.prototype.sayAge = function() {
console.log(this.age);
};

const a = NEW(classA, 19);
a.sayName();
a.sayAge();
a.name = "a";
a.sayName();

Object.create

/**
*
* @param {*} prototype 传入的prototype将作为新对象的__proto__
* @param {*} propertyDescriptors 自定义的属性描述符
*/
function create(prototype, propertyDescriptors) {
const F = function() {};
F.prototype = prototype;
let res = new F();
if (propertyDescriptors) {
Object.defineProperties(res, propertyDescriptors);
}
return res
}

/* Object.create 和 new的本质上都是创建一个新对象,将__proto__指向prototype
两者的区别是前者直接接收prototype, 而后者接收constructor,通过constructor.prototype来间接获得prototype
*/
function create1(prototype, propertyDescriptors) {
let res = {};
res.__proto__ = prototype
if (propertyDescriptors) {
Object.defineProperties(res, propertyDescriptors);
}
return res
}

let obj = {a: 11}
let copy = Object.create(obj, {mm: {value: 10, enumerable: true}})
console.log(copy);
console.log(obj)

let obj1 = {a: 11}
let copy1 = create(obj, {mm: {value: 10, enumerable: true}})
console.log(copy1);
console.log(obj1)

let obj2 = {a: 11}
let copy2 = create1(obj, {mm: {value: 10, enumerable: true}})
console.log(copy2);
console.log(obj2)

继承

// es3
function __extends(child, parent) {
// 继承静态方法和属性
child.__proto__ = parent;
// 常规继承
function __() {
this.constructor = child;
}
__.prototype = parent.prototype;
child.prototype = new __();
}

// es5
function Parent(val) {
this.val = val;
}
Parent.StaticFunc = function() {
console.log("static");
};

Parent.prototype.getValue = function() {
console.log(this.val);
};

// 常规继承
function Child(value) {
Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true,
},
});
// 继承静态属性方法
Child.__proto__ = Parent;

const child = new Child("bob");
Child.StaticFunc();
child.getValue();

// es6就用class继承,太过简单就不写了

this

call

Function.prototype.myCall = function(context = window, ...args) {
const unique = Symbol("fn");
context[unique] = this;
const res = context[unique](...args);
delete context[unique];
return res;
};

apply

Function.prototype.myApply = function(context = window, args = []) {
const unique = Symbol("fn");
context[unique] = this;
const res = context[unique](...args);
delete context[unique];
return res;
};

bind

// 第一版
Function.prototype.myBind = function(context, ...bindArgs) {
const _this = this;
context = context || window;

return function(...args) {
_this.apply(context, [...bindArgs, ...args]);
};
};

// 精简版
Function.prototype.myBind = function(context = window, ...bindArgs) {
return (...args) => this.apply(context, [...bindArgs, ...args]);
};

// 无依赖版
Function.prototype.myBind = function(context = window, ...bindArgs) {
const uniq = Symbol("fn");
context[uniq] = this;
return function(...args) {
const res = context[uniq](...bindArgs, ...args);
delete context[uniq];
return res;
};
};

函数式

curry

function curry(fn, ...fixs) {
return fn.length <= fixs.length
? fn(...fixs)
: (...args) => curry(fn, ...fixs, ...args);
}

// 函数柯里化核心:fn.length
// 当传入的参数列表长度大于等于 原函数所需的参数长度(fn.length)时,执行原函数
// 否则返回一个能接收参数继续进行柯里化的函数

const add = (a, b, c) => a + b + c;
const curried = curry(add);
console.log(curried(1, 2)(3));
console.log(curried(1)(2, 3));
console.log(curried(1)(2)(3));
console.log(curried(1, 2, 3, 4));
function add(...args) {
let arr = args;
function fn(...rest) {
arr = [...arr, ...rest];
return fn;
}
fn[Symbol.toPrimitive] = function() {
return arr.reduce((acc, cur) => acc + parseInt(cur));
};
// fn.toString = fn.valueOf = function () {
// return arr.reduce((acc, cur) => acc + parseInt(cur));
// };

return fn;
}

const res = add(1)(2);
console.log(res + 10); // 13
console.log(add(1)(2)(3));
console.log(add(1, 2, 3));
/**
* 函数柯里化另一种实现思路,可以实现对不定参数的函数实现柯里化,原理是每次调用只存储传入的参数,并且把存储参数的函数返回出去,重写函数的toString和valueOf,在外部对该函数进行使用时,就会调用重写后的toString和valueOf
*/

compose

function compose(...func) {
return func.reduce((a, b) => {
return (...args) => a(b(...args));
});
}

防抖节流

throttle

function throttle(fn, timeout = 200) {
let lastTime = 0;
let cur;
let result;
return function cb(...args) {
cur = Date.now();
if (cur - lastTime >= timeout) {
result = fn.call(this, ...args);
lastTime = cur;
}
return result;
};
}

function throttleSetTimeOutVersion(fn, timeout = 1000, immediate = false) {
let timer = null;
let __immediate = immediate;
return function (...args) {
if (__immediate) {
fn.call(this, ...args);
__immediate = false;
}
if (timer) return;
timer = setTimeout(() => {
fn.call(this, ...args);
timer = null;
}, timeout);
};
}

const obj = {
value: 1,
};

function print(val) {
console.log(this.value + val);
}

const printThrottled = throttle(print, 5000);

// 16毫秒执行一次printThrottled方法
setInterval(() => {
printThrottled.call(obj, 1);
}, 16);

debounce

function debounce(fn, timeout = 1000) {
let t;
let result;
return function cb(...args) {
// 在每次调用的时候都清除上一次的定时器,不管定时器内函数是否已经执行
clearTimeout(t);
t = setTimeout(() => {
result = fn.call(this, ...args);
}, timeout);
return result;
};
}

const obj = {
value: 1,
};

function print(val) {
console.log(this.value + val);
}

const printDebounced = debounce(print, 1000);

// 16毫秒执行一次printThrottled方法, 那么print永远不会被执行到
setInterval(() => {
printDebounced.call(obj, 1);
}, 16);

异步

promise 实现

/** 三种状态 */
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
/** 默认为等待态 */
status = PENDING;
/** 当状态为FULFILLED时存储的值 */
value = undefined;
/** 当状态为REJECTED时存储的拒因 */
reason = undefined;
/** 通过时的回调队列 */
successCallbacks = [];
/** 拒绝时的回调队列 */
failCallbacks = [];

constructor(executor) {
/** promise同步代码执行时的错误处理 */
try {
/** 在创建promise时执行执行器函数 */
executor(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}

/** 以value为值通过该promise */
resolve = (value) => {
/** 只有当状态为等待态时才能改变状态 */
if (this.status === PENDING) {
/** 将状态改为完成 */
this.status = FULFILLED;
this.value = value;
/** 执行成功回调队列中的函数 */
this.successCallbacks.forEach((fn) => fn());
}
};

/** 以reason为拒因拒绝改promise */
reject = (reason) => {
/** 只有当状态为等待态时才能改变状态 */
if (this.status === PENDING) {
/** 将状态改为拒绝 */
this.status = REJECTED;
this.reason = reason;
/** 执行成功回调队列中的函数 */
this.failCallbacks.forEach((fn) => fn());
}
};

/** 向promise中执行或注册promise在不同状态时的回调事件 */
then = (onSuccess, onFail) => {
let promise2;
const resolvePromise = (promise2, x, resolve, reject) => {
/**处理promise循环调用自身的情况 */
if (promise2 === x) {
return reject(new TypeError("不允许promise循环调用自身"));
}
if (x instanceof MyPromise) {
x.then((v) => {
resolvePromise(promise2, v, resolve, reject);
}, reject);
} else {
resolve(x);
}
};
const successCallback = (resolve, reject) => {
/** promise2只有在MyPromise的逻辑结束后才能生成,如果因此如果同步执行下面的代码,获取到的promise2是undefined, 因此需要使用settimeout使下面代码执行时间推迟到promise2生成后 */
setTimeout(() => {
/** then回调函数执行时的错误处理 */
try {
/** 处理传入的callback非法的情况,当callback不是函数时,忽略这个then */
if (typeof onSuccess === "function") {
/** 不仅要执行回调函数,还要处理then中return数据作为下一个then的value的情况 */
let x = onSuccess(this.value);
resolvePromise(promise2, x, resolve, reject);
} else {
/** 以上当前promise的value作为promise2 resolve时的value */
resolve(this.value);
}
} catch (e) {
reject(e);
}
}, 0);
};

const failCallback = (resolve, reject) => {
/** promise2只有在MyPromise的逻辑结束后才能生成,如果因此如果同步执行下面的代码,获取到的promise2是undefined, 因此需要使用settimeout使下面代码执行时间推迟到promise2生成后 */
setTimeout(() => {
/** then回调函数执行时的错误处理 */
try {
/** 处理传入的callback非法的情况,当callback不是函数时,忽略这个then */
if (typeof onFail === "function") {
/** 不仅要执行回调函数,还要处理then中return数据作为下一个then的value的情况 */
let x = onFail(this.reason);
resolvePromise(promise2, x, resolve, reject);
} else {
/** 以上当前promise的reason作为promise2 reject时的reason */
reject(this.reason);
}
} catch (e) {
reject(e);
}
}, 0);
};

/** 为了支持then的链式调用,需要每次都返回一个新的promise */
promise2 = new MyPromise((resolve, reject) => {
/** 如果是PENDING状态,存储回调事件,否则直接执行 */
switch (this.status) {
case FULFILLED:
successCallback(resolve, reject);
break;
case REJECTED:
failCallback(resolve, reject);
break;
case PENDING:
this.successCallbacks.push(() => successCallback(resolve, reject));
this.failCallbacks.push(() => failCallback(resolve, reject));
break;
default:
}
});

return promise2;
};

/** this.then(null, fn)的语法糖 */
catch = (fn) => {
/** 需要链式调用,所以需要return */
return this.then(null, fn);
};

/**
* finally传入的回调函数不管promise被reject还是resolve都会被执行
* finally支持链式调用
* 如果finally返回了普通值,将无视该返回值,下一个then接收的值仍然是finally上游的返回值
* 如果返回了promise,下一个then将等待该promise的执行
* 如果回调函数在执行过程中throw了一个错误,则会作为新的拒因传递给下一个then
* 注意,finally的回调函数不接受value或者reason
*/
finally = (fn) => {
/** 需要链式调用,所以需要return */
return this.then(
(value) => {
/** 在如果fn在执行过程中抛出错误,不会执行then中的回调函数,而是继续向下传递 */
return MyPromise.resolve(fn()).then(() => value);
},
(e) => {
/** 在如果fn在执行过程中抛出错误,不会执行then中的回调函数,而是继续向下传递 */
return MyPromise.resolve(fn()).then(() => {
throw e;
});
}
);
};

/** 如果Promise.resolve的参数是一个promise,直接返回该promise,否则创建一个新的promise,该promise状态为fulfilled */
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise((resolve, reject) => {
resolve(value);
});
}

/** 不管Promise.reject的参数是什么,都将它作为拒因,创建一个新的promise,该promise的状态为rejected */
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
}

/** 下面的race,allSettled,any都是以all为蓝本进行修改的 */
static all(array) {
/** 已经resolve的promise数量 */
let count = 0;
let length = array.length;
/** 返回的结果数组 */
let result = [];
/** 当数组中有一个promise的结果为rejected,直接整个promise reject,并且以该rejected状态的promise的拒因为promise.all的拒因 */
return new MyPromise((resolve, reject) => {
array.forEach((promise, index) => {
/** 通过Promise.resolve将非promise的值转为promise,来统一处理 */
MyPromise.resolve(promise).then((v) => {
result[index] = v;
count++;
/** 只有当已经resolve的promise的数量和传入的数组长度一致,才resolve结果数组 */
if (count === length) {
resolve(result);
}
}, reject);
});
});
}

static race(array) {
/** 当数组中有一个promise被resolve或者reject了,就作为race的value或reason被返回 */
return new MyPromise((resolve, reject) => {
array.forEach((promise) => {
/** 通过Promise.resolve将非promise的值转为promise,来统一处理 */
MyPromise.resolve(promise).then(resolve, reject);
});
});
}

/** 当数组中的promise被resolve或者reject时,都是被settle了,当数组中所有的promise都被settle,返回结果数组 */
static allSettled(array) {
/** 已经settle的promise数量 */
let count = 0;
let length = array.length;
/** 返回的结果数组 */
let result = [];

return new MyPromise((resolve, reject) => {
array.forEach((promise, index) => {
/** 通过Promise.resolve将非promise的值转为promise,来统一处理 */
MyPromise.resolve(promise).then(
(v) => {
result[index] = {
value: v,
status: "fulfilled",
};
count++;
/** 只有当已经settle的promise的数量和传入的数组长度一致,才resolve结果数组 */
if (count === length) {
resolve(result);
}
},
(e) => {
result[index] = {
reason: e,
status: "rejected",
};
count++;
/** 只有当已经settle的promise的数量和传入的数组长度一致,才resolve结果数组 */
if (count === length) {
resolve(result);
}
}
);
});
});
}

/** 借用Promise.all的Promise.allSettle的简化版 */
static allSettled2(array) {
return MyPromise.all(
array.map((promise) => {
return MyPromise.resolve(promise).then(
(v) => ({ status: "fulfilled", value: v }),
(e) => ({ status: "rejected", reason: e })
);
})
);
}

/** https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/any */
static any(array) {
/** 已经reject的promise数量 */
let count = 0;
let length = array.length;
/** 当数组中有一个promise的结果为fulfilled,直接整个promise resolve,并且以该ulfilled状态的promise的value为promise.any的value */
return new MyPromise((resolve, reject) => {
array.forEach((promise) => {
/** 通过Promise.resolve将非promise的值转为promise,来统一处理 */
MyPromise.resolve(promise).then(resolve, (e) => {
count++;
/** 当所有的promise都失败时,reject一个 AggregateError*/
if (count === length) {
reject(
new Error(
"AggregateError: No Promise in Promise.any was resolved"
)
);
}
});
});
});
}
static retry(promiseCreator, times, delay) {
return new MyPromise((resolve, reject) => {
function attempt() {
promiseCreator()
.then((data) => {
resolve(data);
})
.catch((err) => {
if (times === 0) {
reject(err);
} else {
times--;
setTimeout(attempt, delay);
}
});
}
attempt();
});
}
}

module.exports = MyPromise;

promisify

function func(a, b, cb) {
const res = a + b;
cb(null, res);
}

function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
args.push((err, data) => {
if (err) reject(err);
else resolve(data);
});
fn.apply(null, args);
});
};
}

const funcPromisify = promisify(func);

funcPromisify(1, 2).then((val) => {
console.log(val);
});

callbackify

const callbackify = (promiseCreator) => {
return (...args) => {
const arg = args.slice(0, -1);
const cb = args.slice(-1)[0];
promiseCreator(...arg)
.then((val) => {
cb(null, val);
})
.catch((err) => {
cb(err, null);
});
};
};

co

function ajax(url) {
return Promise.resolve(url);
}

function* main() {
let res1 = yield ajax("https://www.baidu.com");
console.log(res1);

let res2 = yield ajax("https://www.google.com");
console.log(res2);

let res3 = yield ajax("https://www.taobao.com");
console.log(res3);
}

function co(generator) {
const g = generator();
function handleResult(result) {
if (result.done) return;
result.value.then(
(res) => {
handleResult(g.next(res));
},
(e) => {
g.throw(e);
}
);
}
handleResult(g.next());
}

co(main);

await

const getData = () =>
new Promise((resolve) => setTimeout(() => resolve("data"), 1000));

const test = asyncToGenerator(function* testG() {
const data = yield getData();
console.log("data: ", data);
const data2 = yield Promise.reject(222);
console.log("data2: ", data2);
return "success";
});

// 这样的一个函数 应该再1秒后打印data 再过一秒打印data2 最后打印success
test().then((res) => {
console.log(res);
});

function asyncToGenerator(generatorFunc) {
return function() {
const gen = generatorFunc.apply(this, arguments);
return new Promise((resolve, reject) => {
function step(operate, val) {
let genRes;
try {
genRes = gen[operate](val);
} catch (err) {
return reject(err);
}
const { value, done } = genRes;
if (done) {
resolve(value);
} else {
Promise.resolve(value).then(
(val) => step("next", val),
(err) => step("throw", err)
);
}
}
step("next");
});
};
}

数组

filter

Array.prototype.filter = function(filterCb, thisArg) {
const res = [];
const arr = this;
for (let i = 0; i < arr.length; i++) {
if (filterCb.call(thisArg, arr[i], i, arr)) {
res.push(arr[i]);
}
}
return res;
};

console.log([1, 2, 3].filter((v) => v > 1));

flat

function flat(arr, depth = Infinity) {
if (depth === 0) return arr;
return arr.reduce(
(acc, cur) => acc.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur),
[]
);
}

console.log(flat([1, 2, [3, 4, 5, [6, 7, 8]]], 1));

let arr = [1, [2, [3, 4], 5], 6];
let str = JSON.stringify(arr);
console.log(str.replace(/\[|\]/g, "").split(","));
console.log(JSON.parse("[" + str.replace(/\[|\]/g, "") + "]"));

其他

Object.is

function is(x, y) {
if (x === y) {
if (x !== 0 && y !== 0) return true;
// x和y相等的情况下,处理+0和-0的情况
else return 1 / x === 1 / y;
} else {
// x和y不相等的情况下,处理NaN的情况
return x !== x && y !== y;
}
}

console.log(is(+0, -0));
console.log(is(NaN, NaN));

deepClone

function isReferenceType(o) {
return o instanceof Object;
}

/**
* 1. 判断是否引用类型,如果不是直接返回
* 2. 针对正则、函数和Date做异常处理
* 3. 获取到原对象的constructor,创建新对象
* 4. 引入WeakMap解决循环引用问题
* 5. 遍历原对象中的数据,将数据通过深拷贝的方式赋值给新对象
*/
function deepClone(obj, hash = new WeakMap(), parent) {
if (!isReferenceType(obj)) {
return obj;
}

if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 传入父级节点解决负责的函数this指向丢失的情况
if (obj instanceof Function) return obj.bind(parent);

const newObj = new obj.constructor();

// 解决循环引用问题
if (hash.has(obj)) {
return hash.get(obj);
}
hash.set(obj, newObj);

// Reflect.ownKeys可将对象上的可枚举和不可枚举以及symbol都访问到
Reflect.ownKeys(obj).forEach((key) => {
newObj[key] = deepClone(obj[key], hash, newObj);
});

return newObj;
}

function add(a, b, c) {
return a + b + c;
}

const obj1 = {
a: 1,
b: 2,
date: new Date(),
arr: [1, 2, 3],
func: add.bind({}, 1),
};
obj1.obj = obj1;

console.log(deepClone(obj1).func(2, 3));

JSONP

function JSONP(url) {
return new Promise((resolve, reject) => {
const callbackName = `callback_${Date.now()}`;
const script = document.createElement('script');

script.src = `${url}${url.indexOf('?') > -1 ? '&' : '?'}callback=${callbackName}`;
document.body.appendChild(script);
window[callbackName] = function (res) {
delete window[callbackName];
script.remove();
resolve(res);
};
script.onerror = function (e) {
delete window[callbackName];
script.remove();
reject(e);
};
});
}

ajax

const addUrl = (_url, param) => {
let url = _url;
if (param && Object.keys(param).length) {
url += url.indexOf("?") === -1 ? "?" : "&";
Object.keys(param).forEach((key) => {
url += `${encodeURIComponent(key)}=${encodeURIComponent(param[key])}`;
});
}
return url;
};

function ajax({
url = "",
method = "GET",
data = {},
header = {},
async = false,
timeout = 5 * 1000,
onSuccess,
onError,
onTimeout,
}) {
const requestURL = method === "GET" ? addUrl(url, data) : url;
const sendData = method === "GET" ? null : data;

const xhr = new XMLHttpRequest();
xhr.open(method, requestURL, async);

if (header && Object.keys(header).length) {
Object.keys(header).forEach((key) => {
xhr.setRequestHeader(key, header[key]);
});
}

xhr.onload = () => {
try {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
onSuccess(xhr.responseText);
} else {
onError(xhr.status + xhr.statusText);
}
} catch (err) {
onError(err);
}
};
xhr.timeout = timeout;
xhr.ontimeout =
onTimeout ||
function() {
console.error("request timeout");
};

xhr.send(sendData);
}

node require

const vm = require("vm");
const path = require("path");
const fs = require("fs");

function customRequire(filePath) {
const pathToFile = path.resolve(__dirname, filePath);
const content = fs.readFileSync(pathToFile, "utf-8");

const wrapper = ["(function(require, module, exports) {", "})"];
const wrappedContent = wrapper[0] + content + wrapper[1];
console.log("wrappedContent: ", wrappedContent);

const script = new vm.Script(wrappedContent, {
filename: "index.js",
});
const module = {
exports: {},
};
const func = script.runInThisContext();
func(customRequire, module, module.exports);
return module.exports;
}

const { add } = customRequire("./module.js");
console.log(add(1, 2));

eventEmitter

class EventEmitter {
constructor() {
this.events = {};
this.count = 0;
}

subscribe(type, listener) {
this.events[type] = this.events[type] || [];
this.events[type].push(listener);

return this.unsubscribe.bind(this, type, listener);
}

unsubscribe(type, listener) {
if (this.events[type]) {
this.events[type] = this.events[type].filter((cb) => cb !== listener);
}
}

publish(type, ...args) {
if (this.events[type]) {
[...this.events[type]].forEach((cb) => {
cb.call(this, ...args);
});
}
}
}

const event = new EventEmitter();
event.subscribe("daily", function() {
// 校验call的绑定
console.log(`there has already ${this.count} subscribers`);
console.log("bob");
});

event.subscribe("evening", () => {
console.log("alice");
});

event.subscribe("noon", () => {
console.log("Jim");
});

const JimEveningFn = function() {
console.log("Jim");
};
event.subscribe("evening", JimEveningFn);

event.publish("daily");
console.log("****");
event.publish("evening");
console.log("****");
event.publish("noon");

eventEmitterProxy

class Observer {
constructor(target) {
this.observers = [];
this.target = new Proxy(target, {
set: (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver);
[...this.observers].forEach((cb) => cb(target, key, value, receiver));
return result;
},
});

return [this.target, this.observe];
}

observe = (fn) => {
this.observers.push(fn);
return this.unobserve.bind(null, fn);
};

unobserve = (fn) => {
this.observers = this.observers.filter((o) => o !== fn);
};
}

let [person, observe] = new Observer({ name: 1, age: 2 });
let unobserve = observe((target, key, value) => {
console.log("target, key, value: ", target, key, value);
});

person.name = "bob"; // target, key, value: { name: 'bob', age: 2 } name bob

unsubscribe();
person.name = "aaa"; // 无输出

shuffle

function shuffle(arr) {
for (let i = 0; i < arr.length; i++) {
let changeIndex = Math.floor(Math.random() * i);
[arr[i], arr[changeIndex]] = [arr[changeIndex], arr[i]];
}
return arr;
}

let arr2 = [1, 2, 3, 4, 5, 6];
console.log(shuffle(arr2));

sleep

function sleep (time) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, time);
})
}

async function main(){
console.log(1)
await sleep(2000)
console.log(2)
}

main()

· 阅读需 2 分钟

滴滴面试被面试官问到promise.all的细节--当Promise.all中一个promise失败时,其它promise的状态会是什么 当时没答上来,现在在此复盘一下

const promise1 = new Promise((resolve) => {
resolve('promise1')
})

const promise2 = new Promise((resolve, reject) => {
reject(new Error('error2'))
})

const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('error3'))

}, 2000);
})

const res = Promise.all([promise3, promise1, promise2]).then(res => {
console.log(res); // 不执行
}).catch(e => {
console.log(e); // Error: error2
}).finally(() => {
console.log(promise1); // Promise { 'promise1' }
console.log(promise2); // Promise {
// <rejected> Error: error2 ...
console.log(promise3); // Promise { <pending>
setTimeout(() => {
console.log(promise3); // Promise {
// <rejected> Error: error3 ...
}, 3000);
})

结论:

  1. 当Promise.all的一个promise reject时,不会影响其它promise的状态
  2. Promise.all 返回的是时间维度的第一个reject的error
  3. 如果Promise.all中一个promise为reject时,其它promise如果还未resolve或者reject,那么仍然处于pending状态,当之后获得异步的结果后,会变成resolve或者reject状态

以下是Promise.all的实现

MyPromise.all = function(promiseList) {
var resPromise = new MyPromise(function(resolve, reject) {
var count = 0;
var result = [];
var length = promiseList.length;

if (length === 0) {
return resolve(result);
}

promiseList.forEach(function(promise, index) {
// 通过Promise.resolve包裹,允许传入的promise参数是个普通的值
MyPromise.resolve(promise).then(function(value) {
count++;
// 使用索引进行存储而非数组的push方法,是为了保证resolve的结果与传入的promise一一对应,防止因为异步导致的错位
result[index] = value;
// 当所有promise都resolve时,promise.all才resolve
if (count === length) {
resolve(result);
}
// 当一个promise被reject时,promise.all立马reject
}, reject);
});
});

return resPromise;
};

· 阅读需 30 分钟

在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广度和深度。
本文从浏览器角度来告诉你,URL后输入后按回车,浏览器内部究竟发生了什么,读完本文后,你将了解到:

  • 浏览器内有哪些进程,这些进程都有些什么作用

  • 浏览器地址输入URL后,内部的进程、线程都做了哪些事

  • 我们与浏览器交互时,内部进程是怎么处理这些交互事件的

浏览器架构

在讲浏览器架构之前,先理解两个概念,进程线程。进程(process)是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,线程(thread)是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。简单的说呢,进程可以理解成正在执行的应用程序,而线程呢,可以理解成我们应用程序中的代码的执行器。而他们的关系可想而知,线程是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程,只能隶属于一个进程。大家都知道,浏览器属于一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程,进程启动后,CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能。而在应用程序中,为了满足功能的需要,启动的进程会创建另外的新的进程来处理其他任务,这些创建出来的新的进程拥有全新的独立的内存空间,不能与原来的进程内向内存,如果这些进程之间需要通信,可以通过IPC机制(Inter Process Communication)来进行。 很多应用程序都会采取这种多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响,也就是说,当其中一个进程挂掉了之后,不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。

浏览器的多进程架构

假如我们去开发一个浏览器,它的架构可以是一个单进程多线程的应用程序,也可以是一个使用IPC通信的多进程应用程序。不同的浏览器使用不同的架构,下面主要以Chrome为例,介绍浏览器的多进程架构。在Chrome中,主要的进程有4个:

  • 浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。

  • 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。

  • 插件进程 (Plugin Process):负责控制网页使用到的插件

  • GPU进程 (GPU Process):负责处理整个应用程序的GPU任务 alt 这4个进程之间的关系是什么呢?首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候Browser Process会向这个URL发送请求,获取这个URL的HTML内容,然后将HTML交给Renderer ProcessRenderer Process解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process进行加载,同时通知Browser Process,需要Plugin Process加载插件资源,执行插件代码。解析完成后,Renderer Process计算得到图像帧,并将这些图像帧交给GPU ProcessGPU Process将其转化为图像显示屏幕。

alt

多进程架构的好处

Chrome为什么要使用多进程架构呢?第一,更高的容错性。当今WEB应用中,HTML,JavaScript和CSS日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG,而有些BUG会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。

alt 第二,更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠第三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。

多进程架构优化

之前的我们说到,Renderer Process的作用是负责一个Tab内的显示相关的工作,这就意味着,一个Tab,就会有一个Renderer Process,这些进程之间的内存无法进行共享,而不同进程的内存常常需要包含相同的内容。

浏览器的进程模式

为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。

  • Process-per-site-instance (default) - 同一个 site-instance 使用一个进程

  • Process-per-site - 同一个 site 使用一个进程

  • Process-per-tab - 每个 tab 使用一个进程

  • Single process - 所有 tab 共用一个进程

这里需要给出 site 和 site-instance 的定义

  • site 指的是相同的 registered domain name(如:google.com ,bbc.co.uk)和scheme (如:https://)。比如a.baidu.com和b.baidu.com就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。

  • site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance

    • 用户通过<a target="_blank">这种方式点击打开的新页面

    • JS代码打开的新页面(比如 window.open)

理解了概念之后,下面解释四个进程模式首先是Single process,顾名思义,单进程模式,所有tab都会使用同一个进程。接下来是Process-per-tab ,也是顾名思义,每打开一个tab,会新建一个进程。而对于Process-per-site,当你打开 a.baidu.com 页面,在打开 b.baidu.com 的页面,这两个页面的tab使用的是共一个进程,因为这两个页面的site相同,而如此一来,如果其中一个tab崩溃了,而另一个tab也会崩溃。Process-per-site-instance 是最重要的,因为这个是 Chrome 默认使用的模式,也就是几乎所有的用户都在用的模式。当你打开一个 tab 访问 a.baidu.com ,然后再打开一个 tab 访问 b.baidu.com,这两个 tab 会使用两个进程。而如果你在 a.baidu.com 中,通过JS代码打开了 b.baidu.com 页面,这两个 tab 会使用同一个进程。

默认模式选择

那么为什么浏览器使用Process-per-site-instance作为默认的进程模式呢?Process-per-site-instance兼容了性能与易用性,是一个比较中庸通用的模式。

  • 相较于 Process-per-tab,能够少开很多进程,就意味着更少的内存占用

  • 相较于 Process-per-site,能够更好的隔离相同域名下毫无关联的 tab,更加安全

导航过程都发生了什么

前面我们讲了浏览器的多进程架构,讲了多进程架构的各种好处,和Chrome是怎么优化多进程架构的,下面从用户浏览网页这一简单的场景,来深入了解进程和线程是如何呈现我们的网站页面的。

网页加载过程

之前我们我们提到,tab以外的大部分工作由浏览器进程Browser Process负责,针对工作的不同,Browser Process 划分出不同的工作线程:

  • UI thread:控制浏览器上的按钮及输入框;

  • network thread:处理网络请求,从网上获取数据;

  • storage thread:控制文件等的访问;

alt

第一步:处理输入

当我们在浏览器的地址栏输入内容按下回车时,UI thread会判断输入的内容是搜索关键词(search query)还是URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索URL,如果输入的内容是URL,则开始请求URL。

第二步:开始导航

回车按下后,UI thread将关键词搜索对应的URL或输入的URL交给网络线程Network thread,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求,如果收到服务器的301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

第三步:读取响应

network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。与此同时,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

第四步:查找渲染进程

各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。

第五步:提交导航

到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。

第六步:初始化加载完成

当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。

网页渲染原理

导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用户可进行交互的web页面。那么渲染进程是如何工作的呢?渲染进程中,包含线程分别是:

  • 一个主线程(main thread)

  • 多个工作线程(work thread)

  • 一个合成器线程(compositor thread)

  • 多个光栅化线程(raster thread)

不同的线程,有着不同的工作职责。

构建DOM

当渲染进程接受到导航的确认信息后,开始接受来自浏览器进程的数据,这个时候,主线程会解析数据转化为DOM(Document Object Model)对象。DOM为WEB开发人员通过JavaScript与网页进行交互的数据结构及API。

资源子加载

在构建DOM的过程中,会解析到图片、CSS、JavaScript脚本等资源,这些资源是需要从网络或者缓存中获取的,主线程在构建DOM过程中如果遇到了这些资源,逐一发起请求去获取,而为了提升效率,浏览器也会运行预加载扫描(preload scanner)程序,如果如果HTML中存在imglink等标签,预加载扫描程序会把这些请求传递给Browser Process的network thread进行资源下载。

JavaScript的下载与执行

构建DOM过程中,如果遇到<script>标签,渲染引擎会停止对HTML的解析,而去加载执行JS代码,原因在于JS代码可能会改变DOM的结构(比如执行document.write()等API)不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 async 或 defer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。

样式计算 - Style calculation

DOM树只是我们页面的结构,我们要知道页面长什么样子,我们还需要知道DOM的每一个节点的样式。主线程在解析页面时,遇到<style>标签或者<link>标签的CSS资源,会加载CSS代码,根据CSS代码确定每个DOM节点的计算样式(computed style)。计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认的样式。

布局 - Layout

DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中,会跳过隐藏的元素(display: none),另外,伪元素虽然在DOM上不可见,但是在布局树上是可见的。

绘制 - Paint

布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。

合成 - Compositing

文档结构、元素的样式、元素的几何关系、绘画顺序,这些信息我们都有了,这个时候如果要绘制一个页面,我们需要做的是把这些信息转化为显示器中的像素,这个转化的过程,叫做光栅化(rasterizing)。那我们要绘制一个页面,最简单的做法是只光栅化视口内(viewport)的网页内容,如果用户进行了页面滚动,就移动光栅帧(rastered frame)并且光栅化更多的内容以补上页面缺失的部分,如下:为了优化显示体验,合成线程可以给不同的光栅线程赋予不同的优先级,将那些在视口中的或者视口附近的层先被光栅化。当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)。

  • 绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息。

  • 合成帧:代表页面一个帧的内容的绘制四边形集合。

以上所有步骤完成后,合成线程就会通过IPC向浏览器进程(browser process)提交(commit)一个渲染帧。这个时候可能有另外一个合成帧被浏览器进程的UI线程(UI thread)提交以改变浏览器的UI。这些合成帧都会被发送给GPU从而展示在屏幕上。如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给GPU来更新页面。合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及JavaScript完成执行。这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。

浏览器对事件的处理

当页面渲染完毕以后,TAB内已经显示出了可交互的WEB页面,用户可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process,但是Browser Process只知道事件发生的类型和发生的位置,具体怎么对这个点击事件进行处理,还是由Tab内的Renderer Process进行的。Browser Process接受到事件后,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)。

渲染进程中合成器线程接收事件

前面我们说到,合成器线程可以独立于主线程之外通过已光栅化的层创建组合帧,例如页面滚动,如果没有对页面滚动绑定相关的事件,组合器线程可以独立于主线程创建组合帧,如果页面绑定了页面滚动事件,合成器线程会等待主线程进行事件处理后才会创建组合帧。那么,合成器线程是如何判断出这个事件是否需要路由给主线程处理的呢?由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。而对于非快速滚动区域的标记,开发者需要注意全局事件的绑定,比如我们使用事件委托,将目标元素的事件交给根元素body进行处理,代码如下:

document.body.addEventListener('touchstart', event => {  if (event.target === area) {    event.preventDefault()  }})

在开发者角度看,这一段代码没什么问题,但是从浏览器角度看,这一段代码给body元素绑定了事件监听器,也就意味着整个页面都被编辑为一个非快速滚动区域,这会使得即使你的页面的某些区域没有绑定任何事件,每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。其实这种情况也很好处理,只需要在事件监听时传递passtive参数为 true,passtive会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。

document.body.addEventListener('touchstart', event => {    if (event.target === area) {        event.preventDefault()    } }, {passive: true});

查找事件的目标对象(event target)

当合成器线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象。具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象。

浏览器对事件的优化

一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗。而对于非连续性的事件,如keydown,keyup,mousedown,mouseup,touchstart,touchend等,会直接派发给主线程去执行。

总结

浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析HTML构建DOM、构建过程加载子资源、下载并执行JS代码、样式计算、布局、绘制、合成,一步一步的构建出一个可交互的WEB页面,之后浏览器进程又接受页面的交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。本文大部分内容也是对inside look at modern web browser系列文章的整理、解读和翻译吧,整理过程还是收获非常大的,希望读者读了本文只有有所启发吧。

· 阅读需 4 分钟
const isObject = (obj) => obj !== null && typeof obj === 'object';

function merge(a, b) {
for (const attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
}
merge({}, JSON.parse('{"__proto__": {"admin": 1}}'));
console.log(({}).admin); // 1 触发原型污染

// 解决方案1 使用[jsonschema](https://imweb.io/topic/56b1b4bb5c49f9d377ed8ee9)对传入的JSON做格式校验
// 解决方案2 Object.freeze(Object.prototype);
// 解决方案3 在上面的merge函数中对key做判断
// 解决方案4 使用无对象原型Object.create(null)
// 解决方案5 使用Map来存取对象

众所周知js通过原型链串起了js的父类和子类,实例和构造函数,所有的对象可以说都是挂载一颗原型树下的。那么理论上如果一个原型发生了变化(比如增加或者删除了一属性),所有依赖于该原型的子对象都会受到影响,原型污染说的就是这件事情。

上述的例子中就展示了js的原型污染。

merge会递归的把b中的属性存入a中,当一个空对象与一个key为proto的对象合并时,虽然使用for in对空对象遍历时不会出现proto属性,但是上面是对另一个对象进行for in 遍历,是能够遍历出proto的key的,而{}.proto虽然不会被遍历到,但是是可以取到的,并且也是一个对象,那么就会进行更深层的合并,之后会把admin属性挂载到Object.prototype上面,造成原型污染,导致之后新建的对象可以通过原型链获取到admin属性

为了解决原型污染的问题

第一,可以通过限制用户的json输入,来获得我们想要的json格式的json数据,比如'{"proto": {"admin": 1}}' 就不是我们想要的json数据,那么可以使用jsonschema库来限制json输入

第二,因为上面我们进行merge的目标对象是个空对象,所以才会出现在访问proto时通过原型链造成了污染,那么可以将空对象替换为Object.create(null),使a.proto判断为undefined而不是一个对象

第三,因为上述merge函数对对象属性的无差别遍历导致了原型污染,所以可以在深层合并时对attr做进一步的判断,如果key为proto,就不进行深层遍历

第四,我们对Object.prototype的修改造成了Object.prototype的改变(说的有点奇怪),是因为Object.prototype是可以被修改的,所以可以通过Object.freeze(Object.prototype)来防止Object.prototype被改变

第五,虽然和现在讨论的对象污染没太大关系,可以使用Map数据结构来替代对象进行存取,好处有两点:一,key不仅限于string和symbol类型,二,存取更快,底层对其进行了优化

· 阅读需 7 分钟

new

new 关键字作为 JS 中通过正常途径实例化一个类的唯一方法,经常出现在我们的视线中,那么其原理又是如何呢?它到底做了什么事情?

先看一段代码

// 定义类 类名字是 classA
function classA() {
this.name = 1;
}
classA.prototype.show = function() {
alert(this.name);
};
// 用new实例化
var b = new classA();
b.show();

这是一个常见的使用 new 创建实例的例子

new 其实干了如下几件事

  1. 创建了一个对象 a(a 是随便取的)
  2. 将该对象的proto属性设置为 classA 的 prototype
  3. 将 classA 中的 this 指向对象 a,也就是之后的this.name=1等同于a.name=1
  4. 执行 classA 函数
  5. 将对象 a 返回出去

接下来通过 JS 模拟一下 new 的操作

function classA() {
this.name = "start";
}
classA.prototype.sayName = function() {
console.log(this.name);
};

function NEW(className) {
let res = {}; // 新建一个对象
res.__proto__ = className.prototype; // 通过原型链继承classA的方法
className.call(res); // 通过call修改classA的this指向,并且调用classA这个函数,在这个过程中内部的name属性会被赋值
return res;
}
let a = NEW(classA);
a.sayName();
a.name = "a";
a.sayName();

以上就是new的作用,如果在实例化的时候忘记使用new了,其实就是简单地调用了一下那个函数,函数内部的 this 其实指向的是外部,

function classA() {
this.name = "start";
}
classA.prototype.sayName = function() {
console.log(this.name);
};
let a = classA();
try {
a.sayName();
} catch (e) {
console.log(e);
}
console.log(name);

上述代码运行后会发生报错,打印的namestart,可见this默认被绑到了外部(浏览器端为 window,nodejs 端为 global)

注意以上是在函数体内,如果在全局环境中浏览器的依然为window,但是 nodejs 端为{}

由以上的所讲的new的原理,进一步,我们可以解释如下现象

function classA() {
this.name = "start";
let obj = { a: 1 };
return obj;
}
classA.prototype.sayName = function() {
console.log(this.name);
};
let a = classA();
console.log(a.a); // 1
a.sayName(); // 报错

因为在return最后的res之前,使用call调用了classA这个函数,结果直接返回出来了,因此所获得的并不是我们想要的结果

构造函数中绝对不能 return 数据

浅拷贝VS深拷贝

function isReferenceType (o) {
return o instanceof Object
}

function deepClone (obj) {
if (!isReferenceType(obj)) {
return obj
}

if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Function) return obj.bind({})

let newObj = new obj.constructor()
Reflect.ownKeys(obj).forEach(key => {
newObj[key] = deepClone(obj[key])
})

return newObj
}

let arr = [1, 2, 3];
arr.push(arr);
var obj = {
a: 1,
arr,
func: () => {
console.log("aaa");
},
date: new Date(),
reg: new RegExp(/test/),
none: undefined,
};
obj.obj = obj;
console.log(deepClone(obj));
// return JSON.parse(JSON.stringify(obj))
// }
// console.log(jsonDeepClone({
// a: 1,
// func: ()=>{
// console.log('aaa');
// },
// date: new Date(),
// reg: new RegExp(/test/),
// none: undefined
// }));

callapply

Function.prototype.call = function() {
let [context, ...args] = [...arguments];
if (!context) {
context = typeof window === "undefined" ? global : window;
}
// 注意,这里必须要context.func 不能是一个简单的变量,因为需要给函数一个运行的上下文,即context
context.func = this;
let res = context.func(...args);
delete context.func;
// context中实际并无func这个key,要删除恢复原样
return res;
};
Function.prototype.apply = function() {
let [context, args] = arguments;
if (!context) {
context = typeof window === "undefined" ? global : window;
}
context.func = this;
let res;
if (!args) res = context.func();
else res = context.func(...args);
delete context.func;
return res;
};

function getName(age, address) {
console.log(this.name);
console.log({ age, address });
}
let obj = {
name: "teefing",
};
getName.apply(obj, [22, "hangzhou"]);
getName.call(obj, 22, "hangzhou");

函数柯里化

const curry = (fn, ...args) =>
args.length < fn.length
? (...arguments) => curry(fn, ...args, ...arguments)
: fn(...args);
function sumFn(a, b, c) {
return a + b + c;
}
var sum = curry(sumFn);
console.log(sum(1)(2)(3)); //6

let add1 = sum(1);
let add2 = add1(2);
let res = add2(3);
console.log(res); //6

注: Function.length返回一个函数的参数个数,但是剩余参数不会算上,还有第一个默认参数及其之后的参数不会被算上

函数柯里化主要作用

  1. 参数复用
  2. 提前返回 – 返回接受余下的参数且返回结果的新函数
  3. 延迟执行 – 返回新函数,等待执行

flat

let flattenDeep1 = function(arr, deepLength) {
return arr.flat(deepLength, Math.pow(2, 53) - 1);
};

let flattenDeep2 = (arr) =>
arr.reduce(
(acc, val) =>
Array.isArray(val) ? acc.concat(flattenDeep2(val)) : acc.concat(val),
[]
);

let flattenDeep3 = (arr) => {
let stack = [...arr];
let res = [];
while (stack.length) {
let next = stack.shift();
if (Array.isArray(next)) {
res = res.concat(flattenDeep4(next));
} else {
res.push(next);
}
}
return res;
};

let flattenDeep4 = (arr) => {
let stack = [...arr];
let res = [];
while (stack.length) {
const next = stack.shift();
if (Array.isArray(next)) {
stack.unshift(...next);
} else {
res.push(next);
}
}
return res;
};
[flattenDeep1, flattenDeep2, flattenDeep3, flattenDeep4].forEach(
(flattenDeep, index) => {
try {
console.log(flattenDeep([1, 2, [3, 4, 5, [6, 7, 8]]]));
} catch (e) {
console.log(`method-${index} not supported`);
}
}
);

uniq函数

let uniq1 = (arr) => [...new Set(arr)];

let uniq2 = (arr) => {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
};

let uniq3 = (arr) => {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) {
res.push(arr[i]);
}
}
return res;
};

let uniq4 = (arr) =>
arr.reduce((acc, val) => (acc.includes(val) ? acc : [...acc, val]), []);

let uniq5 = (arr) => {
let m = new Map();
let res = [];
for (let i = 0; i < arr.length; i++) {
if (m.has(arr[i])) m.set(arr[i], true);
// true和false可以判断是否只存在一次
else {
m.set(arr[i], false);
res.push(arr[i]);
}
}
return res;
};
[uniq1, uniq2, uniq3, uniq4, uniq5].forEach((uniq, index) => {
console.log(uniq([1, 1, 2, 2, 3, 3, 4]));
});

· 阅读需 4 分钟

前言

项目开发中经常需要用到元素大小,网页高度,视口高度,各种距离等等,本文总结了获取各种宽高、距离的方法。

元素大小

祭出这几张神图,简单明了又清晰😄

各种方法总结

注意:这些方法都不考虑混杂模式,请确保你的文档在标准模式下(填写了正确的 doctype)否则获取的值会不准。

1. 获取整个网页高度与宽度

代码说明

1. 火狐不兼容 document.body,所以使用 document.documentElement

2. 理论上没有滚动条时 scrollHeight 和 clientHeight 相同,事实上各种浏览器有不同的处理,所以最保险的方法是取这俩中最大者。

function getPagearea(){
return {
width: Math.max(document.documentElement.scrollWidth,
document.documentElement.clientWidth),
height: Math.max(document.documentElement.scrollHeight,
document.documentElement.clientHeight)
}
}

PS:jq 的话  $(document).height();          $(document).width();

2. 获取视口高度与宽度

代码说明

1. 同上,火狐不兼容 document.body,所以使用 document.documentElement

function getViewport() {
   return {
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
   }
}

PS:jq 的话 $(window).height();           $(window).width();

3. 获取元素距页面高度

function getElementTop(el) {
 let actualTop = el.offsetTop;
 let current = el.offsetParent;

while (current !== null) {
   actualTop += current.offsetTop;
   current = current.offsetParent;
 }
 return actualTop;
}

PS:jq 的话 jq 对象. offset().top          jq 对象. offset().left

4. 获取元素到视口的距离

使用 el.getBoundingClientRect 方法 

getBoundingClientRect 方法返回元素的大小及其相对于视口的位置。  

5. 获取纵向滚动条高度或横向滚动条长度

代码说明

同 1,火狐不兼容 document.body,所以使用 document.documentElement

function getScrollTop() {
let doc = document;
return Math.max(doc.body.scrollTop, doc.documentElement.scrollTop)
};

function getScrollLeft() {
let doc = document;
return Math.max(doc.body.scrollLeft, doc.documentElement.scrollLeft)
};

6. 获取鼠标到元素、视口、文档、屏幕距离

这种主要是读取 event 对象中的值,具体看下图比较清晰。

一个使用例子

上下滚动时判断元素在视口中出现

这个例子使用到了上面的方法

document.onscroll = () => {
let dom = document.getElementById('box');
let top = getElementTop(dom); // 元素距页面高度
let scrollTop = getScrollTop(); // 获取滚动条高度
let viewPortHeight = getViewport().height; // 获取视口宽高

if (top > scrollTop && top <= scrollTop + viewPortHeight) {
console.log('元素出现')
}
}

// 写法2:配合getBoundingClientRect判断
document.onscroll = () => {
let dom = document.getElementById('box2');
let rectTop = dom.getBoundingClientRect().top;
let viewPortHeight = getViewport().height;

if (rectTop > 0 && rectTop < viewPortHeight) {
console.log('元素出现')
}
}

// 用jq的话
document.onscroll = () => {
let $dom = $('#box');
let top = $dom.offset().top;
let scrollTop = $(window).scrollTop();
let viewPortHeight = $(window).height();

if (top > scrollTop && top <= scrollTop + viewPort.height) {
console.log('元素出现')
}
}

· 阅读需 5 分钟

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

为什么要用事件委托

一般来说,dom 需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的 dom 需要添加事件处理呢?比如我们有 100 个 li,每个 li 都有相同的 click 点击事件,可能我们会用 for 循环的方法,来遍历所有的 li,然后给它们添加事件,那这么做会存在什么影响呢?

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与 dom 节点进行交互,访问 dom 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因;如果要用事件委托,就会将所有的操作放到 js 程序里面,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤,哈哈),比如上面的 100 个 li,就要占用 100 个内存空间,如果是 1000 个,10000 个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。

总而言之,就是可以提高性能

实现

<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function() {
var oUl = document.getElementById("ul1");
oUl.onclick = function(e) {
alert(123);
};
};

这样当我们点击 li 的时候也是会 alert 的,但是当我们点击 ul 的时候也会触发,如果想让他不触发,怎么办呢?

当我们点击 li 的时候,会产生一个点击事件,这个点击事件当被点下后就确定了,因为冒泡,就会先触发 li 的 click 事件(如果有的话),再触发 ul 的点击事件,但是传入这两个点击事假的 e 是不变的,从这个 e 中我们就可以判断到底是 ul 点击的还是 li 点击的,从而进行特定的操作

window.onload = function() {
var oUl = document.getElementById("ul1");
oUl.onclick = function(ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == "li") {
alert(123);
alert(target.innerHTML);
}
};
};

这样就只有点击 li 会触发 alert 了

other

使用事件委托还有一个好处,正常情况下我们给 li 添加了点击事件后,如果又新增了 li,新的 li 是没有点击事件的,而使用事件委托因为是交给的父元素进行事件的触发,所以新增多少 li 都没有关系

给父容器绑定事件,判断 target 是哪个子元素,做相关操作 JQuery 的 delegate 和 on 就是这么干的

· 阅读需 3 分钟

1. 方法概述

 map() 方法返回一个由原数组中的每个元素调用一个指定方法后的返回值组成的新数组。

2. 例子

2.1 在字符串中使用map###

在一个 String 上使用 map 方法获取字符串中每个字符所对应的 ASCII 码组成的数组:

var map = Array.prototype.map
var a = map.call("Hello World", function(x) { return x.charCodeAt(0); })
// a的值为[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

2.2 易犯错误###

通常情况下,map 方法中的 callback 函数只需要接受一个参数(很多时候,自定义的函数形参只有一个),就是正在被遍历的数组元素本身。

但这并不意味着 map 只给 callback 传了一个参数(会传递3个参数)。这个思维惯性可能会让我们犯一个很容易犯的错误。

// 下面的语句返回什么呢:
["1", "2", "3"].map(parseInt);
// 你可能觉的会是[1, 2, 3]
// 但实际的结果是 [1, NaN, NaN]

// 通常使用parseInt时,只需要传递一个参数.但实际上,parseInt可以有两个参数.第二个参数是进制数.可以通过语句"alert(parseInt.length)===2"来验证.
// map方法在调用callback函数时,会给它传递三个参数:当前正在遍历的元素, 元素索引, 原数组本身.
// 第三个参数parseInt会忽视, 但第二个参数不会,也就是说,parseInt把传过来的索引值当成进制数来使用.从而返回了NaN.

/*
//应该使用如下的用户函数returnInt

function returnInt(element){
return parseInt(element,10);
}

["1", "2", "3"].map(returnInt);
// 返回[1,2,3]

· 阅读需 1 分钟

事件冒泡是自下而上的去触发事件

<div id="parent">
<div id="child" class="child"></div>
</div>
document.getElementById("parent").addEventListener("click", function(e) {
alert("parent 事件被触发," + this.id);
});
document.getElementById("child").addEventListener("click", function(e) {
alert("child 事件被触发," + this.id);
});
child事件被触发,child
parent事件被触发,parent

事件捕获

事件捕获指的是从 document 到触发事件的那个节点,即自上而下的去触发事件

现在改变第三个参数的值为 true

document.getElementById("parent").addEventListener(
"click",
function(e) {
alert("parent事件被触发," + e.target.id);
},
true
);
document.getElementById("child").addEventListener(
"click",
function(e) {
alert("child事件被触发," + e.target.id);
},
true
);

parent 事件被触发,parent child 事件被触发,child

other

需要记忆的是 blur、focus、load、unload、media 相关事件是不会冒泡的

事件冒泡的应用: 事件委托