目录
用 constructor 构造函数的方式,判断某个实例对象是不是这个构造函数创建的。
通过Object.prototype.toString.call()。最全面的
1、Function内置的构造函数和Object内置的构造函数他们之间的原形链的关系什么?
一、对象与 JSON 的区别?
- JSON 是对象的一个严格的子集
- JOSN 中的key 必须加 " "
- JSON 不能赋值
- 使用JSON.parse() 会把JSON数据中值为undefined的数据过滤掉
1、怎么让对象对自身进行深拷贝
function ss(ob) {
let obj = {}
for (let a in ob) {
if (ob[a] === ob) return Object.assign({}, ob)
}
}
var dd = { name: '搜索', age: 16 }
// 通过它触发改变
Object.defineProperty(dd, 'age', {
get() { return this }
})
let aa = ss(dd)
console.log('aa', aa);
console.log('判断', aa === dd);
二、怎么判断js类型
-
通过 typeof 判断。
console.log(typeof 2); // number console.log(typeof '字符'); // string console.log(typeof true); // boolean console.log(typeof null); // object console.log(typeof undefined); // undefined console.log(typeof Symbol(3)); // symbol console.log(typeof function(){}); // function console.log(typeof NaN); // number console.log(typeof {name:'老六',age:66}); // object console.log(typeof ['1',2,'3']); // object
用来判断基本数据类型,难以判断函数除外的复杂数据类型
-
用 constructor 构造函数的方式,判断某个实例对象是不是这个构造函数创建的。
let a = 2 let b = '字符' let c = true let obj = {name:'老六',age:66} let arr = ['1',2,'3'] let fun = function(){} console.log(a.constructor == Number); // true console.log(b.constructor == String); // true console.log(c.constructor == Boolean); // true console.log(obj.constructor == Object); // true console.log(arr.constructor == Array); // true console.log(fun.constructor == Function); // true
原理:因为构造函数创建的时候,会有一个prototype属性指向原型对象,而原型对象中会有一个constructor属性指回来构造函数
-
通过Object.prototype.toString.call()。最全面的
console.log(Object.prototype.toString.call(2)); // [object Number] console.log(Object.prototype.toString.call('字符')); // [object String] console.log(Object.prototype.toString.call(true)); // [object Boolean] console.log(Object.prototype.toString.call(null)); // [object Null] console.log(Object.prototype.toString.call(undefined)); // [object Undefined] console.log(Object.prototype.toString.call(Symbol(3))); // [object Symbol] console.log(Object.prototype.toString.call(function(){})); // [object Function] console.log(Object.prototype.toString.call(NaN)); // [object Number] console.log(Object.prototype.toString.call({name:'老六',age:66})); // [object Object] console.log(Object.prototype.toString.call(['1',2,'3'])); // [object Array]
必须是Object原型对象上的toString方法才能判断,其他原型对象上的不能判断。
并且进行数据类型判断,我们需要借助apply方法或者call方法或者bind方法。
例如:Array原型对象上的toString方法是判断不了的,因为它重写了toString方法。
注意:判断不了自定义构造函数创建出来的实例对象
-
通过 instanceof 进行判断
console.log(function(){} instanceof Function); // true console.log(new Date instanceof Date); // true console.log({name:'老六',age:66} instanceof Object); // true console.log(['1',2,'3'] instanceof Array); // true
instanceof:检查一个对象(引用类型)是否为构造函数(类)的实例
可以用来判断内置对象
三、 this指向的问题
- 方法() 调用 this指向window
- 一个函数通过对象打点调用,this指向这个对象
- 事件处理函数,谁触发this就指向谁
- 定时器里面的匿名函数this指向window
- 一个方法在数组里面通过下标的方式去调用,this指向数组,
总结:谁调用 this就指向谁
四、改变 this 指向的方法
call():可以调用函数,同时改变this的指向,只能传字符串形式参数
apply():可以调用函数,同时改变this指向,只能传数组形式参数
bind() :不能调用函数,它是重新定义一个函数并返回,只能传字符串形式参数
function fun(a,b){
console.log('this',this);
console.log(a,b);
// 调用fun(obj,'普通'),返回undefined,因为window身上没有age
console.log(this.age);
}
var obj = {
name: '小明',
age:5
}
fun(obj,'普通') // this 指向window
fun.call(obj,3,333) // this 指向obj
fun.apply(obj,[6,666]) // this 指向obj
fun.bind(obj)(9,999) // this 指向obj
五、new 的关键字用什么作用
在函数内部(内存)创建了一个局部变量,是一个空的对象
构造函数中的this(上下文)指向这个空的对象
在这新对象中添加一个__proto__属性,指向构造函数的原型对象prototype
所有的语句执行完毕,函数将自动return这个新对象(this),如果是引用类型,就返回这个引用类型的对象(值)
手写流程:
// 手写new
function newFunc(Func,...args) {
// 1.创建一个新对象
let newObj = {}
// 2.将新对象和构造函数通过原型链连接
newObj.__proto__ = Func.prototype
/*
注意:可以通过【Object.create(新创建对象的原型对象)】,来简化第一、二步骤
let newObj = Object.create(Func.prototype)
*/
// 3.将构造函数的this绑定到新对象上
const result = Func.apply(newObj,args)
// onsole.log(result) // 值为undefined
// 4.根据返回值类型判断,,如果是引用类型直接返回值,否则返回this(创建的新对象)
return result instanceof Object ? result : newObj
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
// 若没有return 相当于 return undefined
// return {sex:'男'} // 返会引用类型
}
// 添加往Person原型对象上添加一个sayName方法
Person.prototype.sayName = function (){
console.log('名字:',this.name);
}
const person1 = newFunc(Person, 'Tom', '18')
console.log(person1);
person1.sayName()
六、什么是构造函数?
- 当一个函数被new调用的时候,这个函数就是一个构造函数
构造函数就是一个普通的函数,里面可以写任何的语句,只不过this指向的是一个对象
构造函数里面写了return的语句会发生两种不同的处理方式。如下:
- 如果return 基本数据类型(number null boolean string undefined)的话,程序会被打断执行
- 如果return 引用数据类型(Array Date Function Object。。。),那么return的值会覆盖掉原来的值
1、Function内置的构造函数和Object内置的构造函数他们之间的原形链的关系什么?
- 任何的函数都是Function 的实例对象
- Object也是Function new出来的实例对象
- Function 自己也是自己的实例对象
- 任何对象都是Object的实例对象。
2、JS 面向对象的内置构造函数
-
Object:用于创建对象
字面量形式创建的对象其实都是Object new的实例对象。
-
Function:创建一个方法
字面量创建的函数都是Function new出来的实例对象
-
Array:用于构造数组
任何的数组的字面量的形式的对象,都是Array new出来的实例对象
-
Number:
任何字面量创建number类型都是Number new出来的实例对象
注意:使用 Number 构造函数创建的对象,也可以进行计算,但有时候有坑
var a = new Number(9)
console.log(a);
if(a) alert('success')
else alert('fail')
-
String:
字符串有很多方法,这些方法都是定义在String的prototype上面(原形上面)
-
Boolean
Var a=true
Var a=new Boolean(true)
总结: 所有的内置的对象,其实都是内置的构造函数
七、原型、原型对象、原型链
Prototype:每一个构造函数都有属性,指向一个空的对象(原型)
Prototype 不需要去定义,天生就有
People.prototype 是People 构造函数的原型
People.prototype 是小明或者小红的原型对象
原型链:任何一个构造函数都有一个属性叫做prototype指向了一个对象,当这个构造函数被new出来的时候,它的每一个实例的__proto__属性 也指向这个对象
__proto__:原型链查找的功能,当小明或者小红没有这个属性或者方法的时候,她就会沿着原型链往上找,直到确定原型对象是否有这个属性或者方法。
Constructor属性:任何一个构造函数的prototype身上都有一个 constructor属性指向他的构造函数
function person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } person.prototype.toString = function () { console.log('aaa'); return "person[" + this.name + "," + this.age + "," + this.gender + "]" } var a = new person("猪八戒", 29, "男"); console.log(a); //一个实例化对象,a a.toString() // 打印 aaa console.log(a.__proto__.constructor); // 打印 person这个函数
instanceof运算符:用来检测一个对象是不是某一个构造函数的实例
console.log(对象 instanceof 构造函数);
总结:函数的prototype指向谁,函数new出来的玩意的__proto__就指向谁
面试题:
八、继承
1、原型链继承
缺点:多个实例化对象共享一个原型对象,一个发送改变,另一个也随着改变
function Person() {
this.name = 'xiangming'
this.age = [1, 2, 3]
}
Person.prototype.shuohua = function () {
console.log("说话")
}
function obj() {
this.type = '老六'
}
obj.prototype = new Person()
var a = new obj()
console.log(a);
a.shuohua()
2、构造函数继承(借助 call)
优点:解决原型链继承不能传递参数给父类
缺点:父类定义方法只能定义在父类的构造函数里面,不能定义在原型上
总结:只能继承父类的实例属性和方法,不能继承原型属性或者方法。
function Person(age){
this.name = '小明'
this.age = age
this.sayName = function(){
return this.name
}
}
// 不能定义在外面
// Person.prototype.sayName = function(){
// return "this.name"
// }
function Chinese(name,age){
Person.call(this,666)
}
var xiaoming = new Chinese(666)
console.log(xiaoming);
console.log(xiaoming.sayName()); // 小明
3、组合继承(前两种的组合)
原型链继承与构造函数继承的组合
优点:能够解决原型链继承与构造函数继承的问题
缺点:构造函数多执行了一遍,另外进行了一次性能开销
function Person() {
this.name = '老六';
this.play = [1, 2, 3];
}
// 往Person原型上添加方法
Person.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Person()
Person.call(this);
this.type = 'child3';
}
// 第一次调用 Person()
Child3.prototype = new Person();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'老六'
console.log(s4.getName()); // 正常输出'老六'
console.log(Child3.prototype.constructor);
4、原型式继承(Object.create)
Object.create()方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。可以实现浅拷贝的作用
缺点:
- 多个实例化对象共享一个原型对象,一个发送改变,另一个也随着改变,存在篡改的可能
- 父类定义方法只能定义在父类的构造函数里面,不能定义在原型上
- 拷贝后的对象(子类)不能直接添加方法,需要添加到原型对象上
let Person={
name : 'xiangming',
age : [1,2,3],
getName: function () {
console.log(this.name);
}
}
let obj1 = Object.create(Person)
obj1.age.push(4)
let obj2 = Object.create(Person)
obj2.age.push('44')
obj1.str=function(){
console.log('我是新增的方法');
}
console.log('obj',obj1.age); //[1,2,3,4,'44']
console.log('obj',obj2.age); //[1,2,3,4,'44']
console.log('obj',obj1.__proto__); // Person
obj1.getName() // xiangming
obj2.str() // 报错。需要添加到原型对象上 obj1.__proto__.str
//【Object.create的实现原理?】
function obj_create(o){
function F(){} //构造函数
F.prototype = o; // 把F的原型替换为传递过来的Person
return new F()
}
let Person={
name : 'xiangming',
age : [1,2,3]
}
let AA = obj_create(Person) // AA 是F的实例化对象
console.log(AA); // 可实现原型链的向上查找功能
5、寄生式继承
寄生式继承:使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法。
优缺点和原型式继承一样
function obj_create(o) {
function F() { } //构造函数
F.prototype = o; // 把F的原型替换为传递过来的Person
return new F()
}
function obj(o) {
var chone = obj_create(o)
chone.sayHi = function () {
console.log('hi')
}
return chone
}
var Person = {
name: "jill",
frindes: ["sk", "xiaoming", "xiaogang"],
age: function () {
console.log(this.name);
}
}
var A = obj(Person)
console.log(A);
A.sayHi()
A.age()
6、寄生组合式继承
结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式
九、JS的执行机制
- 同步任务:代码从上到下顺序执行
- 异步任务:又分为 宏任务 与 微任务
- 宏任务:script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
- 微任务:Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)
运行机制:所有的同步任务都是在主进程的执行中形成一个执行栈,主进程之外还有一个”任务队列(异步任务队列)“,在这个队列中先执行宏任务,在清空(执行)当前宏任务中的所有微任务,然后进行下一个tick形成循环。
<script setup lang='ts'>
async function Prom() {
console.log('y');
await Promise.resolve() // await 相对于微任务
console.log('x'); // 相当于写在Promise.then里面
}
setTimeout(()=>{
console.log('1');
Promise.resolve().then(()=>{
console.log('2');
})
},0)
setTimeout(()=>{
console.log('3');
Promise.resolve().then(()=>{
console.log('4');
})
},0)
Promise.resolve().then(()=>{
console.log('5');
})
Promise.resolve().then(()=>{
console.log('6');
})
Prom()
console.log(0);
// console.log('y 0 5 6 x 1 2 3 4'); // 正确输出结果
</script>
十、闭包
闭包前我们需要清楚自执行函数、函数作用域、内存回收机制、作用域继承
- 自执行函数:可以无需调用,自动执行函数,传参方便
/* 特性:1.自执行函数是很自私的,它的内部可以访问全局变量。 2.但是除了自执行函数自身内部,是无法访问它的。 3.自执行函数无需给函数名,因为根本没办法在其他地方调用,它本身只会执行一次 */ function s1(a1,b1){ return sum1 = a1 + b1 } (function s2(a2,b2){ console.log('内部',s2) // 打印s2函数 return sum2 = a2 + b2 ;})() console.log(s1) // 打印s1函数 console.log(s2) //报错:autoFunc.html:38 Uncaught ReferenceError: s2 is not defined /* 总结:1.能够实现作用域的绝对隔离和函数命名冲突 2.主要用于闭包和创建独立的命名空间两个方面 3.自执行函数将某些代码包裹起来可以实现块级作用域的效果,减少全局变量的数量 4.自执行函数执行结束后变量就会被内存释放掉,从而也会节省了内存。 */
- 函数作用域:函数要执行时就会在内存里面创建一个独立作用域————封闭的盒子。
/*在函数执行完毕,这个独立作用域就会删除。 有一种情况下这个封闭的盒子是不会删除的,那就是“闭包”,*/ // 函数执行 function fn(){ var a = 1 } // 函数执行完毕
- 内存回收机制:把用不到的内容空间,系统会自动清理回收提供给其它程序使用,
/*内部函数引用外部的函数的变量,外部函数执行完毕,作用域也不会删除。 从而形成了一种不删除的独立作用域。*/ function fn(){ // b的独立作用域是不删除的,因为被内部函数引用了 let b = 2 return function(){ // 内部函数引用了外部函数的变量 b console.log(b) } }
某一个变量或者对象被引用,因此在回收的时候不会释放它,因为被引用代表着被使用,回收机制不会对正在引用的变量或对象进行回收的。
- 作用域继承:在子作用域中可获取父作用域中的东西,但父作用域获取不到子作用域的东西
1、什么是闭包
function fn(){
// b的独立作用域是不删除的,因为被内部函数引用了
let b = 2
return function(){
// 内部函数引用了外部函数的变量 b
console.log(b)
}
}
在一个函数里边再定义一个函数。这个内部函数一直保持有对外部函数中作用域的访问(小盒子可以一直访问大盒子但大盒子不能访问小盒子)。
函数执行,形成一个独立作用域,保护里边的私有变量不受外界的干扰,除了保护私有变量外,还可以存储一些内容,这样的模式叫做闭包。
2、闭包的作用
通过一系方法,将函数内部的变量(局部变量)转化为全局变量
3、闭包用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function f1() {
var n = 999;
nAdd = function () {
alert(n += 1);
}
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); //999
nAdd();//1000
result(); //1000
注意:上面代码,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。 其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个操控者,可以在函数外部对函数内部的局部变量进行操作。
4、使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
5、经典面试题
for (var i = 1; i < 5; i++) {
setTimeout(function () {
console.log(i); // 5 5 5 5
}, 0);
}
由于JavaScript是单线程的,setTimeout 是异步任务,JS会将其放入到任务队列中,待同步任务执行完毕后,才执行任务队列中的异步任务。
因为setTimeout函数也是一种闭包,往上找它的父级作用域链(window),因为变量 i 是用 var 声明的全局变量,会被挂载到 window 上,所以变量i的值变成了i = 5,最后执行setTimeout时输出4个5
解决方案:
- 使用立即执行函数锁定参数值,把每次循环的索引传入,从而锁定索引值
for (var i = 1; i < 5; i++) { (function (i) { setTimeout(function () { console.log(i); }, 0); })(i); }
- 使用 let 声明(块级作用域)
// 使用块级作用域变量关键字,会为每次循环创建独立的变量,从而每次打印都会有正确的索引值。 for (let i = 1; i <= 5; i++) { setTimeout(function () { console.log(i); }, 0); }