1.原型指向问题,new Hellow(2).proto.hasOwnProperty("add");是true
因为虽然未在constructor中去定义add方法,但实例对象的__proto__属性会指向构造函数的prototype,因为构造函数的原型上有add方法也就是(Hellow.prototype)上有add方法,所以new Hellow(2)的原型链__proto__上也会有add方法.所以当查找add方法时会查找到自身原型链上的方法.
class Hellow {
constructor(x, y) {
this.x = x;
}
add() {
console.log("888");
}
new Hellow(2).__proto__.hasOwnProperty("add");
2.Object.keys和Reflect.ownKeys(obj)的区别
//非对象参数会强制转换为对象。undefined 和 null 不能被强制转换为对象,会立即抛出 TypeError。
//只有字符串可以有自己的可枚举属性,而其他所有基本类型都返回一个空数组。
// 字符串具有索引作为可枚举的自有属性
console.log(Object.keys("foo")); // ['0', '1', '2']
// 其他基本类型(除了 undefined 和 null)没有自有属性
console.log(Object.keys(100)); // []
//Object.keys(obj) 会返回一个由对象可枚举字符串键属性名组成的数组(enumerable=true)
const keyObj = {
name: "xf",
sex: "1",
age: 100,
};
console.log(Object.keys(keyObj)); //['name','sex','age']
Object.defineProperty(keyObj, "name", {
enumerable: false,
});
console.log(Object.keys(keyObj)); //['sex','age']
2.Object.keys和Reflect.ownKeys(obj)的区别
//非对象参数会强制转换为对象。undefined 和 null 不能被强制转换为对象,会立即抛出 TypeError。
//只有字符串可以有自己的可枚举属性,而其他所有基本类型都返回一个空数组。
// 字符串具有索引作为可枚举的自有属性
console.log(Object.keys("foo")); // ['0', '1', '2']
// 其他基本类型(除了 undefined 和 null)没有自有属性
console.log(Object.keys(100)); // []
//Object.keys(obj) 会返回一个由对象可枚举字符串键属性名组成的数组(enumerable=true)
const keyObj = {
name: "xf",
sex: "1",
age: 100,
};
console.log(Object.keys(keyObj)); //['name','sex','age']
Object.defineProperty(keyObj, "name", {
enumerable: false,
});
console.log(Object.keys(keyObj)); //['sex','age']
复制代码
Object.keys(person)只能遍历出自己对象上的可枚举属性,不可以遍历出原型上的可枚举属性
function Person() {
this.address = "地球";
}
Person.prototype.school = "大学";
const person = new Person();
person.name = "xd";
console.log(Object.keys(person)); //['address', 'name']
for (let i in person) {
console.log(i); //'address', 'name','school'
}
//非对象参数会强制转换为对象。undefined 和 null 不能被强制转换为对象,会立即抛出 TypeError。
//只有字符串可以有自己的可枚举属性,而其他所有基本类型都返回一个空数组。
// 字符串具有索引作为可枚举的自有属性
console.log(Object.keys("foo")); // ['0', '1', '2']
// 其他基本类型(除了 undefined 和 null)没有自有属性
console.log(Object.keys(100)); // []
Object.keys和Reflect.ownKeys(obj)的区别在于Object.keys只可以遍历出自身对象上的可枚举属性,而Reflect.ownKeys(obj)可以遍历出自身对象上的所有属性,二者都不可以遍历自身对像原型上的属性
3.实现Object.getOwnPropertyDescriptors方法和Object.getOwnPropertyDescriptors的作用
Object.getOwnPropertyDescriptors会返回一个对象,对象里包括目标源对象的所有自身属性的描述符,包括可枚举属性和不可枚举属性,以及属性的特性(如 configurable、enumerable、writable 等)。
const person2 = {
name: "xf",
sex: 20,
address: "地球",
};
function getOwnPropertyDescriptors(obj) {
const result = {};
for (key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
4.Object.getOwnPropertyDescriptor的作用
是用于获取对象指定属性的描述符的方法
是用于获取对象指定属性的描述符的方法
5.Object.defineProperty的作用
Object.defineProperty() 是用于定义对象属性的方法。它允许以细粒度的方式定义对象的属性特性,如可写性、可枚举性和可配置性。
6.实现Object.is
实现出+0和−0不在相等2,两个NaN不再相等
实现出+0和−0不在相等2,两个NaN不再相等
Object.defineProperty(Object, "is", {
value: function (x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true,
});
7.js可枚举属性和不可枚举属性
8.Object.getOwnPropertyNames(),for...in,Object.keys()获取属性有什么区别
Object.getOwnPropertyNames()可以遍历出自身对象上的所有属性,但不可以遍历自身对象原型上的属性
for..in可以遍历自身对象原型上的可枚举属性,但只可以遍历自身对象上的可枚举属性
Object.keys只可以遍历出自身对象上的可枚举属性并且不可以遍历出自身对象上的所有属性
function Person() {
this.name = "Ykx";
};
Person.prototype.School = 'Tust';
Object.defineProperty(ykx, "sex", {
value: "male",
//是否为枚举属性
enumerable: false
});
let ykx = new Person();
Object.getOwnPropertyNames(ykx).forEach(function(key) {
console.log(key);//name,sex
});
9.如何克隆原始对象自身的值,克隆它继承的值,如果保持继承链
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
10.为什么上述浅拷贝可以保持继承链(Object.create)
console.log("33", newObj);
const oriObj={
name:'xff',
age:100
say:function(){
console.log('hellow')
}
}
const newUesObj=Object.create(oriObj)
//newUesObj的原型链会指向oriObj 所以newUesObj可以调用say方法
11.实现流式数据的获取
正在更进中
正在更进中
12.如何去除异步的传染性,哪些情况会造成
正在更进中
正在更进中
13.实现如何增加代码的复杂度
正在更进中
正在更进中
14.实现reduce方法
正在更进中
正在更进中
15.判断是否是稀疏数组(判断数组的长度是否和数组内容个数一致,也就是判断数组的索引是否存在数组中)
正在更进中
正在更进中
16.函数的length属性
length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
17.函数尾调用优化
18.基本数据类型和引用数据类型
8种数据类型,Number String null undefined Object Boolean Symbol BigInt
基本数据类型就是指不可变的数据没有地址引用(Undefined、Null、Boolean、Number、String)
引用数据就是有地址引用的,他们是通过地址的引用和保存来进行数据操作的,创建的变量往往不是指向这个变量值的本身,而是指向这个变量的地址值。引用数据类型会在栈中存储一个指针,这个指针会指向堆里面的实体地址。
19.数据检测的方式
1.typeof(数组 对象 null)都会判断为object,不可以用来判读复杂数据类型
a. 为什么typeof []===object
因为数组本质上就是对象的一种特殊体现,每一个对象都会有键值对组成,数组的很多方法其实就是对 象方法的语法糖
b. 为什么typeof null===object
因为null本质上就是指一个空的对象引用,object的机器码标识是000,而null的机器码标识全部都是0,所以判断为typeof null===object
2.instanceof(可以用来判断引用数据类型,不可以用来判断基本数据类型)
因为instanceof 是通过检查一个对象是否属于某一个特定的构造函数的原型链上([] instanceof Arrary===true)而基本数据类型没有引用的概念,没有原型链,就是一个固定的值,因此instanceof无法用来判断基本数据类型
3.constructor(通过判断一个数据的构造函数是什么来判断他是什么类型)
缺点:如果创建了一个对象并且修改了他的构造函数,那就不能用这个方法去进行检测了
function Fn() { };
//修改Fn 的原型(prototype)对象,将其替换为一个数组实例
Fn.prototype = new Array();
const f = new Fn();
console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true
4.Object.prototype.toString.call()
20.检测数组
1.Arrary.isArrary()
2.[] instanceof Arrary
3.[].constructor
4.Object.prototype.toString.call()
21.null和undefined的区别
null是空对象没有引用,undefined是未定义的
22.实现instanceof 操作符
/**
* 实现instanceof 操作符 传递一个被判断的和一个被比较的 left被判断的
*/
function myInstanceOf(left, right) {
//获取被检测数据的原型
let proto = Object.getPrototypeOf(left)
//获取构造函数的原型对象
let prototype = right.prototype
while (true) {
//如果被检测对象的原型是空就代表已经是原型链的终点了就直接false
if (!proto) return false
if (proto === prototype) return true
//如果前面两个判断都没有走就直接接着沿着原型链向上查找,查找proto的原型,也就是left的原型的原型
proto = Object.getPrototypeOf(proto)
}
}
23.基本数据类型有原型链和构造函数吗
基本数据类型没有原型链的概念,基本数据类型也没有直接的构造函数,但是有包装对象如 new Number() new Boolean() new String
24.基本数据类型既然没有构造函数为什么
1.constructor===Number
因为基本数据虽然没有构造函数,但是包装对象会有构造函数,但是js引擎在碰到基本数据类型是会进行以下两个步骤
1.将基本数据类型转化为包装对象(1=>new Number(1))
2.检测包装对象的实例的构造函数(new Number(1).constructor)
25.Object.getPrototypeOf(1)和Object.getPrototypeOf(Number)有什么区别
Object.getPrototypeOf(1)是获取‘包装对象Number(1)’的原型也就是Number.prototype
可以将1看做是一个对象({1})Number(1),则这个对象的原型就是Number.prototype
Object.getPrototypeOf(Number)是获取‘构造函数Number’的原型也就是Function.prototype
const num = 1
console.log((1).__proto__ === Number.prototype);
console.log(Object.getPrototypeOf(1) === Number.prototype);
//解释1
//因为(1)会让js引擎视为一个临时的包装对象,所以在这里的这个包装对像就是 new Number()也就是Number(1)
//又因为__proto__ 是用来访问一个对象的原型,所以Number(1)的原型就是Number.prototype 所以(1).__proto__ === Number.prototype
//解释2
//因为Object.getPrototypeOf()是用来获取对象的原型的,这里的Object.getPrototypeOf(1)中的1会被js引擎自动创建包装对象
//也就是new Number(1) 所以Object.getPrototypeOf(1)所以就是获取包装对象Number的原型, 又因为Number的原型就是Number.prototype
//所以Object.getPrototypeOf(1) === Number.prototype
//Object.getPrototypeOf(1)是获取‘包装对象Number(1)’的原型也就是Number.prototype
//可以将1看做是一个对象({1})Number(1),则这个对象的原型就是Number.prototype
//Object.getPrototypeOf(Number)是获取‘构造函数Number’的原型也就是Function.prototype
24.isNaN和Number.isNaN有什么区别
isNaN会先将数值转为Number(因此非数字的也会被判断为false),再去判断是不是NaN
Number.isNaN会先判断该数据是不是Number再去判断是不是NaN,与前者比较起来判断更为准确
25.对象转化为字符串的规则toString()方法
对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
/**
* 此处调用的toString方法是Object.prototype.toString()方法
*/
const obj = {
name: 'xf'
}
console.log(obj.toString());
/**
* 此处调用的toString方法是对象newObj重写过的toString()方法
*
*/
const newObj = {
name: "xf",
toString: () => {
console.log('哇哇哇哇哇哇');
return void 0
}
}
console.log(newObj.toString());
26.Number(null)===0
27.Object.is()和==和===的区别
1.==会进行强制类型转化后再去判断如‘1’==1
2.===会将双方的数据类型也一并比较不会进行强制数据类型转化 如‘1’!==1,必须是1===1
3.Object.is()和===的比较情况类似但是Object.is()进行了一些特殊情况的处理,如+0和-0不再相等,两个NaN是相等的
28.包装类型和基本类型相互转化
/**
* js中基本数据类型是没有原型链和构造函数这个概念的,所以基本数据就会丧失很对方法
* 但是js中提供了包装对象的概念,比如const num=1,其实在这个过程中1被包装对象所操作得到
* Number(1)这个时候这个‘1’就会继承包装对像Number的原型链,也就可以使用这个包装对象的很多方法了
*/
const num = 1
//Object(val)将基本数据类型转化为包装对象
const newNum = Object(1)
console.log(newNum);
const str = 'abc'
const newStr = Object(str)
console.log(newStr);
const newFalse = new Boolean(false)
if (!newFalse) {
console.log('qqqqq');
}
const newFalseTwo = Object(false)
console.log(newFalseTwo);
console.log(newFalse);
if (!newFalseTwo) {
console.log('aaaa');
}
/**
* 以上的两个判断都不会走,因为当基本数据的false被转化为包装对象时,他就是一个对象了
* 所以不会为空,所以不走两个判断语句
*/
// 包装对象调用valueOf()方法会恢复为原来的基本数据类型
//重写Number的valueOf()方法
Number.prototype.valueOf = () => {
console.log('111111');
}
console.log(Object(1).valueOf());
29.js如何进行隐式类型转化,toPrimitive的转化规则
obj 需要转换的对象type 期望的结果类型ToPrimitive(obj,type)
如何被转化的值是基本数据类型,那么转化后就会返回自身
如果被转化的值是个对象,那么toPrimitive方法会去调用对象的valueOf()方法或者toString方法
1.当type是Number时会调用valueOf()
2.如果当type是String时会调用toString()
如下
const obj = {
valueOf: () => {
return 1
},
toString: () => {
return 'hhh'
}
}
console.log(+obj); //1 等效于 ToPrimitive(obj, 'number')
console.log(String(obj));//'hhh' 等效于 等效于 ToPrimitive(obj, 'string')
30.浅拷贝和深拷贝
1.浅拷贝:拷贝的是对象的引用地址,拷贝出来的另一个对象和原对象会共同指向同一个地址,所以其中一个改变另一个也会改变
2.深拷贝:拷贝的对象的值,会创造一个新对象,开辟新的栈空间有属于自己的单独唯一的地址值,所以拷贝出来的对象和原对象不会相互影响
区别:浅拷贝会复制地址引用深拷贝不会
为什么简单的let obj1={} let obj2=obj1会进行浅拷贝:
因为对于复杂数据类型也就是一个对象里面可能会存在很多未知的引用,js在碰到这种操作的时候会偷懒直接将地址引用进行复制,共同指向同一个地址值,这样就不会存在复制的对象中会有某个未知的引用被遗漏掉
基本数据类型不存在浅拷贝和深拷贝的概念,因为基本数据类型是存放在栈中的,大小是固定的不需要其他的引用,所以在复制基本数据类型的时候只是将其中一个变量的值复制给另一个变量
实现深拷贝
对于数组:
1.slice方法 不完全深拷贝
对于简单的没有对象的数组,slice方法可以实现深拷贝,但对于复杂的对象数组slice只可以实现浅拷贝,因为
Array.prototype.slice 方法用于创建一个新数组,其中包含从原始数组中选择的元素。它接受两个参数,起始索引和结束索引(可选),并返回一个新数组,包含从起始索引到结束索引之间的元素。
2.使用concact() 不完全深拷贝
3.使用扩展运算符 不完全深拷贝
4.Array.from() 不完全深拷贝
对于简单的没有对象的数组,Array.from()方法可以实现深拷贝,但对于复杂的对象数组Array.from()只可以实现浅拷贝 因为Array.from() 方法复制的是原数组的值本身,而不是引用。Array.from() 方法用于将可迭代对象(包括类数组对象和可迭代的值)或类数组对象转换为一个新的数组实例。如果当数组中只是简单的基本数据类型时,Array.from()会复制原数组本身的值,但原数组里面若有复杂数据类型时则会一并被复制,所以这个时候数组里面关于复杂数据类型的引用也会被一并复制(因为复杂数据类型可能会存在数个未知的引用,js在碰到的时候会偷懒直接将地址的引用一并复制,防止会有引用被遗漏) 对于对象:
1.Object.assign() 不完全深拷贝
因为Object.assign()拷贝的是可枚举属性当被拷贝的源数据是一个对象时,它只会拷贝该对象的引用,后面的源对象的属性会覆盖前面的源对象的属性,如果属性名相同的话。
2.扩展运算符 不完全深拷贝
3.使用json字符串相互转化
4.使用递归实现深拷贝
- Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
- 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。
- 链接:juejin.cn/post/699445…
31.手写深拷贝和浅拷贝
手写浅拷贝
const orginObj = {
name: "xf",
age: '100',
do: {
eat: "鸡腿"
}
}
/**
* 实现浅拷贝
*/
function shalldowCopy(obj) {
//先判断obj是基本数据类型还是复杂数据类型,是否是null,因为基本数据类型没有深拷贝浅拷贝的概念
if (typeof obj !== 'object' || obj == null) return
//判断obj的数据类型来决定创建一个空的数组还是空对象,用instanceof判断,因为instanceof唯一的缺点就是不能判断基本数据类型
//而此处已经排除了基本数据类型的情况
let objCopy = obj instanceof Array ? [] : {}
//使用for in 便利出对象或数组上的key
for (let key in obj) {
//判断源对象或数组中这个key是否存在
//使用Object.hasOwnProperty(key)是因为该方法只可以检测自身对象上的属性不可以检测自身对象原型上的属性
if (obj.hasOwnProperty(key)) {
objCopy[key] = obj[key]
}
}
return objCopy
}
let newObj = shalldowCopy(orginObj)
orginObj.do.eat = '猪脚'
console.log(newObj);//猪脚
手写深拷贝
只需要在浅拷贝的基础上判断对象或数组里面某个元素是否是复杂数据类型还是基本数据类型
const orginObj = {
name: "xf",
age: 100,
do: {
eat: "鸡腿"
}
}
const orginArr = [
1, {
name: 'xf'
}
]
function deepClone(obj) {
//先判断obj是不是复制数据类型或者是null
if (typeof obj !== 'object' || obj === null) return
//判断obj的数据类型是数组还是对象来决定创建一个空对象还是一个空数组
let copyObj = obj instanceof Array ? [] : {}
//使用for in 遍历obj身上的属性
for (let key in obj) {
//判断obj上是否有这个属性 ObjeCt.hasOwnProperty()只能判断自身对象上的属性不能判断自身对象原型上的属性
if (obj.hasOwnProperty(key)) {
//判断数组或对象里面的元素是否是复杂数据类型,如果是复杂数据类型还需要再进行一次该方法
if (obj[key] instanceof Object || obj[key] instanceof Array) {
//将递归调用的值进行赋值
copyObj[key] = deepClone(obj[key])
} else {
copyObj[key] = obj[key]
}
}
}
return copyObj
}
const newObj = deepClone(orginObj)
orginObj.do.eat = '猪脚'
console.log(newObj.do);
const newArr = deepClone(orginArr)
orginArr[1].name = 'cl'
console.log(newArr);
32.let 和const和Var的区别
-
let声明变量不需要赋初始值但是const需要,let声明的变量会放在词法环境中
-
let声明的变量可以更改指针指向但是const 不可以
-
var声明的变量会导致变量提升,var声明的变量会放在变量环境中
33.const定义的变量到底是什么不可以变
const 声明的变量其实是地址值不可以变
对于基本类型数据,const 声明的变量的值就直接存放在所对应的地址值中,基本数据类型都是存放在栈中,所以const声明的基本数据值一旦发生改变就意味着地址值也发生改变所以会报错
对于复杂数据类型虽然声明的地址值不变,但该地址是存放在堆中(指针本身),但会有一个引用指向栈中,所以在修改const声明的复杂数据时,修改的只是对象的引用(指针指向),操作的也是对象的引用(指针指向),并没有直接涉及到地址值(指针本身)的改变,所以不会报错
34.实现一个new 操作符
new操作符的实现本质
1创建一个对象
2将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
3指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
4返回新的对象
function Person(name, age) {
this.name = name
this.age = age
}
function myNew(constructor, ...args) {
//创建一个新对象,该对象要继承目标对象的原型链
//所以使用Object.create(originProto)创建的对象可以继承目标对象的原型链
const newObj = Object.create(constructor.prototype)
// 调用构造函数,并将新对象作为上下文,this指向构造函数中的代码,构造函数中的this指向该对象(newObj)
const result = constructor.apply(newObj, args);
// 如果构造函数自身返回了一个对象,则返回该对象;否则返回新创建的对象
//首先,通过 if (result && (typeof result === 'object' || typeof result === 'function')) 判断构造函数的返回值 result 是否存在且是一个对象或函数。这是因为构造函数有可能返回各种类型的值,包括对象、函数或其他。
// 如果构造函数返回的是一个对象或函数,那么直接返回这个结果。这是因为在某些情况下,构造函数可能故意返回一个已经存在的对象,而不是新创建的对象。
// 如果构造函数返回的不是一个对象或函数,说明它没有显式返回一个新对象,因此我们返回刚刚新创建的对象 obj。这确保了 myNew 函数总是返回一个对象,无论构造函数是否显式返回对象。
if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
return newObj;
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
const newPerson = myNew(Person, '薛峰', '100')
console.log(newPerson);
35.改变this指向
call
支持按照形参一个个的传
Function.prototype.callFun = function (context, ...args) {
//判断调用callFun方法的对象是否是个函数
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.customCall called on non-function');
}
//判断传入上下文的对象是否存在,如果不在就设置为window
context = context || window
//处理传入callFun方法的参数,获得args
//创建一个唯一的key,避免覆盖掉对象自身原有的属性
const uniqueKey = Symbol()
//将函数作为目标对象的属性
context[uniqueKey] = this //this是调用callFun的函数对象
//对应下面的sayHellow.callFun(obj,'cl',20),this就是sayHellow
//调用函数并传递参数
const result = context[uniqueKey](...args)
//删除添加的属性
delete context[uniqueKey]
return result
}
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, sex) {
Person.callFun(this, name, age, sex)
this.sex = sex
}
const stu = new Student('xuef', 23, 'g')
console.log(stu);
const obj = {
a: 1
}
function sayHellow(name, age) {
console.log('我叫', name, '我今年', age, '岁');
}
console.log(sayHellow.callFun(obj, 'cl', 20));
apply
传参必须是数组
bind
bind 方法的本质就是创建一个新的函数,并将其内部的 this 关键字绑定到指定的上下文对象上。当你调用 bind 方法时,它会返回一个新的函数,这个新函数的 this 关键字会被永久地设置为 bind 方法的第一个参数,即指定的上下文对象。内部是通过apply方法实现
Function.prototype.bindFun = function (context) {
//判断调用对象是否是函数
if (typeof this !== "function") {
throw new TypeError('Function.prototype.customCall called on non-function');
}
const fn = this
const args = Array.prototype.slice.call(arguments, 1); // 获取除第一个参数(上下文对象)外的其他参数
return function () {
const bindArgs = Array.prototype.slice.call(arguments); // 获取 bind 方法被调用时传入的参数
return fn.apply(context, args.concat(bindArgs)); // 将上下文对象和两次传入的参数拼接后传递给原始函数
};
36.js的内置对象是什么
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
37.原型链解释
构造函数和实例化对象的关系
在js中每一个复杂数据类型都可以看作是某个构造函数的实例对象,构造函数在通过new操作符创建了一个新的实例化对象后,这个实例对象会继承构造函数的原型链并得到一个prototype属性的对象这个对象里面将会放置该构造函数的所有实例共享的属性和方法。如下代码,构造函数Person,他构造出来的实例化对象person1会继承Person的原型链,在Person的原型上(prototype属性)挂载了一个sayHellow方法,所以打印person1可以看到在该实例化对象中会存在一个prototype属性,里面会放置sayHellow方法。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHellow = function () {
console.log('111111');
}
const person1 = new Person('xf', 99)
console.log(person1.__proto__.sayHellow());
总结:通过构造函数创建的实例化对象会继承构造函数的原型链。在实例化对象内部,会创建一个 prototype 属性,该属性里面包含了源构造函数所有实例共享的属性和方法,包括构造函数的原型。同时,实例化对象内部还会创建一个指针,指向构造函数的 prototype 属性。
原型链的概念
当访问一个对象的某个属性时,如果该对象自身访问不到,那么就会去访问该对象的原型,如果该对象的原型上也访问不到就会去访问对象原型的原型,如此一直向上查找。原型链的尽头一般都是Object.prototype
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHellow = function () {
console.log('111111');
}
Function.prototype.funHH = function () {
console.log('56789');
}
console.log(Object.getPrototypeOf(Person) === Function.prototype);//true
const person1 = new Person('xf', 99)
// console.log(person1.__proto__.sayHellow());
//以下都是Object.prototype了
console.log(Object.getPrototypeOf(Person.prototype) === Object.getPrototypeOf(Function.prototype));//true
修改原型的链的尽头让其不再是Object.prototype使(Object.setPrototypeOf(myObject, customPrototype))方法修改原型链
let customPrototype = {
customProperty: 'This is a custom property at the end of the prototype chain'
};
// 创建一个对象,这个对象的原型链将被修改
let myObject = {};
// 将对象的原型指向自定义的原型对象
Object.setPrototypeOf(myObject, customPrototype);
// 现在,myObject 的原型链的尽头就是 customPrototype
// 测试访问自定义属性
console.log(myObject.customProperty); // 输出: This is a custom property at the end of the prototype chain
原型链的特点
JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
原型链的指向
解释最后两个:
首先访问了 p1 对象的原型 (proto),然后获取了原型的 constructor 属性。由于 p1 是通过 Person 构造函数创建的实例,其原型链上的构造函数就是 Person,因此 p1.proto.constructor 返回的是 Person。
另一方面,Person.prototype.constructor 是通过 Person 构造函数创建的原型对象的 constructor 属性。因为在创建 Person 构造函数时,它的原型对象默认是一个空对象,JavaScript 自动设置 Person.prototype.constructor 指向构造函数本身。因此,Person.prototype.constructor 也指向 Person 构造函数。
总结起来,p1.proto.constructor 和 Person.prototype.constructor 都指向 Person 构造函数,因为 p1 是由 Person 构造函数创建的实例,而 Person.prototype 是该实例的原型对象。
构造函数的prototype属性和constructor属性的关系
prototype
当你创建一个构造函数的时候,它会自动获得一个prototype对象,在该对象上挂载的方法或属性可以被该构造函数创建的实例化对象共享和使用,所有通过该构造函数创建的实例对象都会共享到这个prototype对象
constructor
在 JavaScript 中,当你创建一个自定义构造函数时,该构造函数的 prototype 属性会自动获得一个 constructor 属性,指向该构造函数本身。示例如下
function Person(name) {
this.name = name;
}
const person = new Person('John');
在这个例子中,Person.prototype.constructor 将会指向 Person 构造函数本身,因为 Person 是创建 person 对象的构造函数
如何获得对象非原型链上的属性
1.不完全,因为for in只可以获得自身对象上的所有可枚举属性,虽然hasOwnProperty()可以获得到自身对象上的所有属性,但被外层循环限制住了
2.使用Object.getOwnPropertyNames(obj) 仅可以获得到自身对象上的所有属性
const obj = {
name: "xf",
age: 100
};
Object.defineProperty(obj, 'age', {
enumerable: false
});
let res = Object.getOwnPropertyNames(obj);
console.log(res);//['name','age']
38.继承
39.事件循环
主线程队列和其他队列
(必须等待主线程上的任务执行完成后才会去执行其他队列里面的任务,微队列的任务优先级是最高的)
下方代码的打印顺序是54312
function fn1() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
})
}
setTimeout(() => {
console.log(3);
Promise.resolve().then(fn1)
}, 0)
Promise.resolve().then(function () {
console.log(4);
})
console.log(5);
1.主线程任务: console.log(5);
2.微队列任务: Promise.resolve().then()
3.延时队列任务:setTimeout
解释:主线程会率先执行当前的全局代码,不同的代码会进入不同的队列中去,首先 console.log(5);会被分发进入主线程,下述代码会被分发进入微队列
Promise.resolve().then(function () {
console.log(4);
})
setTimeout(() => {
console.log(3);
Promise.resolve().then(fn1)
}, 0)
当主线程上的console.log(5)执行完后,就会立即执行微队列里面的任务,当微队列里面的任务被执行完后就会去执行延时队列里面的任务,延时对列里面的任务又会被进行拆解, console.log(3)进入主线程, Promise.resolve().then(fn1())进入微队列,所以等到主线程的console.log(3)执行完后才会执行Promise.resolve().then(fn1()),当执行到Promise.resolve().then(fn1())时因为里面有fn1,又会对付fn1进行执行拆解(规则如上),所以最后打印顺序是54312
浏览器的进程模型
什么是进程
不同的任务(app)占用的空间
什么是线程
运行代码的‘人’就是线程
浏览器有哪些进程
1.浏览器进程
2.网络进程
3.渲染进程
什么是事件循环
事件循环又叫做消息循环,是一个不会结束的for循环,每次循环会从消息对列里面取出第一个任务交给主线程去执行,而其他的线程只需要在合适的事件将任务加入到队列末尾就可以了
什么是异步(浏览器为什么采用异步的模式)
js是一门单线程语言,是因为它在运行浏览器的渲染主线程中,而渲染主线程就只有一个。但渲染主线程上又进行着很多的工作,如渲染页面,执行js代码。
如果使用同步的方式就很有可能会导致主线程发生阻塞,从而导致消息队列里面的很多其他任务无法被顺利执行,一方面会照成主线程卡顿浪费时间,另一方面会照成页面渲染无法及时更新
所以浏览器采用异步的方式来避免这些问题,具体的做法是当某些任务发生时,比如计时器,网络请求,事件监听,主线程会将这些任务交给其他的线程去进行处理,而主线程会立即结束对这些任务的执行,转而接着去执行主线程里面其他任务,当其他线程完成时(比如定时器的时间到了),会立即将事先从主线程传递过来的回调函数包装成任务,加入到消息队列的末尾,等待主线程的执行
js为什么会阻塞渲染
渲染主线程是如何进行工作的
40.浏览器的渲染原理
layer是对布局树进行分层,这样既可以提高效率又可以防止对整个布局树的绘制,主线程会为每个层单独产生绘制指令集,用于描述这一层的内容要如何绘制出来。
完成绘制后,主线程将·每个图层的信息提交给合成线程,剩余的工作交给合成线程去进行。
合成线程首先对每个图层进行分块,将其划分为更小的区域。它会从线程池中拿取多个·线程来进行分块工作。
分块完成后会进行光栅阶段。
合成线程会将信息交给GPU进程,以极高的速度去完成光栅化。
GPU进程会开启多个线程来进行光栅化,并且优先处理靠近视口区域的块,光栅化的结果就是一块一块的位图
最后一个过程就是画了,合成线程拿到每个层每个块的信息后就会生成一个个的指引信息,然后将其绘制到页面。
在浏览器输入一个地址会发生什么
1.网络进程拿到一个html文档,然后生成一个渲染任务给渲染主线程的消息队列,主线程拿到这个渲染任务后在事件循环机制下就会开启渲染流程
html的解析过程遇到了css代码该怎么办(如何提高整体的解析速度)(为什么css的解析不会阻塞html的解析)
为了提高解析效率,浏览器会开启与一个预解析器去解析和下载css(预解析器就是新开辟的一个线程专门用来解析和下载css).这样渲染主线程就可以在这个期间只负责解析html,css交给预解析去进行,预解析线程会快速浏览css,然后通知网路线程进行对css的下载,下载完成后再回到预解析线程,预解析线程在完成解析后会将解析结果交给主线程,主线程会将这个结果生成CSSOM树(解析html和解析css不在同一个线程上进行,css的解析结果会按照事件循环机制进入主消息队列,排队等待主线程的执行并生成CSSOM树,所以解析css不会造成解析html阻塞)
html解析过程遇到js怎么办(为什么js会阻塞html解析的根本原因)
浏览器开启的预解析线程会率先浏览js,如果主线程解析到scipt位置时,会停止解析,等到网络进程将js文件下载好并传递给主线程后,才会去继续进行解析,这是因为js代码可能会更改当前的dom树,所以遇到js代码时可能会阻塞html的解析。(解析js和下载js虽然都不是在主线程上进行,但js代码会可能对当前的dom进行更改,所以会等待js文件下载完才会继续解析html)
布局树的生成
布局阶段会依次遍历DOM树的每一个节点,计算每个节点的几何信息,如节点的宽高,相对包含块的位置,大部分时候,DOM树和布局树并非一一对应,比如display:none的节点没有几何信息,因此不会生成布局树,又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中,还有匿名行盒,匿名块盒等都会导致DOM树和布局树无法一一对应
什么是浏览器的渲染
html字符串 ===》屏幕上的像素信息
浏览器是如何让渲染页面的
为什么transfrom效率高
1.因为transfrom既不会影响布局也不会影响绘制指令,它只会影响渲染流程的最后一个draw阶段,由于draw阶段是在合成线程中的,所以transfrom也不会影响到渲染主线程,反之渲染主线程不管再怎么忙都不会影响transfrom
2.transfrom不会触发重排和重绘,从而会减少浏览器对页面布局和样式的计算和渲染的工作量,这是因为trnsfrom是由GPU(图形处理单元)来进行处理的并不是由CPU进行处理的,因此可以在GPU线程中异步处理,不会阻塞渲染主线程。同时transfrom也利用了GPU加速可以避免触发重排和重绘。
什么是refolw
reflow的本质就是会重新计算layout(布局)树。
当进行了会影响布局的操作后,需要重新计算布局树,就会引发layout.
为了避免连续的多次的操作导致布局树反复计算,浏览器会合并这些操作,当js代码全部完成后再进行统一的计算,所以,改动属性造成的reflow是异步完成的,也因为如此当js获得布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下就会觉定获取属性立即进行reflow。
为什么设置属性不会立即造成reflow但读取属性会立即造成reflow
在浏览器中,当对会影响布局的属性进行设置时,浏览器通常会将这些修改任务推迟到异步队列中进行处理,而不是立即执行。这是因为浏览器采取了优化策略,将布局更新任务放到单独的线程中执行,以避免频繁的重排(reflow),从而提高性能和用户体验。因此,设置属性通常不会立即触发重排。
相反,读取布局属性的操作通常需要立即获取最新的布局信息,因此会强制浏览器立即进行重排以确保返回准确的结果。由于这些操作需要即时的布局信息,所以它们会立即触发重排,而不会等待浏览器的异步优化处理。"
这样修改后更加准确地解释了为什么设置属性不会立即触发重排,而读取属性会立即触发重排。
什么是repaint(重绘)
重绘的本质就是根据分层信息进行了绘制指令。当改变了可见的样式后,就需要重新计算,就会进行重绘。
由于元素的布局信息也属于可见样式,所以reflow一定会引起重绘。
41.js延迟脚本加载的方法
延迟js加载就是等页面加载完成后再去加载js,这样可以让页面加载更为流畅
1.setTimeout
2.给js脚本添加async属性让脚本异步加载
3.将js脚本放在文档的最底部,尽可能的使js最后加载
4.defer属性,使用defer属性可以让js脚本的加载和文档的解析同时进行,然后再文档解析完成后再去执行js脚本,不会照成页面阻塞。(只对从外部引入的js脚本有效,写在本页面的无效)
5.动态创建dom的方式,可以去监听文档的加载事件,等加载完成后再引入script标签
42.如何快速将类数组如何转为数组
问题的本质在于js的方法和函数的上下文如何传递,如何不修改原型和原型链就可以创建一个数组副本,如何将类数组的每一项整合到真正的数组中,有两种方法,第一种就是使用循环往一个空数组中进行追加元素,另一种就是通过改变this指向使用数组原型上的方法,但是一定要确保类数组里面的元素不会被丢掉,以下三个方法是因为可以确保类数组转为数组后类数组里面的元素不会被丢弃。
const obj = {
0: "apple",
1: "banana",
2: "orange",
3: 1,
length: 4// 设置 length 属性
};
console.log(Array.prototype.filter.call(obj, function (item) {
return typeof item == 'string';
}))
console.log(Array.prototype.filter.apply(obj, [function (item) {
return typeof item === 'string';
}]));
console.log(Array.prototype.slice.call(obj));
console.log(Array.prototype.splice.call(obj, 0));
1.Array.prototype.slice.call(likeArr)
使用数组的slice方法因为slice方法不会修改原数组但会返回一个新数组,再使用call改变this指向,在类数组上调用数组的slice方法。
2.Array.from(likeArr)
因为Array.from()方法可以将可迭代对象(包括类数组对象和可迭代的值)或类数组转为一个新的数组实例。
3.Array.prototype.splice.call(likeArr,0)
4.Array.prototype.concact.apply([],likeArr)
43.对Ajax的理解
44.Promise
promise本身是一个对象
promise本身只有两个状态,
45.宏任务和微任务
46.es6的模块化和CommonJs有什么不同
- 语法差异:
-
- CommonJS 使用 require() 导入模块,使用 module.exports 导出模块。
- ES6 模块化使用 import 导入模块,使用 export 导出模块。
- 静态 vs 动态:
-
- ES6 模块化是静态的,意味着模块的依赖关系在代码执行前就确定了,而且 import 和 export 声明必须位于模块的顶层。
- CommonJS 是动态的,require() 可以在运行时根据条件加载模块,这使得它不太适合在浏览器环境下使用,因为浏览器环境无法确定加载的模块。
- 导出方式:
-
- CommonJS 导出的是值的拷贝,即使模块中的值发生变化,导出的值也不会变化。这是因为 CommonJS 导出的是值的缓存,而不是值本身。
- ES6 模块化导出的是值的引用,所以导出的值会随着模块中值的变化而变化。
- 浏览器支持:
-
- ES6 模块化在现代浏览器中得到了广泛支持,可以直接在浏览器中使用。
- CommonJS 不适用于浏览器环境,需要使用工具将其转换为可在浏览器中运行的代码。
47.如何判断一个对象是否属于某个类
1. instanceof
判断构造函数的prototype属性是否在某一个特定的对象原型链上
2. Object.prototype.toString()
打印对象的[[Class]]属性来判断(需要注意的是toString方法可能会被重写)
3. constructor
通过对象的构造函数也就是constructor属性来进行判断(需要注意的是构造函数的constructor属性可能会被重写)
48.如何使用for ...of 遍历对象
for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。
49. Object.entries(obj)方法
50.自定义迭代器
可以给对象自定义迭代器属性,只需要将data换为对象的键数组
const myIterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]: function() {
let index = 0;
const data = this.data;
return {
next: function() {
return index < data.length ?
{ value: data[index++], done: false } :
{ value: undefined, done: true };
}
};
}
};
for (const value of myIterable) {
console.log(value);
}
51.Ajax ,Axios ,fetch的区别
1.Ajax
'异步的js',Ajax是一个技术统称,是一个概念模型,它包括了很多技术,并不特指某一个技术,它很重要的特性之一就是让页面实现局部刷新。是用来实现网络请求,可以利用XMLHttpRequest实现的
function ajax() {
//创建XHR对象
const xhr = new XMLHttpRequest()
//初始化XHR对象 open方法
xhr.open('GET', url, true)
// xhr.setRequestHeader('Content-Type', 'application/json');如果是post请求需要这段代码来设置请求头控制数据传输格式
//设置处理响应的回调函数
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
console.log('响应成功', xhr.response);
}
}
}
//发送
xhr.send(data)//get就是null post就是需要发送的对象
}
缺点:
本身是针对MVC编程,不符合前端的MVVM
基于原生的XMLHttpRequest开发的,XHR本身就很繁琐
不符合关注分离的原则
配置和调用的方式很混乱,基于事件的异步模型也不好
2.Fetch
是es6的一个Api,是基于Promise实现的,和XMLHttpRequest没有关系
缺点:
fetch只对网络请求报错,对400,500都当作请求成功,服务器返回400,500错误码时不会自己reject,只有在网络请求发生错误时才会导致这些请求不能完成,fetch的状态才会变为reject
fetch默认不会携带cookie,需要添加配置项:fetch(url,{credentials:"include"})
fetch没有abort功能,不支持请求超时控制,使用setTimeout以及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。
fetch不能监测请求的进度但XHR可以
3.Axios
Axios 是一种基于Promise封装的HTTP客户端
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
52.yield
53.数组遍历的方法
cuggz.blog.csdn.net/article/det…
54.数组去重
const arrmore = [ 0, 1, 2, 1, 2, {}, {}, [], []
]
function uniqueArrSet(arr) {
return new Set(arr)
}
uniqueArrSet(arrmore)
function uniqueArrFor(arr) {
let uniqueArr = []
for (let i = 0; i < arr.length; i++) {
if (!uniqueArr.includes(arr[i])) {
uniqueArr.push(arr[i])
}
}
return uniqueArr
}
//上面两种当数组内部有重复的复杂数据类型时都无法顺利去重,因为复杂数据类型无法通过值得比较来进行去重
/**
* 使用复杂数据转json字符串进行去重
*/
function uniqueJson(arr) {
let useArr = []
let uniqueSet = new Set()
for (let i = 0; i < arr.length; i++) {
//准备一个set集合进行基本数据类型去重,用来存放数组里面的复杂数据类型转为json字符串
let item = arr[i]
//判断数组的i项类型是不是复杂数据类型,如果是的就将其转为json字符串并且存到一个临时的数组里面
let jsonItem = (typeof item === 'object') ? JSON.stringify(item) : item
//判断
if (!uniqueSet.has(jsonItem)) {
//将json字符串转变回来push进数组
const normItem = JSON.parse(jsonItem)
useArr.push(normItem)
uniqueSet.add(jsonItem)
}
}
return useArr
}
/**
* 递归去重
*/
function uniqueJsonRecursive(arr) {
let uniqueArr = [];
function isDuplicate(item) {
return uniqueArr.some(uniqueItem => {
if (typeof uniqueItem === 'object') {
return JSON.stringify(uniqueItem) === JSON.stringify(item);
} else {
return uniqueItem === item;
}
});
}
function recursiveUnique(item) {
if (typeof item !== 'object' || Object.keys(item).length === 0) {
if (!isDuplicate(item)) {
uniqueArr.push(item);
}
} else {
if (!isDuplicate(item)) {
uniqueArr.push(item);
}
for (let key in item) {
recursiveUnique(item[key]);
}
}
}
arr.forEach(item => {
recursiveUnique(item);
});
return uniqueArr;
}
const arrWithDuplicates = [0, 1, 2, 1, 2, { a: { b: 1 } }, { a: { b: 1 } }, [], []];
console.log(uniqueJsonRecursive(arrWithDuplicates));
55.实现数组的扁平化
const arr1 = [ 1, 2, 3, [4, 5, 6], [7, 8, 9, [10, 11]]
]
/**
* 使用while循环加concact实现 如果三维数组就不行了
*/
function myFlatWhile(arr) {
//使用some方法查看数组的每一项是否是数组
while (arr.some(v => Array.isArray(v))) {
arr = [].concat(...arr)
}
return arr
}
/**
* 递归map实现
*/
function myFlatPcoss(arr) {
return [].concat(...arr.map(v => Array.isArray(v) ? myFlatPcoss(v) : v))
}
/**
* 递归some实现
*/
function myFlatSome(arr) {
arr.some(v => {
if (Array.isArray(v)) {
arr = [].concat(...arr)
myFlatSome(v)
}
})
return arr
}
56.为什么需要虚拟DOM
1.框架设计层面
数据驱动
2.跨平台
抽象层不能绑定具体的环境,浏览器环境才会有真实dom,一旦脱离浏览器环境其他的环境就不一定能识别真 实DOM,所以需要一个所有平台都能识别的DOM,所以就出现了虚拟DOM.
57.为什么vue引入组件的时候需要前缀(),但react不需要()
-
因为vue的模板函数在编译后会成为一个render函数,而render函数会渲染出虚拟节点,如果模板里的标签是大驼峰的就会被识别为一个组件,如果是短横线的标签横线会被去掉转为大驼峰被识别为一个组件,但是如果不采取横线模板编译后就无法识别出标签是html的标签还是单独的一个组件如()
-
react使用的是jsx语法,balel的转化规则有一项就是如果是小写就转为html标签如果是大驼峰就转为组件
58.ts协变和逆变(类型安全)
包裹与被包裹,保证所有成员可用
59.js的计时器是否精确
1.硬件
复制代码
计算机是使用寄存器进行记时的无法做到精确
2.系统
不同的操作系统有不同的记时时限,并且这个记时时限本身也是不够精确的
3.标准
w3c规定不论使用setTimeout还是setInterval如果嵌套大于5层,那么它的记时间隔最小四毫秒
4.事件循环
受事件循环机制的影响,记时器哪怕到了指定的时也不一定会立即执行,只有当执行栈清空后,才会去执 行计时器。
60.arr[0]和arr['0']
const arr = [1, 2, 3]
console.log(arr['2']); //3
const arr1 = []
arr[0] = 2
arr['0'] = 2
console.log(arr[0] + arr['0']);//4
在js中数组本质上其实也是一个对象,对象的属性又只能是个字符串所以arr['0']就是arr[0]
61.为什么a元素不会继承父元素的颜色
css有明确的规则,只有当一个元素没有相关的样式的时候才会去从父元素继承
62.为什么span是行级元素
如div浏览器的默认样式表里面设置了display:block,但span元素没有设置display属性,没有设置display属性值就默认是inline
实际上在html5里面已经完全摒弃了行级元素和块级元素的说法因为它认为元素是行级还是块级完全是取决于css的display属性
63.为什么浮动的a元素可以设置宽高
因为在css里面任何一个元素只要浮动起来就会变为块盒dispaly属性转为block
64.画三角形
65.css3的object-fit属性
66.css规定文字排布的时候要避开浮动元素
67.为什么给父元素设置over-flow:hidden可以清除浮动
68.如何让网页快速变为黑白
给根元素html设置filter属性:grayscale(1)
69.什么叫包含块
- 并不是所有元素的包含块都是父元素的内容区域(如给父元素设置display:fixed)
70.为什么0.3-0.2!==0.1
因为计算机在运算时是将10进制转为二进制再去运算,而有的10进制转为二进制是一个没有穷尽的0和1组成的字符串(如0.2)
71.求余和求模的区别
72.外边距合并问题
73.无线循环和无线递归的区别
1.无线循环不会报错,不会占用空间,只会消耗cpu性能
2。无线递归会报错,会导致栈溢出
74.微前端中的应用隔离是什么
75.为什么jsonp可以解决跨域
76.windows 1px和缩放比例的关系以及mac
欢迎大家阅读
本文还有很多东西有待补全,知识点也是不够全面和详细欢迎大家阅读并给予指导,本人一定悉心受教。