目录:
- typeof 与instanceof 的区别
- js中的原型链理解
- 浅拷贝和深拷贝
- 用js实现深拷贝
- js执行机制与代码循环
- 简单说说js中的内存模型
- Js中的垃圾回收机制
- 内存泄漏
- JS中常见的错误有哪些
- js中跳转页面的方法
- 如何在JS中动态添加/删除对象的属性
- substr()和substring()函数
- 一道经典的代码题
- this的指向问题
- 函数的节流与防抖
1.typeof 与instanceof 的区别
typeof和instanceof都可以用来判断变量,但它们的用法有很大区别
- typeof只能判断基本数据类型,返回的值就是该变量的基本数据类型(字符串)
- instanceof用来判断引用数据类型,比如判断对象,返回的是个布尔值
例子:
使用typeof判断变量是否存在
console.log(typeof(1));//number
console.log(typeof("abc"));//string
console.log(typeof(true));//boolean
console.log(typeof(m));//undefined
// 判断变量是否存在
if(typeof a != 'undefined'){
//变量存在
}
使用instanceof判断引用类型
function Student(){
console.log('this is student');
}
var stu = new Student();
console.log(stu instanceof Student); // true
console.log(stu instanceof Object); // true
判断是否为数组
let arr = [1,2,3,4,5];
console.log(arr instanceof Array); // true
console.log(typeof arr); // 只能判断类型为 "Object"
2.js中的原型链理解
参考:
原型概述:
任何对象都有一个原型对象,这个原型对象由对象的内置属性**proto**指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的,被创建的对象都可以获得构造函数的prototype属性,注意:对象是没有prototype属性,只有方法才有prototype属性。
任何对象都有一个constructor属性,指向创建此对象的构造函数,比如说{}对象,它的构造函数是function Object(){}
理解原型的要点:
- 只有对象才会有__proto__属性,这个属性是个对象类型
- 只用函数才会有prototype属性,这个属性也是个对象类型
- 对象的__proto__的属性指向它的构造函数的prototype属性
下面根据代码来理解构造函数与原型对象之间的关系
// 1.创建一个构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 2.打印这个构造函数上的prototype属性
// 返回一个对象类型,这个就是原型对象
{
constructor: ƒ Person(name,age)
__proto__: Object
}
根据上面代码看出,函数的prototype属性是一个对象,这个对象就是原型对象,并且该对象内部有constructor属性,这个属性指向的就是其本身的构造函数。
原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将共有的内容,统一设置到原型对象中。
看图一句话理解:构造函数的prototype属性指向它的原型对象,而原型对象的constructor属性又指向构造函数本身
根据构造函数创建一个对象
// 1.使用构造函数创建一个对象
var p = new Person('zs',20);
console.log(p);
// 2.打印这个对象
{
constructor: ƒ Person(name,age)
__proto__: Object
}
上面代码可以看出,实例对象上的proto属性所对应的对象就是原型对象
所以,总结原型的终极就是:构造函数的prototype属性指向它的原型对象,而原型对象的constructor属性又指向构造函数本身。由构造函数创建的实例对象上的proto属性访问的就是原型对象
// 这时候再去理解上面原型概述的话语:这个原型对象由对象的内置属性_proto_指向它的构造函数的prototype指向的对象
// 就很轻松了
console.log(p.__proto__ === Person.prototype); //true
因此,实例对象的proto属性和构造函数的prototype属性是相等的
什么是原型链:
在js中,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在js中通过prototype对象(即原型对象中的proto属性)指向父类对象,直到指向object对象为止。
每个对象都可以有一个原型_proto_,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找… 这个操作被委托在整个原型链上,这个就是我们说的原型链了。
看下面的代码:
// 构造函数Person
function Person(name,age) {
this.name = name;
this.age = age;
}
// 在原型上定义属性
Person.prototype.a = 123;
Person.prototype.sayHello = function () {
console.log('hello');
};
let p = new Person();
console.log(p.a);
p.sayHello();
person实例中没有a这个属性,从person对象中找不到a属性就会从person的原型也就是person.proto,也就是Person.prototype中查找,找到a的值为123,假如person._proto_中也没有该属性,又该如何查找?
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层的Object为止。
Object是JS中所有对象数据类型的基类(最顶层的类),在object.Prototype上没有_proto_这个属性
Console.log(Object.prototype.proto===null)//true
3.浅拷贝和深拷贝
参考:https://juejin.cn/post/6844903745961066503
**浅拷贝概念:**创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
深拷贝概念:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响
所以:浅拷贝与深拷贝的最大区别就是:对于对象类型,浅拷贝只会拷贝引用地址,而深拷贝则会将对象内容也完全拷贝
浅拷贝的代码示例:
// 创建一个对象
let a = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
// 使用Object.assign()方法进行浅拷贝
let b = Object.assign({}, a);
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "55"}
// }
深拷贝代码示例:
JSON.parse(JSON.stringify(object))
let a = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
4.用js实现深拷贝
思路:定义一个函数,然后传入一个obj,获取这个obj上的所有属性,然后遍历,获取对应的value,根据value的不同类型分别进行不同的处理,如果是基本类型直接赋值,如果是数组类型,copy这个数组,如果是对象类型,则需要进一步进行遍历
下面这段深拷贝代码是自己按照上面思路实现的,功能可以实现,但是有一定的代码冗余性,还可以进行优化
// 深拷贝
function deepClone(obj) {
if(obj == null){
return null;
}
let _thisObj = {};
let objKeys = Object.keys(obj);
objKeys.forEach(((value, index) => {
let _tempVariable = obj[value];
// 如果是变量
if(typeof _tempVariable === 'string' || typeof _tempVariable === 'number' ||
typeof _tempVariable === 'boolean' || typeof _tempVariable === 'undefined'){
_thisObj[value] = _tempVariable;
}
// 如果是数组
if(_tempVariable instanceof Array){
let _arr = [];
value.forEach((val) =>{
_arr.push(value);
});
_thisObj[value] = _arr;
}
// 如果是对象
if(_tempVariable instanceof Object){
let resObj = deepClone(_tempVariable);
_thisObj[value] = resObj;
}
}));
return _thisObj;
}
}
进行调用
let Person = {
name: 'xiaoming',
age: 20,
sex: 'nan',
student: {
book:{
yuwen:'语文',
shuxue:'数学',
excrise: {
yuwenScore:100,
shuxueScore:100
}
}
}
};
let deepClonePerson = deepClone(Person);
console.log("deepClonePerson:",deepClonePerson);
下面参考了其他网友的代码,这个版本代码比较简化,但是思路大致一样
// 深拷贝
function deepClone(o) {
// 判断如果不是引用类型,直接返回数据即可
if (typeof o === 'string' || typeof o === 'number' || typeof o === 'boolean' || typeof o === 'undefined') {
return o
} else if (Array.isArray(o)) { // 如果是数组,则定义一个新数组,完成复制后返回
// 注意,这里判断数组不能用typeof,因为typeof Array 返回的是object
console.log(typeof []); // --> object
var _arr = [];
o.forEach(item => { _arr.push(item) });
return _arr
} else if (typeof o === 'object') {
var _o = {};
for (let key in o) {
_o[key] = deepClone(o[key])
}
return _o
}
}
5.js执行机制与代码循环
- 直接看大佬阮一峰的博客吧:http://www.ruanyifeng.com/blog/2014/10/event-loop.html
6.简单说说js中的内存模型
参考:https://blog.csdn.net/qq_42349946/article/details/108370789
JS 中的数据类型,整体上来说只有两类:基本类型和引用类型。
类型 | 说明 |
---|---|
基本类型 | Sting、Number、Boolean、null、undefined、Symbol。它们被放在 JS 的栈内存里存储 |
引用类型 | Object、Array、Function。也叫做复杂数组类型。它们被放在 JS 的堆内存里存储 |
栈是线性表的一种,而堆则是树形结构
上图即是js的内存模型
7.简单说说Js中的垃圾回收机制
每隔一段时间,JS 的垃圾收集器就会对变量做 “巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占用的内存空间给释放掉,这个过程叫做垃圾回收
js中的垃圾回收算法有两种,非常类似java当中的jvm虚拟机回收算法
**引用计数法:**在引用计数法的机制下,内存中的每一个值都会对应一个引用计数(当变量被引用一次时,这个值就会加一)。当垃圾收集器感知到某个值的引用计
数为 0 时,就判断它 “没用” 了,随即这块内存就会被释放。
**标记清除法:**标记清除法是现代浏览器的标准垃圾回收算法。在标记清除算法中,一个变量是否被需要的判断标准,是它是否可抵达,也就是可达性分析这个算法有两个阶段,分别是标记阶段和清除阶段: 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里,根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除。
8.内存泄漏
参考:https://blog.csdn.net/qq_40028324/article/details/92970588
**什么是内存泄漏:**不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
内存泄漏的案例:
-
意外的全局变量
function foo() { bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1 this.bar2 = 'some text' // 全局变量 => window.bar2 } foo(); // 在这个例子中,意外的创建了两个全局变量 bar1 和 bar2
-
被遗忘的定时器和回调函数
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 每 5 秒调用一次
如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。
-
闭包
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 对于 'originalThing'的引用 console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。当这段代码被反复执行时,内存会持续增长。
-
DOM操作
很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 对象 中
var elements = { image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { document.body.removeChild(document.getElementById('image')); // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收. }
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收
9.JS中常见的错误有哪些
错误 | 说明 |
---|---|
Uncaught SytanxError | 语法错误,很显然要么多输入字符,要么就是少括号之类的 |
Uncaught ReferenceError | 引用类型错误,引用不存在的属性就会报错 |
Uncaught TypeError | 提供的类型错误,提供的类型不是Js代码中所需要的。比如常见的调用一个不存在的函数就会报: xx is not a function |
RangeError | 范围错误,比如创建数组给个负值 |
URIError | 调用URL函数时传参不正确导致的错误 |
10.js中跳转页面的方法有哪些,他们的区别是什么
有三种方法:location.href,location.replace,location.reload
- location.href:用法
location.href="http://www.baidu.com"
,会写入 浏览器的历史 window.history 对象中 ,这就导致可以通过浏览器上的后退按钮来返回原页面 - location.replace:用法
location.replace("http://www.baidu.com")
,不会写入浏览器历史的history对象中,所以没法点击后退按钮 - location.reload():就相当于浏览器上的刷新按钮,可传入俩参数,为false时表示从本地缓存加载页面,为true时表示重新从服务器获取页面
11.如何在JS中动态添加/删除对象的属性
- 使用object.property_name = value向对象添加属性
- delete object.property_name 用于删除属性
12.JS中的substr()和substring()函数有什么区别
let a = 'helloworld';
console.log(a.substr(1, 4)); // ello
console.log(a.substring(1,4));// ell
substr:从下标位置开始,截取4个元素,左开右开[1,4]
substring:从下标位置开始,截取4-1个元素,左开右闭[1,4)
13.一道经典的代码题:函数提升,运算符优先级,变量污染等
参考: https://www.cnblogs.com/haojf/p/13037941.html
function Foo() {
getName = function () { alert (1); };
return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
这道题的经典之处在于它综合考察了面试者的JavaScript的综合能力,包含了变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识
运行结果是:2 4 1 1 2 3 3
我们先来分析以下这段代码的做了什么事
// 1.定义了一个Foo构造函数
function Foo() {
getName = function () { alert (1); };
return this;
}
// 2.给Foo对象添加了一个静态方法getName
Foo.getName = function () { alert (2);};
// 3.为Foo的原型对象新创建了一个叫getName的匿名函数
Foo.prototype.getName = function () { alert (3);};
// 4.通过函数变量表达式创建了一个getName的函数
var getName = function () { alert (4);};
// 5.声明了一个普通函数getName
function getName() { alert (5);}
再来看解析
// 1.这里调用的Foo对象上的静态方法,无需实例化对象即可调用,所以输出的是2
Foo.getName(); // 2
// 2.这里看似是调用了函数getName输出的是5,实际上这是一个坑,没错,声明式函数getName会被提升到前面,但是后面又用函数表达式声明了一个同样名为
// getName的函数,这时候后面的这个同名函数会覆盖掉前面的函数,所以输出4
getName(); // 4
// 3. 这里先执行函数Foo(),然后给全局变量getName赋值,所以var getName = function(){ alert (1); }; 然后Foo函数返回的是一个window对象
// 相当于window.getName();所以输出1
Foo().getName();// 1
// 4.由于上面的Foo().getName()把函数修改了,所以这里输出的是1,因为直接调用getName函数,相当于window.getName()
getName();// 1
// 5.这里涉及到运算符优先级的问题,new(带参数列表)> 函数调用 > new(无参数列表)
// 所以这段代码相当于new (Foo.getName()); 因此返回2
new Foo.getName(); // 2
// 6.这里依然根据运算符优先级,所以这段代码是这样的 (new Foo()).getName();
// 由于new出来的对象是个空对象,自然没有getName()这个函数,所以回去原型上去找,所以输出3
new Foo().getName();
// 7.根据5点和第6点说明分析,这里等价于new (new Foo().getName()),所以输出3
new new Foo().getName();
关于第二点的详细解析:
var getName = function () { alert (4);};
function getName() { alert (5);}
这里的代码在被Js解析后是这样的
var getName; // 此时这里为undefined
function getName() { alert (5);}
getName = function () { alert (4);}; // getName = function ()实际上相当于 function getName();
所以js在执行到最后一段代码时,发现getName后面值是函数表达式,所以会覆盖掉上面的getName()函数声明,所以这里输出的是4
关于第三点的详细解析:
下面这段代码由于变量的提升,var getName会提升到作用域最前面
var getName = function () { alert (4);};
所以解析后的代码是这样的
var getName;
function Foo() {
getName = function () { alert (1); };
return this;
}
getName = function () { alert (4);};
Foo().getName();
先执行函数Foo,这个函数内部把全局变量getName的值给修改成了getName = function () { alert (1); };
,而这个函数返回的this对象是window对象,所以。Foo().getName()相当于window.getName();所以输出的值就是1了
关于最后三点的分析:
实际上最后三点比较难,考查的是运算符优先级的问题,可以去搜以下MDN上关于运算符优先级表,上面列出了运算符优先级。
无参数列表的优先级为18,而成员访问的优先级为19,高于无参数列表。因此new Foo.getName()先执行Foo.getName()
带参数列表的优先级为19,而成员访问的优先级也为19,按照运算符规则(同一优先级,按照从左向右的执行顺序),new Foo().getName()先执行new Foo(),再对new之后的实例进行成员访问.getName()操作。
所以总结出了new(带参数列表)> 函数调用 > new(无参数列表)
另外new Foo()创造出来的对象是个空对象,自然会去原型上找getName方法,根据Object._proto_ = constructor.Prototype。所以能找到原型上的方法
14.this的指向问题
参考:https://www.cnblogs.com/pssp/p/5216085.html
首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉)
上面这段话说的非常有道理,不能单纯的认为this指向是它的调用者,虽然大多数情况下是。还是要根据它所在的上下文去理解
下面根据代码的环境上下文可以做一个总结:
-
普通函数的this指向window,注意是单独定义的函数,不是定义在对象中的
function func() { console.log(this); // window } let fn = function () { console.log(this); // window }
-
定义在对象中的函数,很明显,对象调用,this指向这个对象
var o = { user:"追梦子", fn:function(){ console.log(this.user); //追梦子 } } o.fn();
这里的this指向的是对象o,因为你调用这个fn是通过o.fn()执行的,那自然指向就是对象o,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个。
再看一个例子
var o = { a:10, b:{ a:12, fn:function(){ console.log(this); // b } } } o.b.fn();
尽管是对象嵌套调用,但是this指向的仍然是它的调用者
因此:this的指向问题需要分情况讨论,这下面的三种情况说的是普通函数
情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window, 但是我们这里不探讨严格版的问题,你想了解可以自行上网查找。
情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
下面再看一个特殊的例子:
let obj={
a:222,
fn:function(){
setTimeout(function(){console.log(this)}) // window
}
};
obj.fn();//undefined
let obj={
a:222,
fn:function(){
setTimeout(()=>{console.log(this)});// obj
}
};
obj.fn();//222
分析:第一段代码传入的是个普通函数,这个setTimeout里的参数函数其实是由setTimeout本身调用的,setTimeout是window对象下的,所以这里的this指向window。第二段代码由于是箭头函数,箭头函数没有this对象,它的this从上一级继承,也就是obj对象
下面再看一个怪异例子
var x=11;
var obj={
x:22,
say:()=>{
console.log(this.x);
console.log(this); // window
}
}
obj.say();
//输出的值为11
看完这段代码,我靠,你会发现上面说的情况又不符合了。这里的this不是继承它的上一级obj。注意啊,这里箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,也就是说,say箭头函数的上一级就是obj的上一级,就是windows对象!因此这里的this.x实际上表示的是window.x,因此输出的是11。所以这里涉及到的关键点在于箭头函数执行的上下文。
类似的还有:
var a=11;
function test1(){
this.a=22;
let b=function(){
console.log(this);// window
};
b();
}
test1();
function test2(){
this.a=22;
let b=()=>{console.log(this)};// window
b();
}
test2();
所以,关于this的总结就是:
-
单独定义的函数(不在对象,其他函数中定义的)一般this指向window对象
例如
function func() { console.log(this); // window } let fn = function () { console.log(this); // window } let b = ()=>{ console.log(this) };
-
定义在对象中的普通函数,一般this指向调用它的对象
-
箭头函数根据不同的情况,不仅跟它的调用者有关,也跟它执行的上下文有关。箭头函数的this指向箭头函数定义时所处的对象,而不是箭头函数使用时所在的对象,默认使用父级的this
15.函数的节流与防抖
参考:https://blog.csdn.net/zuorishu/article/details/93630578
函数防抖(debounce):触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。
函数节流(throttle):高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。
函数节流(throttle)与函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。