基本类型存储在栈,被引用或拷贝,会创建一个完全相等的变量
引用类型存储在堆,存储地址,多个引用指向同一地址,并涉及到共享
”共享“:
let a = {
name: 'aa',
age: 20
}
function cc (o) {
o.age = 30;
o = {
name: 'bb',
age: 40
}
return o;
}
let b = cc(a);
// b.age = 40; a.age = 30
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol // 'symbol'
typeof null // 'object'(*)
typeof []/{}/console // 'object'
typeof console.log // 'function'
判断null
if(? === null)
new 一个对象就是原型链继承上面的对象,通过instanceof 能判断这个对象是否是之前构造函数生成的对象
let str = new String('aaa');
str instanceof String // true
let str = 'aaa'
str instanceof String // false
自己实现底层instanceof
function myInstanceof (left, right) {
// 如果是基本数据类型,直接返回false
if(typeof left != 'object' || left === null) return false;
// Object.getPrototypeOf() 能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while((proto = Object.getPrototypeOf(proto)) !== null) {
if(proto === right.prototype) return true;
}
}
myInstanceOf(new Number(234), Number);
instanceof 可以准确判断引用数据类型,不能正确判断基本数据类型
typeof 可以判断剧本数据类型,但只能判断一个 function 引用类型
对于Object 对象,可以直接调用 toString(), 其他对象需要 通过 call
Object.prototype.toString({}) // [object Object]
Object.prototype.toString({}) // [object Object]
Object.prototype.toString(1) // [object Number]
Object.prototype.toString('1') // [object String]
Object.prototype.toString(true) // [object Boolean]
Object.prototype.toString(function() {}) // [object Function]
Object.prototype.toString(null/ undifined) // [.. Null/ Undefined]
Object.prototype.toString(/123/g) // RegExp
Object.prototype.toString(new Date()) // Date
Object.prototype.toString([]) // Array
Object.prototype.toString(document) // HTMLDocument
Object.prototype.toString([]) // Window
function getType (obj) {
let type = typeof obj
if(typeof obj !== 'object') {
return type;
}
// 通过正则获取结果
return Oject.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}
Number() / parseInt() / parseFloat() / toString() / String() / Boolean()
除了 undefined / null / false / ‘’ / 0 / NaN 为false ,其他为 true
NaN 是一个全局对象的属性,NaN 是一个全局对象的属性,NaN是一种特殊的Number类型
toString()可以将数据都转为字符串,但是null和undefined不可以转换
console.log(null.toString())
// 报错 TypeError: Cannot read property 'toString' of null
console.log(undefined.toString())
// 报错 TypeError: Cannot read property 'toString' of undefined
String
String()可以将null和undefined转换为字符串,但是没法转进制字符串
console.log(String(null));
// null
console.log(String(undefined));
// undefine
toString()括号中可以写数字,代表进制
通过逻辑运算符,关系运算符,相等运算符,或if/while条件操作,如果两个类型不相同,就会隐式转换
数字相加(两边都为数字), 字符串拼接(两边都为字符串)
特殊规则:
对象转换会先调用内置的[ToPrimitive] 函数
let obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3';
},
[Symbol.toPrimitive]() {
return 4;
}
}
console.log(obj + 1)
// 5 -> Symbol.toPrimitive
// 3 -> valueOf
// 31 -> toString
10 + {}
// 10[object Object]
{} + 10
// 10]
[1, 2, undefined, 4, 5] + 10
// "1,2,,4,510"
如果对象属性是基本类型,复制的就是基本类型的值,如果是引用类型,复制的就是内存地址,一个对象改变则另一个也改变
该方法可以用于对象合并,可以进行浅拷贝。第一个参数是拷贝的目标,后面的参数是拷贝的来源对象(可以多个来源)
Object.assign(target, ...sources);
注意:
Object.defineProperty(obj1, 'innumerable', {
value: '不可枚举属性',
enumerable: false
})
let obj2 = {};
Object.assign(obj2, obj1);
let cloneObj = { ...obj};
如果属性都为基本类型的值,使用扩展运算符会更加方便
只能用于数组的浅拷贝,比较局限
let newarr = arr.concat();
arr.slice(begin, end);
function shallowClone (target) {
if(typeof target === 'object' && target !== null) {
const cloneT = Array.isArray(target) ? [] : {};
for(let prop in target) {
// 只检查对象的自有属性
if(target.hasOwnProperty(prop)) {
cloneT[prop] = target[prop];
}
}
return cloneT;
}
return target;
}
深拷贝对于复杂的引用类型数据类型,其在堆内存中完全开辟一块内存地址
let obj1 = {a: 1, b: [1, 2, 3]};
let obj2 = JSON.parse(JSON.stringify(obj1));
function deepClone (obj) {
let cloneObj= {};
for(let key in obj) {
if(typeof obj[key] === 'object' && typeof obj[key] !== null) {
cloneOb[key] = deepClone(obj[key]);
}else {
cloneObj[key] = obj[key];
}
}
return cloneObj;
}
问题:
const deepClone = function(obj, hash = new WeakMap()) {
// 日期对象
if(obj.constructor == Date) return new Date(obj);
// 正则对象
if(obj.construtor == RegExp) return new RegExp(obj);
// 循环引用用weakmap 解决
if(hash.has(obj)) return hash.get(obj);
// 获取所有属性
let allDesc = Object.getOwnPropertyDescriptors(obj);
// 传入所有键的特性
// Object.create(proto[,propertiesObject])
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
// 继承原型链
hash.set(obj, cloneObj);
// 针对不可枚举属性
for(let key in Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key];
}
return cloneObj;
}
每个构造函数都有一个原型对象, 原型对象包含一个指向构造函数的指针,而实例则包含一个原型对象指针
function Parent() {
this.name = 'Parent';
}
function Child() {
this.age = 10;
}
Child.prototype = new Parent();
缺点:
实例使用同一个原型对象,内存空间是共享的
function Parent() {
this.name = 'Parent';
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {
Parent.call(this);
}
let Child = new Child();
Child.name = 'Parent' // 不报错
Child.getName(); // 报错
缺点:
父类的引用属性不会共享,只能继承实例属性和方法, 不能继承原型属性或方法
结合前两种
function Parent() {
this.name = 'Parent';
this.arr = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {
// 第二次调用
Parent.call(this);
}
// 第一次调用
Child.prototype = new Parent();
// 手动构造器,指向自己的构造函数
Child.constructor.prototype = Child;
缺点:
Parent 被构造了两次,造成了性能开销
let Parent = {
name: 'name',
getName() {
return this.name
}
}
let Child = Object.create(Parent);
缺点:
多个实例的引用类型指向相同的内存
原型式继承获得目标对象的浅拷贝, 然后利用浅拷贝能力进行增强,添加方法
let Parent = {
name: 'name',
friend: 10,
getName() {
return this.name
}
}
function clone(original) {
let clone = Object.create(original);
clone.getFriend = function() {
return this.friend;
}
return clone;
}
let Child = clone(Parent);
想对最优的解决方法
function clone(Parent, Child) {
// 使用 Object.create() 方法减少组合继承种一次构造
Child.prototype = Object.create(Parent.prototype);
chile.prototype.constuctor = Child;
}
function Parent {
this.name = 'Parent';
}
Parent.prototype = getName() {
return this.name;
}
function Child() {
Parent.call(this);
}
clone(Parent, Child);
Child.prototype.getFriend = function() {
return this.friend;
}
let Child1 = new Child();
当构造函数 return 一个新对象与 this 无关时, new 会直接返回新对象,而不通过执行步骤
function Person() {
this.name = 'people';
return {
age: 18
}
}
let p1 = new Person();
console.log(p1) // { age: 18 }
三个方法是挂在 Function 对象上的三个方法,调用三个方法必须是一个函数
tihsArg 表示所指向的对象
func.call(thisArg, param1, param2, ..);
func.apply(thisArg, [param1, param2]);
func.bind(thisArg, param1, param2);
let a = {
name: 'aaa',
getName(msg) {
return this.name + this.msg;
}
}
let b = {
name: 'bbb'
}
console.log(a.getName.call(b, '->message')) // bbb->message
console.log(a.getName.apply(b, ['->message'])) // bbb->message
let name = a.getName(b, '->message')
name(); // bbb->message
Object.prototype.call(obj).replace(//);
let arrLike = {
0: 'aa',
1: 'bb',
length: 2
}
Array.prototype.push.call(arrLike, 'cc', 'dd');
// {..2: 'cc', 3: 'dd', length: 4}
减少展开数组的一步
let arr = [12, 324, 12 , 4, 456];
const max = Math.max.apply(Math, arr);
-
eval() 函数计算 JavaScript 字符串,并把它作为脚本代码来执行。
如果参数是一个表达式,eval() 函数将执行表达式。如果参数是JavaScript语句,eval()将执行 JavaScript 语句(代码)。
Function.prototype.call = function(context, ...args) {
// 判断context是否有传值,没有则默认为window
let context = context || window;
// 将被调用的方法设置为context的属性(修改this指向为context)
context.fn = this;
let result = eval('context.fn(..args)');
// 删除手动增加的属性方法
delete context.fn;
return result;
}
Function.prototype.apply = function(context, args) {
let context = context || window;
context.fn = this;
let result = eval('context.fn(..args)');
delete context.fn;
return result;
}
不需要直接执行,所以不需要 eval,而是需要通过一个函数的方式将结果返回
Function.prototype.bind = function(context, ...args) {
if(typeof this !== 'function') throw Error("must function");
let self = this;
let fbound = function() {
self.apply(this.intanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
}
if(this.prototype) {
fbound.prototype = Object.create(this.prototype);
}
return fbound;
}
实现bind 的核心是返回的时候需要返回函数,且返回过程的原型链的属性不能丢失,所以需要 Object.create() 方法
是指有权访问另外一个函数作用域中的变量的函数,内层函数访问外层函数的作用域
fon(var i = 0; i < 5; i ++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 666666
解释:
解决:
for(var i = 0; i < 5; i ++) {
(function(j) {
setTimeout(() => {
console.log(j)
});
})(i);
}
for(let i = 0; i < 5; i ++) {
setTimeout(() => {
console.log(i);
}, 0);
}
setTimeout(() => {
console.log(i);
}, 0, i);
通过 typeof 根据数据类型来处理不同情况
function jsonStringify(data) {
let type = typeof data;
if(type !== 'object') {
let result = data;
if(Number.isNaN(data) || data == Infinity) {
result = "null";
}else if(type === 'function' || type === 'undefined' || type === 'symbol') {
result = undefined;
}else if(type === 'string') {
result = `${ data }`;
}
return String(result);
}else if(type === 'object') {
if(data === null) {
return "null";
// Date 对象
}else if(data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
// 处理数组
}else if(data instanceof Array) {
let result = [];
// 数组需要判断每一项
data.forEach((o, i) => {
if(typeof o === 'undefined' || typeof o === 'function' || typeof o === 'symbol') {
result[i] = "null";
}else {
result[i] = jsonStringify(o);
}
});
return `${result}`.replace(/'/, "");
// 处理对象
}else {
let result = [];
Object.keys(data).forEach((o, i) => {
// 忽略 symbol
if(typeof o !== 'symbol') {
if(data[o] !== undefined typeof data[o] !== 'function' || typeof data[o] !== 'symbol') {
result.push(`${o}:${jsonStringify(data[o])}`)
}
}
})
}
}
}
从一个类数组的可迭代对象中创建一个新的数组实例,返回新数组,步改变原对象
三个参数:
let obj = {0: 'a', 1: 'b', 2: 'c', length: 3};
Array.from(obj, (value) => value.repeat(3))
// String
Array.from('abc') // ['a', 'b', 'c']
// Set
Array.from(new Set(['a', 'b'])) // ['a', 'b']
// Map
Array.from(new Map([1, 'a'], [2, 'b'])) // [[1, 'a'], [2, 'b']]
let arr = [];
// 1. es6 isArray()
Array.isArray(arr)
// 2. instanceof
arr instanceof Array
// 3. constructor === Array
arr.constructor === Array
// 4. Object.prototype.isPrototypeOf
// 对象的原型(__proto__)指向Object的原型(prototype)
Array.prototype.isPrototypeOf(arr)
// 5. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype()
// 6. prototype.toString
Object.prototype.call(arr) === '[object Array]'
callback:
initialValue: 可选初始值
for(let i = 0; i < arguments.length; i ++) {
console.log(arguments[i])
}
let args = Array.prototype.slice.call(argument, 1);
return args.join(',')
function foo() {
bar.apply(this, arguments);
}
function bar(a, b, c) {
console.log(a, b, c);
}
// 通过 apply 将 foo 的参数给 bar
foo(1, 2, 3);
let args = Array.from(arguments);
let args = [...arguments];
sum(..args)
Array.prototype.slice.call(arguments);
// ==
[].slice(arguments);
function flatten(arr) {
let result = [];
for(let i < arr.length; i ++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
}else {
result.push(arr[i]);
}
}
return result;
}
function flatten(arr) {
return arr.reduce((pre, cur) => Array.isArray(cur) ? pre.concat(flatten(cur)) : pre.push(cur), []);
}
function flatten(arr) {
// 一层层展开
while(arr.some(o => Array.isArray(o))) {
arr = [].concat(...arr);
}
return arr;
}
function flatten(arr) {
return arr.toString().split(',');
}
return flat(Infinity);
function flatten(arr) {
return JSON.parse(`[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`);
}
Array.prototype.push = function(...items) {
// 先转为对象
let O = Object(this);
// 设置默认为0 -> lenght || 0
let len = this.length >>> 0;
// 参数数量
let argCount = items.length >>> 0
// 2 ^ 53 - 1 js 能表示的最大数
if(len + argCount > 2 ** 53 - 1) {
throw new TypeError("over the max value")
}
for(let i = 0; i < argCount; i ++) {
O[len + i] = items[i];
}
let newLength = len + argCount;
O.lenght = newLength;
return newLength;
}
执行代码时,在改代码没有得到返回结果之前,其他代码无法执行, 一旦执行完成拿到结果就会执行其他代码。也就是会阻塞之后的代码执行。
一段代码异步过程调用发出后,这段代码不会立刻得到返回结果,而是在发出后通过回调函数处理调用之后拿到结果。不会阻塞后面代码执行。
早年的异步编程采用回调函数的方式,如事件回调。会存在回调地狱的问题。
没有根本解决回调地狱问题,换了一种写法,提升了可读性
function read(url) {
return new Promise((resolve, reject) => {
fs.read(url, 'utf-8', (err, data) => {
if(err) reject(err);
resolve(data);
})
})
}
Promise.all([read(A), read(B)]).then(data => {
console.log(data);
}).catch(err => console.log(data));
三大技术手段解决回调地狱:
回调函数延迟绑定,返回值穿透, 错误冒泡
回调函数不是直接声明的,而是通过后面的 then 方法传入,延迟传入,也就是回调函数延迟绑定
根据 then 中回调函数的传入值创建不同类型 promise 然后把返回的Promise 穿透到外层,以供后续调用
let x = readFilePromise('x.json').then(data => readFilePromise('x.json'));
x.then();
前面产生的错误会一直向后传递, 被 catch 接收,错误冒泡
readFilePromise('x.json', then(data => readFilePromise(''))).then().then().catch(err => //xxx)
参数为可迭代对象,如数组
参数为数组,返回新的Promise
全部处理完后拿到每个 Promise 状态, 不管是否成功
const resolved = Promise.resolve(2);
const rejected= Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolve, rejected]);
allSettledPromise.then(results => console.log(results));
// result:
[
{status: 'fulfilled', value: 2},
{status: 'rejected', reason: -1}
]
只要有一个为 fulfilled 最后返回的实例就是 fulfilled 状态,否则为 rejected
race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。
// 请求资源
function requestImg() {
let p = new Promise(function(resolve, reject) {
let img = new Image();
img.onload = function() {resolve(img);}
img.src = 'http://xxx';
})
}
// 延时函数
function timeout() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {reject('图片请求超时');}, 5000);
})
}
Promise.race([requestImg(), timeout()]).then(result => {console.log)(result)}).catch(reason => {console.log(reason)});
可以交出函数的执行权,可以看为异步任务的容器,需要暂停时,用yield语法标注
function* gen() {
let a = yield 1;
let b = yield (function(){return 2})();
return 3;
}
// 程序阻塞不会执行任何语句
let g = gen();
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: true}
g.next(); // {value: undefined, done: true}
const gen = function* () {
const data1 = yield readFilePromise('');
console.log(data2.toString());
const data2 = yield readFilePromise('');
console.log(data2.toString());
}
let g = gen();
function run(gen) {
const next = (err, data) => {
let res = gen.next(data);
if(res.done) return;
res.value.then(next);
}
next();
}
run(g);
const con = require('co');
let g = gen();
co(g).then(res => {
console.log(res);
})
是 Generator 函数的语法糖,代码更为清晰,使异步同步化
function testWait() {
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log("test");
resolve();
}, 1000);
})
}
async function test() {
await testWait();
console.log("hello");
}
// test hello
在 javascript 中创建变量时, 系统会自动给对象分配对应的内存
简单数据类型 -》 (stack) 引用数据类型 -》 (heap)
V8 引擎将堆内存分为两类,新生代回收和老生代回收机制
在64位下分配 32MB ,新生代变量存活时间短, 不太容易产生太大的内存压力。
内存分为作用两个空间 from - to。当浏览器开始进行内存垃圾回收时,引擎会将左边的对象检查一遍, 如果是存活对象,就会复制到右边的空间去,如果不是存活的就直接进行系统回收。当左边内存没有对象时, 等有新生代对象产生时, 内存则左右对调,循环处理。
利用 Scavenge 算法处理内存碎片, 使其紧密排列
如果新生代中的变量经过回收之后依然存在,那么就会被放入老生代内存中。只要是经历过一次 Scavenge 算法回收的就可以晋升为老生代内存的对象。进入老生代内存后就不能再用 Scavenge 的算法了。因为不适合内存比较大的。
老生代中采用 Mark-Sweep (标记清除) 和 Mark-Compact (标记整理) 的策略
首先它会遍历堆上的所有的对象,分别对它们打上标记;然后在代码执行过程结束之后,对使用过的变量取消标记。那么没取消标记的就是没有使用过的变量,因此在清除阶段,就会把还有标记的进行整体清除,从而释放内存空间。
为了方便解决浏览器中的内存碎片问题,标记整理这个策略被提出。这个策略是在标记清除的基础上演进而来的,和标记清除来对比来看,标记整理添加了活动对象整理阶段,处理过程中会将所有的活动对象往一端靠拢
,整体移动完成后,直接清理掉边界外的内存。
每当一个函数执行完成,就会从堆栈中弹出 (pop) 该执行完成函数,有代码需要执行就进行 push
根据从调用堆栈收到的命令,API 开始自己的单线程操作。其中 setTimeout 方法就是一个比较典型的例子,在堆栈中处理 setTimeout 操作时,会将其发送到相应的 API,该 API 一直等到指定的时间将此操作送回进行处理。它将操作发送到事件队列(event queue)。这样,就有了一个循环系统,用于在 JavaScript 中运行异步操作。
事件循环(Eventloop)促进了这一过程,它会不断检查调用堆栈是否为空。如果为空,则从事件队列中添加新的函数进入调用栈(call stack);如果不为空,则处理当前函数的调用。
以 setTimeout 为代表的任务被称为宏任务,放到宏任务队列(macrotask queue)中;而以 Promise 为代表的任务被称为微任务,放到微任务队列(microtask queue)中。
V8 是众多浏览器的 JS 引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。
这个阶段会将源代码拆成最小的、不可再分的词法单元,称为 token。比如这行代码 var a =1;通常会被分解成 var 、a、=、2、; 这五个词法单元。另外刚才代码中的空格在 JavaScript 中是直接忽略的。
这个过程是将词法单元转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称为抽象语法树。