- js的变量、作用域、闭包、this、原型及原型链
- js的事件循环
- es6+新特性
js数据类型
js数据类型分为基本数据类型和引用数据类型两种,分别是:
1 基本数据类型
string(字符串)
boolean(布尔)
number(数字)
undefined()
null
symbol
2 引用类型
Object
Array
Function
两种数据类型有什么区别?
1 存放位置不同,
基本类型的变量会保存在栈内存中,如果一个函数中声明一个基本类型的变量,这个变量在函数执行结束后会自带销毁
而引用类型的变量名会保存在栈内存中,但是变量值会保存在堆内存中,引用类型的变量不会自动销毁,当没有引用变量引用它时,
系统的垃圾回收机制会回收它。
赋值不同
基本类型的赋值相当于深拷贝,复制后相当于又开辟了一个内存空间
let a=100;
let b = a;
b = 101;
console.log(a,b);//100 101
可以看出a的值改变时不影响b的值
let a = 100;
let b = a;
b = 101
而引用类型的赋值是浅拷贝,当我们对对象进行操作时,
其实操作的只是对象的引用
let obj = {
name:"蓝桥",
};// 创建一个obj对象
let obj2 = obj; //obj赋值给obj2
obj2.name = "蓝桥云课";
console.log(obj.name,obj2.name) // obj2指向的内容变了,obj也变了
可以看出,我们把obj的值赋给obj2,当obj2的值改变时,obj的值也被改变
如何实现一个对象的深拷贝呢?
1 使用JSON.stringfy 和JSON。parse这组API
JSON.stringfy()用于将js值转换为JSON字符串;
JSON.parse() 方法将JSON字符串转化为js对象
JSON.stringfy | JSON.parse() |
---|---|
将js值转换为JSON字符串 | 将JSON字符串转化为js对象 |
– | – |
let obj = {
name:"蓝桥"
};//创建一个obj对象
let obj2 = JSON.parse(JSON.stringify(obj));
obj2.name = "蓝桥云课"
console.log(obj.name,obj2.name) //蓝桥 蓝桥云课
但是这种方式有些问题,当obj里面有函数或者undefind就会失效
let obj = {
fn:function () {
console.log("我是蓝桥")
},
name:undefined
};
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2) //{}
?
所以完全之策还是要用递归,如下所示
function deep(obj) {
let oo = {};
for(const key in obj ) {
if ( typeof obj[key] === " object") {
oo[key] = deep(obj[key]);
}else {
oo[key] = obj[key];
}
}
return oo;
}
上述代码的原理就是递归遍历对象的每个属性,分别复制到一个新对象去
第2遍
function deep(obj) {
let oo = {};
for(count key in obj) {
if(typeof obj[key] ==="object") {
oo[key] = deep(obj[key]);
}else {
oo[key] = obj[key];
}
}
return oo;
}
有经验的面试官会从一道题目上层层递进的提问,来区分不同面试者的水平
原型及原型链
面试中经常问道,你知道什么是原型吗?或者你对原型链是如何理解的,这道面试题挺难的,要想彻底搞清楚原型,及原型链
所有的知识点都来自于一下理论
每个函数都有prototype属性,每一个对象都有_proto_属性,这个属性称之为原型,在我们执行new的时候,对象的_proto_指向这个构造函数的prototype
首先,每个函数都有一个prototype属性,这个属性指向函数的原型对象,同时prototype里面有个constructor属性会指道该函数
function Demo() {
Demo.prototype.constructor === Demo; // true
Demo.prototype结构如下所示
Demo.prototype
{constructor:f}
constructor:f Demo()
arguments: null
length:0
name:"Demo"
prototype:{constructor:f}
_proto_:f()
[[functionLocation]]:
[[Scopes]]:Scopes[1]
_proto_:Object
现在我们使用new操作符来创建一个实例对象,如下所示
当使用new操作符后,Demo就变成了构造函数
function Demo() {
const d = new Demo(); //d就是创建出来的实例对象
d既然是对象,自然就有proto,同时指向构造函数Demo的prototype
function Demo() {
this.name = " 蓝桥";
}
Demo.prototype.say = function () {
console.log("我是",this.name);
};
const d = new Demo();
// 虽然Demo 上没有say方法,但是Demo的prototype上有此方法,所以下面的调用可以正常打印
d.say()
用一张图来描述这个查找过程
d.say()
d中查找say方法
发现d中没有
去d的_proto_中查找
d.proto === Demo.prototype
等价于去Demo.prototype查找
Demo.prototype中找到say方法
调用say方法
这里我们只是体现出来一层查找,实际上__proto是逐层向上查找的,这个查找的过程也就是我们所说的原型链
如果面试中遇到面试官,只要记住上涨途中的查找流程然后描述给面试官即可
继承
js是如何实现继承的?在ES6之前,我们都是通过原型的方式来是按,这也是原型的重要使用场景之一,通常由如下集中实现方法:
1 原型赋值
function Per(name) {
this.name = name || "蓝桥";
}
Per.prototype.say = function () {
console.log(this.name);
};
function Sun() {
Sun.prototype = new Per() ; //直接new了一个父类的实例,然后赋给了子类的原型
Sun.prototype.love = function () {
console.log(this.name);
};
const sun = new Sun();
console.log(sun.name)
这种方法是直接new了一个父类的实例,然后赋给子类的原型。这样也就相当于直接将父类原型中的方法属性以及挂载this上的各种方法属性全赋给了子类的原型,简单粗暴!
因为简单,所以弊端,问题
1 创建子类实例时,无法向父类构造函数传参
2 来自原型对象的所有属性被所有实例共享
2构造函数
直接new 一个父类赋值给子类不合适,那我们在子类的prototype通过改变this指向,调用父类也可以达到继承的目的
function Per(name) {
this.name = name || "蓝桥";
}
Per.prototype.say = function () {
console.log(this.name);
};
function Sun() {
Per.apply(this,arguments); //通过apply,调用Pre,海边Pre的this指向
}
const sun = new Sun();
console.log(sun.name)//蓝桥
这种方式不能改变prototype的this指向,所以不能继承父类的prototype
sun.say() //报错
3 组合继承
既然前两者由自己的优缺点,那组合起来实现继承吗?
在这里插入代码片
省略很多
面试官问:js如何实现继承的
1 原型继承,创建一个父类直接赋值给子类的prototype
缺点:父类的属性会被所示的子类共享,不能给父类传参
2 构造函数继承,在子类中通过改变this的指向来调用父类
缺点:不能继承父类的prototype
3 组合继承,结合原型继承和构造函数继承的有点
缺点:子类继承了两份父类,重复继承
4 寄生组合继承,通过Object.create() 方式来优化组合继承
缺点:实现起来比较复杂
5 ES6 extends实现继承
缺点: 低版本浏览器存在兼容性问题