JS
Object.create
根据给定对象,创建以该对象为原型的新对象
// 简单模拟实现Object.create
function create(proto) {
const Noop = function() {};
Noop.prototype = proto;
return new Noop();
}
new
new语句执行过程做了以下事情:
- 创建一个新对象;
- 该对象会执行
[[Prototype]]
(即__proto__
)链接; - 将构造函数的作用域赋给新对象(this指向该新对象);
- 执行构造函数中的代码(给该新对象添加属性、方法);
- 若无显式返回对象或函数,才返回新对象。
// 模拟实现new1:
function createInstance(Constructor, ...args) {
const instance = Object.create(Constructor.prototype);
Constructor.call(instance, ...args);
return instance;
}
// 模拟实现new2:
function createInstance() {
const obj = new Object();
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const result = Constructor.apply(obj, arguments);
return typeof result === 'object' ? result : obj;
}
原型与原型链
prototype的定义:
给其他对象提供共享属性的对象。每个函数对象(只有函数对象才有该属性)都有一个prototype
属性,此属性指向一个对象(prototype
自己也是对象,其实prototype
描述的是两个对象之间的某种关系),该对象就是调用构造函数而创建的实例的原型。
__proto__
的定义:
每个js对象(除null外),都具有__proto__
属性,该属性指向该对象的原型(构造函数的原型上不存在此属性)。此属性是来自Object.prototype
,与其说其是一个属性,倒不如说是一个getter/setter
。
constructor的定义:
每个原型都有一个constructor
属性指向与之关联的构造函数。它是本身或者继承而来的。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。
普通对象和函数对象:
凡通过new Function()
创建的对象都是函数对象,其他的都是普通对象(Function、Object
也是通过new Function()
创建的)。
function Person() {}
const person = new Person();
person.__proto__ === Person.prototype; // true
// Person为构造函数
Person.__proto__ === Function.prototype; // true
// Person.prototype为一个对象(prototype自身也是一个对象)
Person.prototype.__proto__ === Object.prototype; // true
// Object为构造函数(也由new Function()创建而来的)
Object.__proto__ === Function.prototype; // true
// 为原型链的顶端
Object.prototype.__proto__ === null; // true
// Function.prototype为一个对象(prototype自身也是一个对象)
Function.prototype.__proto__ === Object.prototype; // true
原型:每个js对象(除null外),在创建的时候都会与之关联另一个对象,此对象就是我们所说的原型,每个对象都会从原型“继承”属性。
原型比较少人知道的特性:
- ES3时代只有访问属性的get操作才能触发对原型链的查找;ES5新增访问器属性的概念,定义属性getter/setter操作;
- 普通对象的
__proto__
属性,其实是在原型链查找出来的,定义在Object.prototype
对象上的。
继承
原型继承
- 将公用方法和属性添加到父级的原型上面;
- 将子级的原型替换成父级的实例对象。
// 优点:子类可以访问到父类原型上共享的属性和方法;
// 缺点:1.子类修改共享的引用属性会导致父类的也给修改了;
// 2.子类实例时无法向父类传递参数
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name) {
this.name = name;
}
Person.prototype.getName = () {
return this.name;
}
// 子类
function Student(name, age, stuClass) {
this.name = name;
this.age = age;
this.stuClass = stuClass;
}
// 将子类的原型换成父类的实例对象--就能访问到共享的父类实例的原型了
Student.prototype = new Person();
构造函数继承
function SuperPerson(name, age) {
this.name = name;
this.age = age;
}
SuperPerson.prototype.getName = function() {
return this.name;
}
function subStudent(name, age, stuClass) {
this.stuClass = stuClass;
SuperPerson.call(this, name, age);
}
组合继承
原型链继承+构造函数继承
原型式继承
Object.create()的实现方式
function SuperPerson(SubStudent) {
const Noop = function() {};
Noop.prototype = SubStudent;
return new Noop();
}
寄生式继承
function SuperPerson(SubStudent) {
const instance = Object.create(SubStudent.prototype);
// 原型上添加属性和方法
// ...
return instance;
}
寄生式组合继承
function inheritPrototype(SuperParent, SubChildren) {
const instance = Object.create(SuperParent.prototype);
instance.constructor = SubChildren;
SubChildren.prototype = instance;
}
instanceof
- 判断一个实例是否属于某种类型;
- 判断一个实例是否是其父类或祖先类的实例。
function createInstanceOf(children, parent) {
while(true) {
if (children.__proto__ === null) { // 原型链顶层
return false
} else if (children.__proto__ === parent.prototype) {
return true
}
}
}
function Foo() { }
const foo = new Foo()
console.log(createInstanceOf(foo, Foo));
typeof
底层判断是用机器码进行判断的。
- 对象:000;
- 浮点型:010;
- 字符串:100;
- 布尔类型:110;
- 整数:1
因为null
的机器码为000
,typeof
判断时当作对象。
闭包
- 能够访问自由变量的函数
- 自由变量是只在函数中使用,但既不是函数参数也不是函数的局部变量的变量;
- 闭包 = 函数 + 函数能够访问自由变量。
bind
bind
方法会创建一个新函数,当此函数被调用时,bind
的第一个参数将作为它运行的this
,之后一系列参数将会在传递实参前传入作为它的参数。
Function.prototype.bind2 = function(context) {
// 若传递进来的不是function,抛出错误
if (typeof this !== 'function') throw new TypeError(this + "must be function")
const self = this;
// 剔除context的其他参数,转为数组
const args = [].slice.call(arguments, 1);
const bound = function () {
const boundArgs = [].slice.call(arguments);
const finalArgs = args.concat(boundArgs);
// new 调用(其实this instanceof bound判断并不是很准确,ES6的new.target可以解决此问题)
if (this instanceof bound) {
// 实现new
// self可能是ES6的箭头函数,没有prototype,所以没有必要再指向做prototype操作
if (self.prototype) {
function Noop() {}
Noop.prototype = self.prototype;
bound.prototype = new Noop();
}
// 生成的新对象会绑定到函数调用的this
const result = self.apply(this, finalArgs);
// new若没有显式返回,会返回新对象
const isObject = result !== null && typeof result === 'object';
const isFunc = typeof result === 'function';
if (isObject || isFunc) return result;
return this;
} else {
return self.apply(context, finalArgs);
}
}
return bound;
}
call和apply
call
call方法使用一个指定的this
值和若干个指定参数的前提下调用某个函数或方法。
Function.prototype.call2 = function(context) {
var context = context || window;
context.func = this;
var args = [];
var array = [].slice.call(arguments, 1)
for (let i = 1; i < array.length; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.func(' + args + ')');
delete context.func;
return result;
}
apply
apply方法使用一个指定的this
和一个指定数组参数的前提下调用某个函数或方法。
Function.prototype.apply2 = function (context, args) {
var context = context || window;
context.func = this;
var result;
if (args) {
var array = [];
for (let i = 0; i < args.length; i++) {
array.push("args[" + i + ']')
}
result = eval("context.func(" + array + ")")
} else {
result = context.func();
}
delete context.func;
return result;
}
深拷贝
const reference = ['Set', 'WeakSet', 'Map', 'WeakMap', 'Date', 'RegExp', 'Error'];
const getType = (target) => Object.prototype.toString.call(target).replace(/\[|\]|object /g, '');
// 方案1--通俗写法
function deepClone(target) {
let result = null; // 存储拷贝后的对象
const type = getType(target); // 获取目标参数的类型
if (type === 'Object') { // 为对象--遍历+递归
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
result[key] = deepClone(target[key]);
}
}
} else if (type === 'Array') { // 为数组--遍历+递归
target.forEach((el, i) => {
result[i] = deepClone(el);
})
} else if (reference.includes(type)) { // 为定义好的类型
result = new target.constructor(target);
} else { // 基本数据类型与function直接赋值
result = target;
}
return result;
}
// 方案2--使用ES7中的属性
function deepClone(target) {
let result;
// 利用Object.create(proto[, propertiesObject])创建一个新对象
// 参数2即使用对象的描述属性作为新对象的属性
const _target = Object.create(Object.getPrototypeOf(target), Object.getOwnPropertyDescriptors(target));
// 遍历对象,克隆属性
// Reflect.ownKeys(object)-->创建一个新数组,此数组的元素即为参数对象属性的key
for (const key of Reflect.ownKeys(_target)) {
const value = _target(key);
const type = getType(_target);
if (value !== null && typeOf value === 'object') { // 对象或者数组-->递归
result[key] = deepClone(value);
} else if (reference.includes(type)) {
result[key] = new value.constructor(value);
} else {
result[key] = value;
}
}
return result;
}
首层拷贝
// 数组中的concat、slice;Object.assign();展开运算符都是首层拷贝
function shallowClone (target) {
const _target = target.constructor === 'Array' ? [] : {};
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
_target[key] = target[key];
}
}
return _target;
}
iterator迭代器
ES6中的解构赋值、剩余/扩展运算,生成器、for of循环,底层都是iterator迭代器去实现的。
所谓迭代器就是一个具有next()
方法的对象,每次调用next()
方法都会返回一个结果对象,该结果对象有两属性,value表示当前的值,done表示遍历是否结束。
function createInterator(items) {
var i = 0;
return {
next: function () {
var done = i >= items.length;
var value = !done ? items[i++] : undefined;
return { value, done }
}
}
}
Promise对象
是async/await
语法基础,是js中处理异步的标准形式。
promise3个状态,分别是pending、fulfilled、rejected:
- pending状态,promise可以切换到fulfilled或rejected;
- fulfilled状态,不能迁移到其他状态,必须有个不可变的value;
- rejected状态,不能迁移到其他状态,必须有个不可变的reason。
/*
步骤1:new Promise((resolve, reject) => {})
一个函数参数,包含两个参数
两个参数分别为函数
*/
/*
步骤2:promise有3个状态:pending、fulfilled、rejected
pending(初始化状态):可以将状态迁移成fulfilled或者rejected;
fufilled(成功状态):状态不可迁移,此时有一个不可变的值value;
rejected(失败状态):状态不可迁移,此时有一个不可变的reason
*/
/*
步骤3:then方法,接收两个参数:onFulfilled函数,onRejected函数
*/
/*
步骤4:解决异步问题:当resolve在异步中执行时,then中的state还处于pending
解决方案:将成功和失败存到数组中,一旦resolve或reject时,调用数组中的每一个成功或失败
*/
/*
步骤5:解决链式调用
1.在第一个then中返回一个新的Promise对象(普通值,将普通值传递给下一个then;promise将promise返回的值传递给下一个then);
*/
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
// 链式调用解决函数
// 1.循环引用问题
// 2.结果若是对象或函数时,递归解析promise,否则直接resolve
function resolvePromise(promise, result, resolve, reject) {
// 循环引用问题
if (promise === result) return new TypeError('Chaining cycle detected for promise')
// 为对象或函数
if (result !== null && (typeof result === 'objected' || typeof result === 'function')) {
// 防止多次调用
let called = false
try {
// result下的then
const then = result.then
// then若为对象,默认是promise
if (typeof then === 'objected') {
// 调用then的方法的回调继续解析
then.call(result, value => {
// 防止多次调用
if (called) return
called = true
resolvePromise(promise, value, resolve, reject)
}, reason => {
// 防止多次调用
if (called) return
called = true
reject(reason)
})
} else {
resolve(result)
}
} catch (error) {
// 防止多次调用
if (called) return
called = true
reject(error)
}
} else { // 直接将普通值resolve出去
resolve(result)
}
}
class Promise2 {
constructor (executor) {
// 初始化状态为pending
this.state = PENDING
// 不可变的值value
this.value = undefined
// 不可变的原因reason
this.reason = undefined
// 收集成功、失败函数
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
// resolve函数
const resolvedFunc = (value) => {
this.value = value
// 执行成功函数--改变状态
this.state = FULFILLED
// 执行成功数组中的函数
this.onFulfilledCallbacks.length && this.onFulfilledCallbacks.forEach(func => func())
}
// reject函数
const rejectedFunc = (reason) => {
this.reason = reason
// 执行失败函数--改变状态
this.state = REJECTED
// 执行失败数组中的函数
this.onRejectedCallbacks.length && this.onRejectedCallbacks.forEach(func => func())
}
// 执行executor函数
// 使用trycatch防止出错
try {
executor(resolvedFunc, rejectedFunc)
} catch (error) {
rejectedFunc(error)
}
}
// then()
then(onFulfilled, onRejected) {
// 兼容onFulfilled、onRejected非函数情况
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
const promise = new Promise2((resolve, reject) => {
// 解决resolve、reject在异步中执行问题
if (this.state === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
})
}
if (this.state === FUFILLED) {
setTimeout(() => {
try {
const result = onFulfilled(this.value)
resolvePromise(promise, result, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
const result = onRejected(this.reason)
resolvePromise(promise, result, resolve, reject)
} catch (error) {
reject(error)
}
}, 0)
}
})
return promise
}
// catch()
catch(func) {
return this.then(null, func)
}
// 静态方法resolve
static resolve(value) {
return new Promise(resolve => resolve(value))
}
// 静态方法reject
static reject(reason) {
return new Promise((_, reject)=> reject(reason))
}
// 静态方法race
static race(promises) {
if (typeof promises !== 'Array') return new TypeError('race arguments must be Array')
return new Promise2((resolve, reject)=> {
for (let i = 0, length = promises.length; i < length; i++) {
promises[i].then(resolve, reject)
}
})
}
// 静态方法all
static all(promises) {
if (typeof promises !== 'Array') return new TypeError('all arguments must be Array')
const resultArray = []
let i = 0
const processData = (data, index, resolve) => {
resultArray[index] = data
i++
if (i === promises.length) resolve(resultArray)
}
return new Promise2((resolve, reject) => {
for (let i = 0, length = promises.length; i < length; i++) {
promises[i].then(value => {
processData(value, i, resolve)
}, reject)
}
})
}
}
隐式转换介绍
在js中,当运算符在运算时,若两边数据不统一,CPU就无法计算。此时编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算。由编译器自动转换的方式称为隐式转换。
隐式转换规则
- 转成String类型:+(字符串连接符);
- 转成Number类型:++、–(自增自减运算符)、+、-、*、/、%(算术运算符)、>、<、>=、<=、==、!=(关系运算符);
- 转成Boolean类型:!(逻辑非运算符)。
以下8中情况转换为布尔类型会得到false:
0、-0、NaN、undefined、null、空字符换、false、document.all()。除以上8中情况之外都会得到true。
// + 是字符换连接符:String(1) + 'true' = '1true'
console.log(1 + 'true');
// + 是算术运算符:1 + Number(true) = 1 + 1 = 2
console.log(1 + true);
// + 是算术运算符:1 + Number(undefined) = 1 + NaN = NaN
console.log(1 + undefined);
// + 是算术运算符:1 + Number(null) = 1 + 0 = 1
console.log(1 + null);
// 关系运算符两边有一边是字符串的时候,会将其他数据类型使用Number()转换,然后比较。
// Number('2') > 10 = 2 > 10 = false
console.log('2' > 10);
// 关系运算符两边都是字符串时,此时同时转成number然后比较关系。
// 注意:此时并不是按照Number()的形式转成数字,而是按照字符串对应的unicode编码来转成数字
// '2'.charCodeAr() > '10'.charCodeAt() = 50 > 49 = true
console.log('2' > '10');
// 多个字符从左到右依次比较
// 先比较'a'、'b','a'与'b'不等,则直接出结果(a的unicode码比b的unicode码小)
console.log('abc' > 'b'); // false
// 先比较'a'和'a',两者相等,继续比较第二个字符'a'和'b',得出结果
console.log('abc', 'aad'); // true
// 特殊情况(无视规则):若数据类型是undefined、null,得出固定结果
console.log(undefined == undefined); // true
console.log(undefined == null); // true
console.log(null == null); // true
// 特殊情况(无视规则):NaN与任何数据比较(包括其本身)都是NaN
console.loe(NaN == NaN); // false
复杂数据类型在隐式转换时会先转成String,然后再转成Number运算
var a = ??
if (a == 1 && a == 2 && a == 3) {
console.log(1);
}
// 如何完善a,使其正确打印1
// 因为对象的valueOf()可以被重写,重写a对象中的valueOf()使条件成立
var a = {
i: 0, // 声明一个变量属性
valueOf: function() {
// 每调用一次,让对象a的i属性自增一次并返回
return ++a.i;
}
}
if (a == 1 && a == 2 && a == 3) { // 每一次运算时都会调用一次valueOf()
console.log(1);
}
// 复杂数据类型转number顺序:
// 1.先使用valueOf()方法获取其原始值,若原始值不是number类型,则使用toString()方法转成String;
// 2.再将string转成number运算。
// 先将左边数组转成string,然后右边也是string,则转成Unicode编码运算。
// [1,2].valueOf() = [1,2],非number类型,使用toString()
// [1,2].valueOf().toString() = '1,2'
console.log([1, 2] == '1,2'); // true
// a.valueOf().toString() = '[object Object]'
var a = {};
console.log(a == '[object Object]'); // true
// [].valueOf().toString() = ''
// Number('') = 0;
console.log([] == 0); // true
// 逻辑非优先级高于关系运算符
// ![] = false([]转为布尔类型为true(布尔类型除8种情况以外都为true))
// false == 0;
cosole.log(![] == 0); // true
// 逻辑非优先级高于关系运算符
// ![] = false([]转为布尔类型为true(布尔类型除8种情况以外都为true))
// [] == false
// 根据隐式转换规则(==:转为number类型比较)
// [].valueOf().toString() = ''
// Number('') == Number(false)
console.log([] == ![]); // true
//引用类型存储在堆中,栈中存储的是地址,所以结果是false
console.log([] == []); // false
// 逻辑非优先级高于关系运算符
// !{} = false({}转为布尔类型为true(布尔类型除8种情况以外都为true))
// 根据隐式转换规则(==:转为number类型比较)
// {}.valueOf().toString() = '[object Object]'
// Number('[object Object]') == Number(false)
console.log({} == !{}); // false
//引用类型存储在堆中,栈中存储的是地址,所以结果是false
console.log({} == {}); // false
proxy的理解
Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。
var target = {
name: 'zhangsan',
age:20,
sex:'男'
}
var logHandler = {
get(target, key) {
console.log(`${key}被读取`)
return target[key]
},
set(target, key, value) {
console.log(`${key}被设置为${value}`)
target[key] = value
}
}
var demo = new Proxy(target, logHandler)
demo.name //name被读取
JavaScript中的数组和函数在内存中如何存储
- 同种类型数据的数组分配连续的内存空间;
- 存在非同种类型数据的数组使用哈希映射分配内存空间。
温馨提示:连续的内存空间只需要根据索引(指针)直接计算存储位置即可。哈希映射需要计算索引值,然后索引值有冲突的场景下还需要二次查找(需要知道哈希的存储方式)
Object.defineProperty有哪几个参数,各自都有什么作用
可以给一个对象添加属性以及这个属性的属性描述符/访问器(这2个不能共存,同一属性只能有其中一个),属性描述符有configurable,writable,enumerable,value这4个属性,分别代表是否可配置,是否只读,是否可枚举和属性的值,访问器有configurable,enumerable,get,set,前2个和属性描述符功能相同,后2个都是函数,定义了get,set后对元素的读写操作都会执行后面的getter/setter函数,并且覆盖默认的读写行为。
Object.defindProperty
有三个参数:
- 对象;
- 对象的属性
- 属性描述符
Object.defineProperty和ES6的Proxy的区别
Proxy的优势:
Proxy
可以直接监听对象而非属性;Proxy
可以直接监听数组的变化:
1.Proxy
有多大13种拦截方法,不限于apply、ownKeys、deleteProperty、has
等,是Object.definedProperty
不具备的;
2.Proxy返回的是一个新对象,可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改;
3.Proxy
作为新标准将受到浏览器厂商重新持续的性能优化(即传说中的新标准的性能红利);
Object.defineProperty的优势:
- 兼容性能好,支持IE9,而Proxy存在浏览器兼容性问题,且无法使用polyfill磨平。
宏任务和微任务
在异步模式下,创建异步任务主要分为宏任务和微任务两种。ES6规范中,宏任务(Macrotask)称为Task;微任务(Microtask)称为Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由js自身发起的。
宏任务(Macrotask) | 微任务(Microtask) |
---|---|
setTimeout | requestAnimtionFrame(有争议) |
setInterval | MutationObserver(浏览器) |
MessageChannel | Promise.[ then/catch/finally ] |
I/O,事件队列 | process.nextTick(Node环境) |
setImmediate(Node环境) | queueMicrotask |
script(整体代码块) |
如何理解script(整体代码块)是宏任务:
实际上若同时存在两个script代码块,会首先执行第一个script代码块中的同步代码。若此过程中创建了微任务并进入了微任务队列,第一个script同步代码执行完之后,会首先去清空微任务队列,再去开启第二个script代码块的执行。
EventLoop
js引擎遇到异步事件
之后,会将此事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果之后,将此事件加入到当前执行栈的另一队列中(即事件队列)。被放入事件队列中并不会立即执行回调,而是等待当前执行栈中的所有同步任务执行完毕,主线程处于闲置状态下,主线程会去查找事件队列中是否有任务。若有的话,主线程会从当中取出在第一位的事件,并将此事件对应的回调加入到执行栈中,然后执行其他同步代码。如此反复,形成了无限循环(即事件循环(EventLoop))。