第二节:Object和Array相关面试题剖析(原型链、各种算法、手动实现等)
一. Object类型相关
1. 对于引用类型,new操作符的作用是什么?
new操作符做了以下三件事:
var person={};
person.__proto__=Person.prototype;
Person.call(person)
剖析: 构造函数实际上等价于下面代码
new Person("zhangsan", 20);
//构造函数代码
function Person(userName, age) {
console.log(this);//输出的是Person{ }对象
this.userName = userName;
this.age = age;
}
//等价代码
function Person(userName, age) {
var person = {};
person.userName = userName;
person.age = age;
return person;
}
2. 如何理解prototype、constructor、__proto__ 三个属性?
先上代码:
{
// 函数
function Person() {}
// 实例
let p = new Person();
// prototype和__proto__的关系
console.log(p.__proto__ === Person.prototype); //true
// prototype和constructor的关系
console.log(Person.prototype.constructor === Person); //true
// 推导结论
console.log(p.__proto__.constructor === Person); //true
}
prototype:
(1). 我们创建的每一个Function(构造)函数都有一个prototype属性,这个属性是一个指针,它指向一个对象 (即:Person.prototype指向一个对象,这个对象名为 原型对象)。
(2). 这个对象的用途是包含所有实例共享的属性和方法,即:在这个对象上定义的属性和方法可以供它的所有实例调用。
(3). 该函数实例化的所有对象的 __proto__ 属性,都指向这个对象(即 p.__proto__ == Person.prototype),也就是说它是该函数所有实例化对象的原型。
__proto__:
(1). 首先它是实例化后的对象的属性,即每个Function函数实例化后的对象,都有一个__proto__属性。
(2). 它指向Function构造函数的原型对象 (即 p.__proto__===Person.prototype)
constructor:
(1). 我们创建的每一个Function函数都有一个prototype属性,它指向一个对象,默认情况下,该对象会有一个constructor属性
(2). 该属性是一个指针,它指向prototype属性所在的函数(即:Person.prototype.constructor === Person)
PS: 由上述我们可以推导出来 p.__proto__.constructor === Person (根据 p.__proto__===Person.prototype Person.prototype.constructor === Person 推导)
关系图(非常重要):
3. 如何判断一个属性是否在对象实例上?(不含原型上)
使用hasOwnProtoType方法,实例属性返回true,原型属性返回false,但有一种特殊情况,实例和原型上都有这种属性,那么也是返回true的。
{
console.log("2. hasOwnProperty用于判断该属性是否是实例属性");
function Person() {
// 实例属性
this.name = "ypf";
}
// 原型属性
Person.prototype.age = 18;
let p = new Person();
console.log(p.hasOwnProperty("name")); //true
console.log(p.hasOwnProperty("age")); //false
// 但是会遇到一种情况
p.age = 19; //屏蔽了原型属性
console.log(p.hasOwnProperty("age")); //true
}
4. 获取对象属性的方式有哪些,他们有啥区别?
(1). Object.keys 只能获取实例属性,不能获取原型上继承的属性和不可枚举的属性
(2). Object.getOwnPropertyNames 可以获取实例属性和不可枚举的属性,不能获取到原型上继承的属性
(3). for in 可以获取实例属性和原型上继承的属性,不能获取不可枚举属性
{
console.log(
"3. Object.keys.getOwnPropertyNames、for-in 获取对象属性时的区别"
);
//实例属性
function Person() {
this.name1 = "ypf1";
}
// 原型属性
Person.prototype = {
name2: "ypf2",
};
let p = new Person();
// 不可枚举属性
Object.defineProperty(p, "name3", {
value: "ypf3",
enumerable: false,
});
//3.1 Object.keys 只能获取实例属性,不能获取原型上继承的属性和不可枚举的属性
console.log(Object.keys(p)); //[ 'name1' ]
//3.2 Object.getOwnPropertyNames 可以获取实例属性和不可枚举的属性,不能获取到原型上继承的属性
console.log(Object.getOwnPropertyNames(p)); //[ 'name1', 'name3' ]
//3.3 for in 可以获取实例属性和原型上继承的属性,不能获取不可枚举属性
let pArray = [];
for (const key in p) {
pArray.push(key);
}
console.log(pArray); //[ 'name1', 'name2' ]
}
5. 如何判断某个属性是实例属性且是可以枚举的?
使用propertyIsEnumerable方法,只有同时满足上述两个条件,才会返回ture,否则返回false。
{
console.log("5. 如何判断某个属性是实例属性且是可以枚举的?");
function Student(userName) {
this.userName = userName;
}
Student.prototype.sayHello = function () {
console.log("hello" + this.userName);
};
var stu = new Student();
console.log(stu.propertyIsEnumerable("userName")); //true:userName为自身定义的实例属性
console.log(stu.propertyIsEnumerable("age")); // false:age属性不存在,返回false
console.log(stu.propertyIsEnumerable("sayHello")); // false :sayHello属于原型上的函数
//将userName属性设置为不可枚举
Object.defineProperty(stu, "userName", {
enumerable: false,
});
console.log(stu.propertyIsEnumerable("userName")); // false: userName设置了不可枚举
}
6. 谈谈对原型链的理解?【重点】
(1). 先上代码
{
console.log("6. 原型链的理解");
function A() {}
let a = new A();
console.log("---------------第一次推导-------------");
console.log(a.__proto__ === A.prototype); //true
console.log("---------------第二次推导-------------");
console.log(A.prototype.__proto__ === Object.prototype); //true 【最关键的一步 原型对象是通过Object构造函数生成的!!】
console.log(a.__proto__.__proto__ === Object.prototype); //true
console.log("---------------第三次推导-------------");
console.log(Object.prototype.__proto__ === null); //true
console.log(a.__proto__.__proto__.__proto__ === Object.prototype.__proto__); //true
console.log(a.__proto__.__proto__.__proto__ === null); //true
}
(2). 分期推导过程
第一次推导:根据前面所学的知识,我们知道 实例的__proto__属性 指向 构造函数的原型对象,即 a.__proto__ === A.prototype
第二次推导:我们需要知道一个前提,原型对象都是通过Object构造函数生成的, 所以 原型对象的__proto__属性指向 Object构造函数的原型, 即 A.prototype.__proto__ === Object.prototype
所以我们就能推导出来 a.__proto__.__proto__ === Object.prototype
第三次推导: 我们需要知道一个前提,Object.prototype.__proto__ 的值为 null ( Object.prototype 没有原型 ),即 Object.prototype.__proto__ === null
由第二次推导可以得出:a.__proto__.__proto__.__proto__ === Object.prototype.__proto__ ,再结合 Object.prototype.__proto__ === null
得出最终结论:
a.__proto__.__proto__.__proto__ === null,即停止了原型链上的查找
(3). 图解
再分享一个案例,帮助理解:
查看代码
{
console.log("6. 原型链的理解---案例2");
function Super() {}
function Middle() {}
function Sub() {}
Middle.prototype = new Super();
Sub.prototype = new Middle();
var suber = new Sub();
console.log("---------------第一次推导-------------");
console.log(suber.__proto__ === Sub.prototype);
console.log("---------------第二次推导-------------");
console.log(Sub.prototype.__proto__ === Middle.prototype);
console.log(suber.__proto__.__proto__ === Middle.prototype);
console.log("---------------第三次推导-------------");
console.log(Middle.prototype.__proto__ === Super.prototype);
console.log(suber.__proto__.__proto__.__proto__ === Super.prototype);
console.log("---------------第四次推导-------------");
console.log(Super.prototype.__proto__ === Object.prototype);
console.log(
suber.__proto__.__proto__.__proto__.__proto__ === Object.prototype
);
console.log("---------------第五次推导-------------");
console.log(Object.prototype.__proto__ === null);
console.log(suber.__proto__.__proto__.__proto__.__proto__.__proto__ === null);
}
图例:
7. 原型链的特点?
(1). 在查找某个属性的时候,js引擎会先查找对象本身是否有该属性,如果没有,则沿着原型链继续往上查找,直到找到Object.prototype,如果还没找到,则返回undefined,如果期间找到,则直接返回结果。
(2). 由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大
function Person(){ }
var p = new Person();
p.toString(); // 实际上调用的是Object.prototype.toString( )
二. Array类型相关
1. 如何判断一个变量是数组还是对象?
ps:typeof 不能使用,因为数组和对象都返回object
{
// typeof不能不适用, 因为对象和数组返回的都是object
let array1 = ["1"];
let obj1 = { name: "ypf" };
console.log(typeof array1); //object
console.log(typeof obj1); //object
}
方案1:使用instanceof方法,通过查找原型链,判断某变量是否是某数据类型的实例。
注意:Array类型也可以理解为继承了Object类型,所以Array的实例既在Array上,也在Object上,所以这种模式判断,需要先判断某个变量是否是数组。
{
console.log("1. 判断一个变量是数组还是对象 方案1 ");
let array1 = ["1"];
let obj1 = { name: "ypf" };
function GetType(item) {
if (item instanceof Array) {
return "Array";
} else if (item instanceof Object) {
return "Object";
} else {
return "不是Array,也不是Object";
}
}
console.log(GetType(array1)); //Array
console.log(GetType(obj1)); //Object
}
方案2:构造函数
{
console.log("1. 判断一个变量是数组还是对象 方案2 ");
let array1 = ["1"];
let obj1 = { name: "ypf" };
// 根据 p.__proto__===Person.prototype Person.prototype.constructor === Person 推导 p.__proto__.constructor === Person 同理
console.log(array1.__proto__.constructor === Array); //true
console.log(array1.__proto__.constructor === Object); //false
console.log(obj1.__proto__.constructor === Array); //false
console.log(obj1.__proto__.constructor === Object); //true
}
方案3:toString函数,Object
类型,因此它们都包含toString( )
{
console.log("1. 判断一个变量是数组还是对象 方案3 ");
let array1 = ["1"];
let obj1 = { name: "ypf" };
console.log(Object.prototype.toString.call(array1) === "[object Array]"); //true
console.log(Object.prototype.toString.call(obj1) === "[object Object]"); //true
}
方案4: Array.isArray() 【推荐,最简洁】
{
console.log("1. 判断一个变量是数组还是对象 方案4 ");
let array1 = ["1"];
let obj1 = { name: "ypf" };
console.log(Array.isArray(array1)); //true
console.log(Array.isArray(obj1)); //false
}
2. 如何对数组进行累加操作?
这里在用reduce方法,参数的含义: preValue表示return返回上一次的值,currentValue表示遍历的当前值; 第二个参数:表示默认值,即preValue的初始值,也可以省略
{
console.log(" 2. 如何对数组进行累加操作");
let array1 = [1, 2, 3, 4, 5];
/**
* preValue:代表上次返回的值, currentValue:代表当前值
* 第二个参数代表第一次的默认值
*/
let sum = array1.reduce((preValue, currentValue) => {
return preValue + currentValue;
}, 0);
console.log(sum); //15
}
3. 如何求数组中的最大值和最小值?
方案1:利用Math.max /min方法 + 展开运算符
{
console.log("3. 如何求数组的最大值和最小值 方案1");
let array1 = [2, 56, -7, 100, 5, 34];
console.log(Math.max(1, 2, 3, 4, 5)); //5
console.log(Math.max(...array1)); //100
console.log(Math.min(...array1)); //-7
}
方案2:原型扩展+ reduce
{
console.log("3. 如何求数组的最大值和最小值 方案2");
Array.prototype.max = function () {
return this.reduce((preValue, currentValue) => {
return preValue > currentValue ? preValue : currentValue;
});
};
Array.prototype.min = function () {
return this.reduce((preValue, currentValue) => {
return preValue < currentValue ? preValue : currentValue;
});
};
let array1 = [2, 56, -7, 100, 5, 34];
console.log(array1.max()); //100
console.log(array1.min()); //-7
}
方案3:原型扩展+遍历循环业务比较
{
console.log("3. 如何求数组的最大值和最小值 方案3");
Array.prototype.max = function () {
let maxValue = this[0];
let len = this.length;
for (let i = 1; i < len; i++) {
if (this[i] > maxValue) {
maxValue = this[i];
}
}
return maxValue;
};
Array.prototype.min = function () {
let minValue = this[0];
let len = this.length;
for (let i = 1; i < len; i++) {
if (this[i] < minValue) {
minValue = this[i];
}
}
return minValue;
};
let array1 = [2, 56, -7, 100, 5, 34];
console.log(array1.max()); //100
console.log(array1.min()); //-7
}
4. 如何实现数组的去重?
方案1: Set结构天然去重 (注意Set转换数组的两种写法)
{
console.log("4. 实现数组去重 方案1 ");
function QcArray(array) {
return Array.from(new Set(array));
}
function QcArray2(array) {
return [...new Set(array)];
}
let array1 = [1, 3, 3, 4, 2, 2, 3, 5];
console.log(QcArray(array1)); //[ 1, 3, 4, 2, 5 ]
console.log(QcArray2(array1)); //[ 1, 3, 4, 2, 5 ]
}
方案2:遍历数组+includes方法
{
console.log("4. 实现数组去重 方案2 ");
function QcArray(array) {
let newArray = [];
array.forEach(el => {
if (!newArray.includes(el)) {
newArray.push(el);
}
});
return newArray;
}
let array1 = [1, 3, 3, 4, 2, 2, 3, 5];
console.log(QcArray(array1)); //[ 1, 3, 4, 2, 5 ]
}
方案3:利用Object键值对去重【掌握原理即可, 不推荐,方案1 方案2 更常用】
原理:遍历数组将数组的值作为obj的key,后面通过判断obj中key是否存在来判断,但这种写法有个问题, 5 和 "5" 对于obj的key而言,是一样的,所以需要继续改造,需要判断数组中内容的类型。
注:该方案仍然存在bug,比如 数组中有两个字符串 "5", 仍然无法去重。
查看代码
// 方案3--利用键值对去重
{
console.log("4. 实现数组去重 方案3 ");
function QcArray(array) {
let obj = {};
array.forEach(el => {
if (!obj[el]) {
obj[el] = el;
}
});
// 获取obj中所有的value组成数组
return Object.values(obj);
}
let array1 = [1, 3, 3, 4, 2, 2, 3, 5, "5"];
console.log(QcArray(array1)); //[ 1, 2, 3, 4, 5 ] 无法识别 "5", 他认为5和"5"是一样的
// 改造,用于区分“5”和5
function QcArray2(array) {
let obj = {};
let newArray = [];
let myType;
array.forEach(el => {
myType = typeof el;
if (!obj[el]) {
obj[el] = myType;
newArray.push(el);
} else if (obj[el] != myType) {
newArray.push(el);
}
});
return newArray;
}
let array2 = [1, 3, 3, 4, 2, 2, 3, 5, "5"];
let array3 = [1, 3, 3, 4, 2, 2, 3, 5, "5", "5"];
console.log(QcArray2(array2)); //[ 1, 3, 4, 2, 5, '5' ], 可以区分出来5和"5"
console.log(QcArray2(array3)); //[ 1, 3, 4, 2, 5, '5' ,'5'], 仍然存在问题,输出两个 '5'
}
5. 如何获取数组中的最多元素及其个数?
原理:利用obj的键值对,将数组中的元素作为key,出现的次数作为value,通过遍历循环,完成obj键值对存储的同时,进行最多元素个数的比较。
查看代码
{
console.log("5. 如何获取数组中的最多元素及其个数");
function GetMaxAndNum(array) {
let obj = {}; // 空键值对,key表示数组的值(自动去重),value表示个数
let maxValue; //最多元素的值
let maxCount = 0; //最多元素的个数
// 遍历数组
array.forEach(val => {
//累加个数
obj[val] === undefined ? (obj[val] = 1) : obj[val]++;
// 处理最多元素问题
if (obj[val] > maxCount) {
maxValue = val;
maxCount = obj[val];
}
});
// 返回结果
return `出现次数最多的元素为${maxValue},次数为:${maxCount}`;
}
let array = [1, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 6];
console.log(GetMaxAndNum(array));
}
6. 数组遍历的方式有哪些?
for、forEach、map、some、find、for-in、for-of, 注意:every不能用于单纯遍历,只返回第一个原型。
其中:map、some、every、find各自的用途,详见: 第三节:数组Array高频方法详解(filter/forEach/map/find/findIndex/forEach/includes/some等等)
查看代码
{
console.log("6. 数组遍历的方式");
let array1 = [1, 2, 3];
// 6.1 for
console.log("6.1 for");
for (let i = 0; i < array1.length; i++) {
const element = array1[i];
console.log(element);
}
// 6.2 forEach
console.log("6.2 forEach");
array1.forEach((val, index, array) => {
console.log(val);
});
// 6.3 map
console.log("6.3 map");
array1.map((val, index, array) => {
console.log(val);
});
// 6.4 some
console.log("6.4 some");
array1.some((val, index, array) => {
console.log(val);
});
// 6.5 every 【不能用于常规遍历,仅返回1】
console.log("6.5 every");
array1.every((val, index, array) => {
console.log(val);
});
// 6.6 find
console.log("6.6 find");
array1.find((val, index, array) => {
console.log(val);
});
}
7. 手写实现find、filter、some、every、map、reduce方法
手写find方法
{
console.log("7.1 手写实现find方法");
Array.prototype.find2 = function (fn) {
// 这里建议使用for循环,而不是foreach,因为终止麻烦
for (let i = 0; i < this.length; i++) {
const ele = this[i];
let flag = fn(ele); //将当前元素传递给函数
if (flag) {
//true,表示符合条件,直接返回即可
return ele;
}
}
};
let array1 = [1, 2, 3, 4, 5];
let result = array1.find2(item => item > 3);
console.log(result); //4
}
手写filter方法
{
console.log("7.2 手写filter方法");
Array.prototype.filter2 = function (fn) {
let newArray = [];
for (let i = 0; i < this.length; i++) {
const ele = this[i];
let flag = fn(ele); //将当前元素传递给函数
if (flag) {
//true,表示符合条件,存到新数组里
newArray.push(ele);
}
}
return newArray;
};
let array1 = [1, 2, 3, 4, 5];
let result = array1.filter2(item => item > 3);
console.log(result); // [ 4, 5 ]
}
手写some方法
{
console.log("7.3 手写some方法");
Array.prototype.some2 = function (fn) {
for (let i = 0; i < this.length; i++) {
const ele = this[i];
let flag = fn(ele); //将当前元素传递给函数
if (flag) {
return true;
}
}
return false;
};
let array1 = [1, 2, 3, 4, 5];
let result = array1.some2(item => item > 3);
console.log(result); // true
}
手写every方法
{
console.log("7.4 手写every方法");
Array.prototype.every2 = function (fn) {
for (let i = 0; i < this.length; i++) {
const ele = this[i];
let flag = fn(ele); //将当前元素传递给函数
if (!flag) {
// 只要有1个不符合,马上返回false
return false;
}
}
return true;
};
let array1 = [1, 2, 3, 4, 5];
let result1 = array1.every2(item => item > 3);
let result2 = array1.every2(item => item > 0);
console.log(result1); // false
console.log(result2); // true
}
手写map方法
{
console.log("7.5 手写map方法");
Array.prototype.map2 = function (fn) {
let newArray = [];
for (let i = 0; i < this.length; i++) {
const ele = this[i];
let item = fn(ele, i, this); //将当前元素传递给函数
newArray.push(item);
}
return newArray;
};
let array1 = [1, 2, 3, 4, 5];
let result1 = array1.map2((val, index, array) => {
return val * 2;
});
console.log(result1); // [ 2, 4, 6, 8, 10 ]
}
手写reduce方法
核心点:判断initialValue是否传递值,从而决定preValue是initialValue 还是 this[0]; 代码中的myValue既作为第一次的值,也作为后续的preValue进行传递
查看代码
//先分析reduce的用法
{
console.log("7.6 手写reduce方法 先分析reduce的用法");
let array1 = [1, 2, 3, 4, 5];
/**
* 第一个参数是function,包括四个参数:
* preValue:代表上次返回的值
* currentValue:代表当前值
* index: 表示数组当前索引
* myArray:表示当前数组
* 第二个参数代表第一次的默认值
*/
let sum1 = array1.reduce((preValue, currentValue, index, myArray) => {
return preValue + currentValue;
}, 0);
let sum2 = array1.reduce((preValue, currentValue, index, myArray) => {
return preValue + currentValue;
}, 10);
console.log(sum1); //15
console.log(sum2); //25
}
// 开始实操
{
console.log("7.6 手写reduce方法 实操");
Array.prototype.reduce2 = function (fn, initialValue) {
let hasInitialValue = initialValue !== undefined; //表示是否传递initialValue参数
//表示初始值,initialValue有内容则代表初始值,否则数组的第一个元素作为初始值; 同时也作为处理后的值进行传递
let myValue = hasInitialValue ? initialValue : this[0];
// this表示数组
let myLength = this.length; //数组的长度
let originIndex = hasInitialValue ? 0 : 1; //表示遍历从第几个元素开始,如果initialValue有内容,则索引从0开始,否则从1开始
for (let i = originIndex; i < myLength; i++) {
myValue = fn(myValue, this[i], originIndex, this);
}
return myValue;
};
let array1 = [1, 2, 3, 4, 5];
let sum1 = array1.reduce((preValue, currentValue, index, myArray) => {
return preValue + currentValue;
});
let sum2 = array1.reduce((preValue, currentValue, index, myArray) => {
return preValue + currentValue;
}, 10);
console.log(sum1); //15
console.log(sum2); //25
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。