第二节: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键值对存储的同时,进行最多元素个数的比较。

PS: 掌握思路即可,这里不考虑 键值对 "1" 和 1 作为key的时候,相同的问题
查看代码
 {
	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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2022-07-12 20:57  Yaopengfei  阅读(136)  评论(6编辑  收藏  举报