一、原理实现
1.1 Object.create()
function myCreate(proto) {
function F() {}; // 1.构造函数
F.prototype = proto; // 2.指定prototype
return new F(); // 3.返回实例
}
1.2 instanceof()
function myInstanceOf(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
let prototype = constructor.prototype;
while (true) {
if (!proto) { // 结束条件:原型链找到头了,null
return false;
}
if (proto === prototype) { // 结束条件:原型链中找到了constructor的原型
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
1.3.new 操作符
function myNew() {
let constructor = arguments.shift();
// 参数判断
if (typeof constructor !== "function") {
console.error('type error');
return;
}
// 创建一个空对象,并指定原型
const obj = Object.create(constructor.prototype);
// 绑定this,执行构造函数绑定方法属性
const result = constructor.apply(obj, arguments);
if(result && ['function', 'object'].includes(typeof result) ){
return result
} else {
return null
}
}
1.4. call
call 函数的实现步骤:
- 1.判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 2.判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 3.处理传入的参数,截取第一个参数后的所有参数。
- 4.将函数作为上下文对象的一个属性。
- 5.使用上下文对象来调用这个方法,并保存返回结果。
- 6.删除刚才新增的属性。
- 7.返回结果。
function myCall() {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
return;
}
// 判断 context 是否传入,如果未传入则设置为 window
const context = Array.prototype.shift.call(arguments) || window;
let result = null;
// 将调用函数设为context对象的方法
context.fn = this;
// 调用方法
result = context.fn(...arguments)
// 将属性删除
delete context.fn;
return result;
}
1.5. apply
apply 函数的实现步骤:
- 1.判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 2.判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 3.将函数作为上下文对象的一个属性。
- 4.判断参数值是否传入
- 4.使用上下文对象来调用这个方法,并保存返回结果。
- 5.删除刚才新增的属性
- 6.返回结果
function myApply() {
// 判断调用对象
if(typeof this !== "function"){
console.error("type error")
return;
}
// 判断 context 是否传入,如果未传入则设置为 window
const context = Array.prototype.shift.call(arguments) || window;
let result = null;
// 将调用函数设为context对象的方法
context.fn = this;
// 调用方法
result = context.fn([...arguments])
// 将属性删除
delete context.fn;
return result;
}
1.6. bind
bind 函数的实现步骤:
- 1.判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 2.保存当前函数的引用,获取其余传入参数值。
- 3.创建一个函数返回
- 4.函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
function myBind() {
// 判断调用对象
if(typeof this !== "function"){
console.error("type error")
return;
}
// 固定指向函数
const fn = this;
// 获取绑定对象context
const context = Array.prototype.shift.call(arguments);
// 获取参数
const args = [...arguments]
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply( this instanceOf Fn ? this : context, [...args, ...arguments])
}
}
1.7. setTimeout实现setInterval
启动一个定时器,时间到了执行封装的回调; 回调中,执行callback,并且套娃启动新的定时器
function myInterval(callback, timeout) {
let timer = {
flag: true
}
function interval() {
if(timer.flag) {
callback()
setTimeout(interval, timeout)
}
}
setTimeout(interval, timeout)
return timer
}
1.8.Promise
const PENDDING = 'pendding';
const REJECTED = 'rejected';
const RESOLVED = 'resolved';
function MyPromise (fn) {
// 保存初始化状态
let self = this;
// 初始化状态
this.state = PENDDING;
// 用于保存 resolve 或者 rejected 传入的值
this.value = null;
// 用于保存 resolve 的回调函数
this.resolveCallbacks = [];
// 用于保存 reject 的回调函数
this.rejectCallbacks = [];
// 状态转变为 resolved 方法
function resolve(value) {
// 传入的元素是否为Promise
// 如果是,等前面一个状态改变后再改变
if (value instanceOf MyPromise) {
reutrn value.then(resolve, reject)
};
//保证代码的执行为本轮事件循环的末尾
setTimeout(() => {
// 只有pendding状态才能调整
if (self.state === PENDDING) {
self.state = RESOLVED;
self.value = value; //传入promise的resolve的值
// 执行回调
self.resolvedCallbacks.forEach(callback => {
callback(value)
})
}
}, 0)
}
// 状态转变为 rejected 方法
function reject(value) {
//保证代码的执行为本轮事件循环的末尾
setTimeout(() => {
// 只有pendding状态才能调整
if (self.state === PENDDING) {
self.state = REJECTED;
self.value = value; //传入promise的resolve的值
// 执行回调
self.rejectedCallbacks.forEach(callback => {
callback(value)
})
}
}, 0)
}
try{
fn(resolve, reject)
} catch(err) {
reject(err)
}
}
MyPromise.prototype.then(onResolved, onRejected) {
onResolved = typeof onResolved === "function" ?
onResolved :
function(value){
return value
}
onRejected = typeof onRejected === "function" ?
onRejected :
function(value) {
return value
}
// 如果是等待状态,则将函数加入对应列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) {
onResolved(this.value);
}
if (this.state === REJECTED) {
onRejected(this.value);
}
}
1.9. 观察者模式 / EventEmitter
const events = (function() {
let topics = {};
return {
on: function(topic, callback) {
if (!topics.hasOwnProperty(topic)) {
topics[topic] = []
}
topics[topic].push(callback)
},
emit: function(topic) {
const args = argumens.slice(1)
if(topics.hasOwnProperty(topic)) {
topics[topic].forEach(item => {
item(...args)
})
}
},
remove: function(topic, callback) {
if(!topics.hasOwnProterty(topic)){
return;
}
topics[topic] = topics[topic].filter(item => item !== callback);
},
}
})()
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
let callbacks = this.events[event] || [];
callbacks.push(callback);
this.events[event] = callbacks;
return this;
}
off(event, callback) {
let callbacks = this.events[event];
this.events[event] = callbacks && callbacks.filter(fn => fn !== callback);
return this;
}
emit(event, ...args) {
let callbacks = this.events[event];
callbacks.forEach(fn => {
fn(...args);
});
return this;
}
once(event, callback) {
let wrapFun = (...args) => {
callback(...args);
this.off(event, wrapFun);
};
this.on(event, wrapFun);
return this;
}
}
1.10.函数柯里化
// 支持多参数传递
function progressCurrying(fn, args) {
var _this = this
var len = fn.length;
var args = args || [];
return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(_args, args);
// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 参数收集完毕,则执行fn
return fn.apply(_this, _args);
}
}
ES6写法
function currying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
// 剩余参数
return (...args2) => currying(fn, ...args, ...args2);
}
}
反柯里化 反柯里化,将柯里化过后的函数反转回来,由原先的接受单个参数的几个调用转变为接受多个参数的单个调用 一种简单的实现方法是:将多个参数一次性传给柯里化的函数,因为我们的柯里化函数本身就支持多个参数的传入处理,反柯里化调用时,仅使用“一次调用”即可。
// 反柯里化
function uncurry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
var arg = [].slice.call(arguments);
args = args.concat(arg);
return fn.apply(this, args);
}
}
var uncurryAdd = uncurry(curryAdd);
console.log(uncurryAdd(1, 2, 3, 4)); // 10
1.11 实现一个Promise.prototype.finally
Promise.prototype.myFinally = function (callback) {
// this指向 Promise.prototype
// this.constructor 指向 Promise
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
err => P.reject(callback()).then(() => {
throw new Error(err)
})
)
}
1.12 实现一个Promise.race()
function _race(p) {
return new Promise((resolve, reject) => {
if(!Array.isArray(p)) { //不严谨。因为类数组也可以,所有具备Iterator接口的都可以
return reject(new TypeError("argument must be an array"))
}
p.forEach(item => {
Promise.then(item).then(resolve, reject) // 调用的是最外层的resolve和reject
})
})
}
1.13 实现一个Promise.All()
// 靠计数来确认全部执行完毕
function promiseAll(promises) {
return new Promise(function (resolve, reject) {
if (!Array.isArray(promises)) {
return reject(new TypeError("argument must be an array"))
}
let countNum = 0;
let promiseNum = promises.length;
let resolvedValue = new Array(promiseNum);
for (let i = 0; i < promiseNum; i++) {
// 注意闭包(用的let块作用域)
Promise.resolve(promises[i]).then(function (val) {
countNum++;
resolvedValue[i] = val;
if (countNum === promiseNum) {
return resolve(resolvedValue)
}
}, function (err) {
return reject(err)
})
}
})
}
1.14 数组flatten
let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]]
function myFlatten(arr) {
let arrs = [...arr];
let newArr = [];
while(arrs.length) {
let item = arrs.shift();
if(Array.isArray(item)) {
arrs.unshift(...item)
} else {
newArr.push(item)
}
}
return newArr
}
# 递归实现
function dfsFlatten(arr) {
const attrs = [];
arr.forEach(item => {
if(Array.isArray(item)) {
arrs.push(...dfsFlatten(item))
} else {
arrs.push(item)
}
})
return attrs;
}
1.15 js深浅拷贝
注意,一定是自身属性,但for in中有继承的属性,所以要剔除掉。
# 浅拷贝
function shallowCopy(object){
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
# 深拷贝
function deepCopy(obj) {
//普通值
if (!obj || typeof obj !== "object") return obj;
let newObject = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObject[key] = deepCopy(obj[key]);
}
}
return newObject
}
1.16 防抖
函数防抖: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。
防抖分为两种:
-
非立即执行版: 事件触发 - > 延时 - > 执行回调函数;
如果在延时中, 继续触发事件, 则会重新进行延时.在延时结束后执行回调函数.常见例子: 就是input搜索框, 客户输完过一会就会自动搜索 -
立即执行版: 事件触发 - > 执行回调函数 - > 延时; 如果在延时中, 继续触发事件, 则会重新进行延时.在延时结束后, 并不会执行回调函数.常见例子: 就是对于按钮防点击.例如点赞, 心标, 收藏等有立即反馈的按钮.
# 1非立即执行版
function debounce1(fn, delay) {
let timer = null;
return function() {
const that = this;
const args = Array.prototype.slice.call(arguments);
if(timer){
clearTimeout(timer)
}
timer = setTimeout(function() {
fn.call(that, args)
}, delay)
}
}
debounce1(fn, 100)(...balabala)
# 2立即执行版
function debounce2(fn, delay) {
let timer = null;
return function() {
const that = this;
const args = Array.prototype.slice.call(arguments);
if(timer){
clearTimeout(timer)
}
fn.apply(that, args)
timer = setTimeout(function() {
timer = null;
}, delay)
}
}
1.17 节流
函数节流: 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
节流有两种实现方式
1)时间戳方式: 通过闭包保存上一次的时间戳, 然后与事件触发的时间戳比较.如果大于规定时间, 则执行回调.否则就什么都不处理.
特点: 一般第一次会立即执行, 之后连续频繁地触发事件, 也是超过了规定时间才会执行一次。 最后一次触发事件, 也不会执行(说明: 如果你最后一次触发时间大于规定时间, 这样就算不上连续频繁触发了).
2) 定时器方式: 原理与防抖类似.通过闭包保存上一次定时器状态.然后事件触发时, 如果定时器为null(即代表此时间隔已经大于规定时间), 则设置新的定时器.到时间后执行回调函数, 并将定时器置为null.
特点: 当第一次触发事件时, 不会立即执行函数, 到了规定时间后才会执行。 之后连续频繁地触发事件, 也是到了规定时间才会执行一次(因为定时器)。 当最后一次停止触发后, 由于定时器的延时, 还会执行一次回调函数(那也是上一次成功成功触发执行的回调, 而不是你最后一次触发产生的)。 一句话总结就是延时回调, 你能看到的回调都是上次成功触发产生的, 而不是你此刻触发产生的.
这两者最大的区别: 是时间戳版的函数触发是在规定时间开始的时候, 而定时器版的函数触发是在规定时间结束的时候。
# 1 时间戳版
function throttle(fun, delay = 500) {
let previous = 0; //记录上一次触发的时间戳.这里初始设为0,是为了确保第一次触发产生回调
return function () {
let now = Date.now(); //记录此刻触发时的时间戳
let that = this;
let _args = Array.prototype.slice.call(arguments);
if (now - previous > delay) { //如果时间差大于规定时间,则触发
fun.apply(that, _args);
previous = now;
}
}
}
# 2 定时器版
function throttle(fun, delay = 500) {
let timer;
let _args = args;
return function (args) {
let that = this;
if (!timer) { //如果定时器不存在,则设置新的定时器,到时后,才执行回调,并将定时器设为null
timer = setTimeout(function () {
timer = null;
fun.apply(that, _args)
}, delay)
}
}
}
1.18 list转map
function listToMap(list){
if (!list || !list.length){
return {};
}
let root = null;
list.forEach(it => {
let PNode = list.filter(item => item.id === it.pid)[0];
if (PNode){
PNode.children ? (PNode.children.push(it)) : (PNode.children = [it]);
} else {
root = it
}
})
return root;
}
1.19 数组随机打乱
function randomSort(list) {
for(let i = 0; i < list.length; i++) {
let rIdx = Math.floor(Math.random() * list.length)
[list[i], list[rIdx]] = [list[rIdx],list[i]]
}
return list
}
1.20 LazyMan
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch
LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
解答:
class LazyManClass {
constructor(name) {
this.taskList = [];
this.name = name;
console.log(`Hi I am ${this.name}`);=
setTimeout(() => {
this.next();
}, 0);
}
eat (name) {
var that = this;
var fn = (function (n) {
return function () {
console.log(`I am eating ${n}`)
that.next();
}
})(name);
this.taskList.push(fn);
return this; **//必须得返回实例 才能链式编程**
}
sleepFirst (time) {
var that = this;
var fn = (function (t) {
return function () {
setTimeout(() => {
console.log(`等待了${t}秒...`)
that.next();
}, t * 1000);
}
})(time);
this.taskList.unshift(fn);
return this;
}
sleep (time) {
var that = this
var fn = (function (t) {
return function () {
setTimeout(() => {
console.log(`等待了${t}秒...`)
that.next();
}, t * 1000);
}
})(time);
this.taskList.push(fn);
return this;
}
next () {//直接执行******
var fn = this.taskList.shift();
fn && fn();
}
}
function LazyMan(name) {
return new LazyManClass(name);
}
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(4).eat('junk food');****
二、 应用
2.1 XHR封装
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;
}
2.2 常用正则表达式
// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/
// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g
// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g
// (5)用户名正则
var regex = /^[a-zA-Z$][a-zA-Z0-9_$]{4,16}$/
// (6)Email正则
var ePattern = /^([A-Za-z0-9_-.])+@([A-Za-z0-9_-.])+.([A-Za-z]{2,4})$/
2.3 使用闭包实现每隔一秒打印 1,2,3,4
function console1234() {
// # 法1 利用闭包
for (var i = 1; i < 5; i++) {
(function (m) {
setTimeout(function () {
(function () {
console.log(m)
})()
}, m * 1000)
})(i) //闭包固定i
}
// # 法2 利用定时器api
for (var i = 1; i < 5; i++) {
setTimeout(function (m) {
console.log(m)
}, i * 1000, i)
}
// # 法3 利用let块级作用域
for (let i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
}
2.4 如何将浮点数点左边的数每三位添加一个逗号,如 12000000.11 转化为『12,000,000.11』?
// 方法一
function format(number) {
return number && number.replace(/(?!^)(?=(\d{3})+.)/g, ",");
}
// 方法二
function format2(number) {
return number.toLocaleString('en')
}
2.5 ["1", "2", "3"].map(parseInt) 答案是多少?
map 遍历参数 ==> ("1", 0) , ("2", 1), ("3", 2)
parseInt(),
("1", 0) 第二个参数为0,默认 10 进制,所以先输出一个 1
("2", 1) 第二个参数为1,默认 1 进制,所以 返回NaN
("3", 2) 第二个参数为1,默认 2 进制,所以 返回NaN
2.6 js 中倒计时的纠偏实现
在前端实现中我们一般通过 setTimeout 和 setInterval 方法来实现一个倒计时效果。
但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的,
setTimeout 和 setInterval 的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,
它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。
一般解决倒计时中的误差的有这样两种办法:
(1)第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。
(2)第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。
这一种方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。
每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。
然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,
因此我们在设置下一个定时器的间隔大小的时候,
我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。
2.7 一个列表,假设有 100000 个数据,这个该怎么办?
我们需要思考的问题:该处理是否必须同步完成?数据是否必须按顺序完成?
解决办法:
(1)将数据分页,利用分页的原理,每次服务器端只返回一定数目的数据,浏览器每次只对一部分进行加载。
(2)使用懒加载的方法,每次加载一部分数据,其余数据当需要使用时再去加载。
(3)使用数组分块技术,基本思路是为要处理的项目创建一个队列,然后设置定时器每过一段时间取出一部分数据,
然后再使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
2.8 把俩个数组 [A1, A2, B1, B2, C1, C2, D1, D2] 和 [A, B, C, D],合并为 [A1, A2, A, B1, B2, B, C1, C2, C, D1, D2, D]
# 法1
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})
let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})
# 法2
var arr1 = ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]
var arr2 = ["A", "B", "C", "D"]
var arr3 = arr1.concat(arr2);
arr3.sort().sort(function(a,b){
if (a.charAt(0) == b.charAt(0) && a.length > b.length){
return -1 //小于零的时候不需要调整顺序
}
})
2.9 具名自执行函数变量本身是只读的
var b = 10;
(function b() {
// 内部作用域,会先去查找是否已有变量b的声明,有就直接赋值20,
// 确实有了呀。发现了具名函数 function b(){},//拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();
所以严格模式下能看到错误:Uncaught TypeError: Assignment to constant variable
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})()
// "Uncaught TypeError: Assignment to constant variable."
# 有window的情况
var b = 10;
(function b() {
window.b = 20;
console.log(b); // [Function b]
console.log(window.b); // 20是必然的
})();
# 有var声明的情况:
var b = 10;
(function b() {
var b = 20; // IIFE内部变量
console.log(b); // 20
console.log(window.b); // 10
})();
解释: 具名自执行函数的变量本身为只读属性,不可修改,非匿名自执行函数,函数名只读。
IIFE中的函数是函数表达式,而不是函数声明。
函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
函数表达式中带函数名称的,函数名称只能作为常量在函数体内访问,不可以被重新赋值的。
2.10 同时成立 a == 1 && a == 2 && a == 3
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
# 法1
var a = {
i: 1,
toString() {
return a.i++;
}
}
# 法2
var a = {
i: 1,
valueOf() {
return a.i++;
}
}
# 法3
数组这个就有点妖了
var a = [1,2,3];
a.join = a.shift;
2.11 a.x = a = {n: 2}的执行
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x) // undefined
console.log(b.x) // {n: 2}
解释:
1、优先级。
**.的优先级高于=**,所以先执行a.x,**堆内存中的{n: 1}就会变成{n: 1, x: undefined}** ,
改变之后相应的b.x也变化了,因为指向的是同一个对象。a.x表示一个固定指针,指向{n:1}这个堆中。
2、赋值操作是从右到左,所以先执行a = {n: 2},a的引用就被改变了,
(此时不影响左侧a.x,可以理解为其已经解析成固定的内存指向)
(**之后执行a.x = {n:2}的时候,** **并不会重新解析一遍a,而是沿用最初解析a.x时候的a** **,也即旧对象**)
然后这个返回值又赋值给了a.x,
需要注意的是这时候a.x是第一步中的{n: 1, x: undefined}那个对象,
其实就是b.x,相当于b.x = {n: 2}
2.12 a.b.c.d 和 a['b']['c']['d'],哪个性能更高
应该是 a.b.c.d 比 a['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况,
再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点
2.13 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因?
for 循环没有任何额外的函数调用栈和上下文;
forEach函数签名实际上是array.forEach(function(currentValue, index, arr), thisValue)
它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
for循环是常见的循环语句forEach和map是在ES5出的,但是在性能上后者不如前者,*
*在次数少的情况下forEach会比for要快,但是到达了十万次时forEach明显就跟不上了**。
在大数据量的情况下for循环的兼容性和多环境运行表现比较优秀,
forEach的优点是在数据量小时占优势,语义话更简洁。
循环时没有返回值。map和forEach差不多但是map循环有返回值。
2.14 new的时候会执行构造函数(全部)
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() { //*step1
console.log(4)
}
Foo.a(); //此时实在Foo对象上添加了静态a方法,
let obj = new Foo();
// new的时候会重新执行Foo构造函数,
// 此时构造函数中就不会有后面的*step1这步的静态方法。因为他不在函数中。
obj.a();
Foo.a();
输出顺序是 4 2 1 .
let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/
new 的时候会将函数的主函数(Foo)执行一遍,并将原型挂载至新的函数上
Foo函数内部由于同样有Foo.a = function ...,new的时候Foo.a的值相当于被重置了,则输出1
因执行了一次new 操作,故再次调用Foo.a()时调用了Foo函数体内的静态方法a。
obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2
Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1