前端常见面试题总结——JavaScript部分(二)
1.如何对一个数组进行去重/排序?
去重:
除了常用的双重循环,还有两种方法
方法一:遍历该数组,利用indexOf()方法判断新数组中是否存在,不存在就push到新驻足中,代码如下:
var arr = ['a', 'b', 'b', 'c', 'c', 'd'];
var newArr = [];
for (var i = 0; i < arr.length; i++) {
if (newArr.indexOf(arr[i]) == -1) {
newArr.push(arr[i]);
}
}
方法二:通过es6中的set数据结构,对数组去重
var setNum = new Set(arr);
Array.from(setNum);
//或者用rest参数
[...setNum]
排序:
方法一:利用数组的sort()方法排序,如果调用该方法时没有使用参数,是按照字符编码的顺序进行排序,并不是数字大小排序。
var arr = [5, 1, 3, 6, 8, 12];
arr.sort(); //[1, 12, 3, 5, 6, 8]
arr.sort(function (a, b) {
return a - b;
});
console.log(arr); //[1, 3, 5, 6, 8, 12]
方法二:双重循环,冒泡排序
var arr2 = [5, 1, 3, 6, 8, 2, 14, 17];
for(var i = 0; i < arr2.length - 1; i++) {
for(var j = 0; j < arr2.length - 1 - i; j++) {
if (arr2[j] > arr2[j + 1]) {
var item = arr2[j];
arr2[j] = arr2[j + 1];
arr2[j + 1] = item;
}
}
}
console.log(arr2); //[1, 2, 3, 5, 6, 8, 14, 17]
还有数组排序的其他几种算法,可以参考:https://www.cnblogs.com/real-me/p/7103375.html
2.什么是简单类型(原始类型primitive type),什么是复杂类型(合成类型complex type或引用类型)。
原始类型就是直接存储在栈中的数据,是最基本的数据类型,不可以再分了。
- 数值(number):整数和小数(比如
1
和3.14
) - 字符串(string):文本(比如
Hello World
)。 - 布尔值(boolean):表示真伪的两个特殊值,即
true
(真)和false
(假) -
undefined
:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值 -
null
:表示空值,即此处的值为空。 - Symbol:表示独一无二的,是es6中新增的类型
复杂类型就是指存储在堆中的数据。常用复杂类型如下:
- 数组(Array):存储一系列的值
- 日期(Date):用于处理日期和时间
- 算数(Math):执行普通的算数任务
- 正则表达式(RegExp):描述了字符的模式对象
3.深拷贝与浅拷贝的区别,如何进行深拷贝?
深拷贝和浅拷贝都是针对复杂类型的,简单类型没有深浅拷贝之分。
对于仅仅是复制了引用(地址),换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝。
而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝。
深浅拷贝 的主要区别就是:复制的是引用(地址)还是复制的是实例。
那么如何进行深拷贝呢?
方法一:利用jQuery中的extend()方法实现深拷贝
$.extend( [deep ], target, object1 [, objectN ] )
deep:表示是否深度合并对象,为true是就是神拷贝
target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1 objectN可选。 Object类型 第一个以及第N个被合并的对象。
var obj = {name:'ming',age:19,company : { name : 'RNG', address : '北京'} };
var obj_extend = $.extend(true,{}, obj);
console.log(obj === obj_extend); //false
extend()方法可以参考菜鸟教程:http://www.runoob.com/jquery/misc-extend.html
方法二:利用JSON 对象的 parse 和 stringify方法
var obj = {name:'xixi',age:20,company : { name : '腾讯', address : '深圳'} };
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json); //false
方法三:es6中的rest参数
var aa = [...array]; //es6
方法四:利用 递归 来实现深复制,对属性中所有引用类型的值,遍历到是基本类型的值为止。
function deepClone(source){
if(!source && typeof source !== 'object'){
throw new Error('error arguments', 'shallowClone');
}
var targetObj = Array.isArray(source) ? [] : {};
for(var keys in source){
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){
targetObj[keys] = deepClone(source[keys]); //递归
}else{
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
注意:Array对象的slice()和concat()方法不是真正的深拷贝。
参考自知乎:https://zhuanlan.zhihu.com/p/26282765 (这里详细写了深浅拷贝的原理,此处不再赘述)
4.说几个常用的数组/字符串的原生方法
数组:
- splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。
- reverse() 方法用于颠倒数组中元素的顺序。该方法会改变原有数组。
- slice()方法 返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。
- join() 方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。
字符串:
- indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。没有则返回-1。
- split() 方法用于把一个字符串分割成字符串数组。
- slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
- substring() 方法用于提取字符串中介于两个指定下标之间的字符。
5. 面向对象的继承有几种方式?
方式一:原型链继承
修改子类的prototype为父类,这样会使子类的原型对象的constructor变成了父类,也可以手动修改回来
子类.prototype=new 父类();
子类.prototype.constructor=子类;
特点:
(1)非常纯粹的继承关系,实例是子类的实例,也是父类的实例
(2)继承父类本身的属性/方法、父类原型属性/方法、父类新增原型方法/原型属性
(3)无法实现多继承
(4)创建子类实例时,无法向父类构造函数传参
(5)子类的一个实例属性值改变时,会影响所有子类的实例属性。因为所有子类的prototype指向父类(new 父类),所有没有设置自己的属性的子类的实例属性都会改变。
但是如果是子类的一个实例属性重新赋值(子类的实例设置了自己的属性),则不会影响其他实例的属性。
function A(){
this.aNum=1;
}
A.prototype.getaNum= function () {
console.log(this.aNum);
}
B.prototype=new A();
var b=new B();
console.log(b.aNum); //父类本身的属性和方法
b.getaNum(); //原型链上的属性和方法
方式二:构造函数继承(对象冒充)
子类调用父类的构造函数,并且把this传进去,
子类函数中:父类.call(this);
子类函数中:父类.apply(this);
特点:
(1)只继承父类构造函数中的属性和方法,不能继承原型属性/方法。
(2)可以实现多重继承(call/apply多个父类对象)
(3)实例只是子类的实例,不是父类的实例。
(4)创建子类实例时,可以向父类传递参数
(5)无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
function E(num){
this.eNum = num;
this.geteNum = function () {
alert(this.eNum);
}
}
function F(num){
E.call(this, num);
}
var f=new F(5);
f.geteNum();
方式三:组合式继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
特点:
(1)既可以继承父类本身的属性/方法,也可以继承原型属性/方法
(2)既是子类的实例,也是父类的实例
(3)不存在子类的一个实例属性值改变时,会影响所有子类的实例属性的问题。
每创建一个子类对象实例,就调用父类的构造函数并且将实例对象作为this值传过去,所以每个实例对象都有自己的属性值。
(4)可传参
(5)函数可复用
(6)调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
function G(){
this.gNum = 6;
}
G.prototype.getgNum = function () {
console.log(this.gNum);
}
function H(){
G.call(this);
}
H.prototype = new G();
var h = new H();
console.log(h.gNum);
h.getgNum();
6.面向对象中的this代表什么,可否举例说明?
this是一个指针,this总是指向调用方法的对象,作为方法调用,那么this就是指实例化的对象。
举例说明:jQuery中的链式调用,就是this对象的应用。
7.说一下事件委派(事件委托)的原理
通事件委派的原理是事件冒泡机制,通过给父标签绑定事件,然后利用事件冒泡的现象使得点击子元素的时候,可以触发事件达到给子元素绑定事件的效果。
Event对象提供了一个属性叫target,表示为当前的事件操作的dom,这个属性是有兼容性的,标准浏览器用event.target,IE浏览器用event.srcElement。
优点:1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适。
8.window的ready与onload事件的区别,执行的先后顺序是什么?
ready,表示文档结构(DOM结构)已经加载完成(不包含图片等非文字媒体文件),
onload,指示页面包含图片等文件在内的所有元素都加载完成。
可以说:ready 在onload 前加载!!!
9.如何判断一个数据是不是数组?
通过typeof检测数组,得到的结构是object,并不能区分是不是数组,我们可以通过以下几种方法来判断一个数据是不是数组。
方法一:instanceof 用来判断一个对象是否存在于另一个对象的原型链上。
var str=["aa", "bb", "cc"];
console.log(str instanceof Array); //true
方法二:isArray 函数
var arr=["aa", "bb", "cc"];
console.log(Array.isArray(arr)); //true
方法三:constructor 属性返回对创建此对象的函数的引用,使用此属性可以检测数组类型。
var arr=["aa", "bb", "cc"];
if(arr.constructor===Array){
console.log('array');
}
//array