JavaScript面试题
数据类型
1.JavaScript有哪些数据类型,它们的区别?
类型:JavaScript共有8种数据类型,undefined,null,Boolean,string,number,bigint,symbol,object。
其中symbol和bigint是es6中新增的。
symbol代表创建后独一无二且不可变的数据类型,主要为了解决可能出现的全局变量冲突的问题。
bigint可以安全的存储和操作大整数。
区别:
2.JavaScript中数据类型的检测方式有哪些?
- 使用typeof判断,其中数组,对象,null,都会被判断为object,其他数据类型可以正确判断。例如
typeof 123
'number'
- 使用instanceof判断对象的类型,内部运行机制是判断其在原型链中能否找到该类型的原型,因此instanceof只能用于判断引用类型的数据,不能判断基本类型。例如:
[] instanceof Array
true
- 使用constructor判断,但null,undefined不可使用,因为它们不是由对象构建。例如:
true.constructor
ƒ Boolean() { [native code] }
- 使用Object.prototype.toString.call() 使用Object对象的原型方法toString来判断数据类型。例如:
Object.prototype.toString.call(123);
'[object Number]'
3.判断数组的方式有哪些?
- 通过ES6的
Array.isArray()
判断 - 通过原型链判断
arr.__proto__ === Array.prototype
- 通过instanceof判断
arr instanceof Array
- 通过Object判断
Object.prototype.toString.call(arr);
- 通过**isPrototypeOf()**判断
Array.prototype.isPrototypeOf(arr);
4.null和undefined区别?
null代表的是空对象,一般用于返回值可能为对象的变量的初始化。
undefined代表未定义,变量声明了但是未赋其值时返回值可能为undefined
5.instanceof操作符的实现及其原理?
原理:instanceof通过判断构造函数的prototype属性是否出现在对象的原型链中来判断数据是否属于该类型
function myInstanceOf(left, right) {
let proto = Object.getPrototypeOf(left);
let prototype = right.prototype;
while (true) {
if (!proto) {
return false;
}
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
console.log(myInstanceOf([], Array)); //true
console.log([] instanceof Array); //true
console.log(myInstanceOf(123, Array)); //false
console.log(123 instanceof Array); //false
6.为什么0.1+0.2 !== 0.3? 如何让其相等?
因为计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。而这两个数的二进制都是无限循环的数。
如何相等:(n1+n2).toFixed(2);
7.typeof NaN 的结果是什么
typeof NaN; // ‘number’
NaN是一个特殊值 它和自身不相等 所以 NaN !== NaN 为true
8.其它值到字符串的转换规则
Null和Undefined 转换成 ‘null’和’undefined’
Boolean类型 true转换’true’ false转换’false’
Number类型的值直接转换 不过极大极小的数字会使用指数形式
Symbol类型的值直接转换,但只允许显式强制类型转换
对于普通对象来说 一般会调用Object的toString()方法(Object.prototype.toString())来返回内部属性[[class]]的值,如”[object Object]“;如果对象有自己的toString()方法,字符串化时就会调用该方法并使用其返回值
9.其他值到数字值的转换规则
Undefined类型的值转换为NaN
Null类型的值转换为0
Boolean类型的值,true转换为1 false转换为0
String类型的值转换如同使用Number()函数进行转换,如果包含非数字值则转换为NaN,空字符串为0
Symbol类型的值不能转换为数字 会报错
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,再遵循上述的规则将其强制转换为数字
10.其他值到布尔类型的值的转换规则
除了 undefined null false +0 -0 NaN “” 以外 都是true
11.Object.is()与比较操作符 "=" ""的区别
(Object.is(A,B)方法判断两个值是否是相同的值。)
双等号进行相等判断时,如果两边的类型不一致,会先进行强制类型转换后再进行比较
三等号=)进行相等判断时,如果两边的类型不一致,不会做强制类型转换,直接返回false
使用Object.is进行相等判断时,一般情况下和三等号判断相同,它处理了一些特殊情况,比如-0和+0不再相等,两个NaN是相等的
12.如何判断一个对象是空对象
if(Json.stringify(obj) == '{}'){
console.log('空对象');
}
if(Object.keys(obj).length < 0){
console.log('空对象');
}
13.Array的常见面试题
一个字符在数组中出现的次数
let str = '1232223123413';
// 方式一
let arr = Array.from(str);
let count = 0;
arr.forEach((e) => {
if (e == 2) {
count++;
}
});
console.log('count: ', count);//5
// 方式二
[...str].filter((i) => {
return i == 2;
}).length;//5
// 方式三
// str.split('2')// ['1', '3', '', '', '31', '3413']
str.split('2').length - 1;
判断每个字符出现次数
let str = '1232223123413';
// 方式一
let obj = {};
Array.prototype.forEach.call(str, (e) => {
if (typeof obj[e] === 'undefined') {
obj[e] = 1;
} else {
obj[e]++;
}
});
console.log('obj: ', obj);
// 方式二
let set = new Set();
let arr3 = [];
Array.prototype.forEach.call(str, (e) => {
set.add(e);
});
for (let value of set) {
arr3.push({ val: value, count: 0 });
}
Array.prototype.forEach.call(str, (e) => {
arr3.forEach((ele) => {
if (ele.val === e) {
ele.count++;
}
});
});
console.log('arr3: ', arr3);
不改变原数组的情况下插入数字 生成新数组
let arr4 = [1, 3, 4, 5];
let arr5 = arr4.map((ele) => {
return ele;
});
arr5.splice(1, 0, 2);
console.log('arr5: ', arr5);
// 移出数组arr([1,2,3,4,2,5,6,2,7,2])中与2相等的元素,并生成一个新数组,不改变原数组。
let arr6 = [1, 2, 3, 4, 2, 5, 6, 2, 7, 2];
let arr7 = arr6.filter((ele) => {
return ele !== 2;
});
arr7.splice(1, 0, 2);
console.log('arr7: ', arr7);
数组常见面试题
/**
* 1.数组的去重
*/
let arr = [1, 2, 3, 4, 3, 7, 7];
let set = new Set([...arr]); //set: Set(5) { 1, 2, 3, 4, 7 }
let newArr = [];
arr.forEach((ele) => {
if (newArr.indexOf(ele) == -1) {
newArr.push(ele);
}
});
console.log('newArr: ', newArr); //[ 1, 2, 3, 4, 7 ]
/**
* 2.数组的扁平化
*/
let res = [1, 2, [3, [4, [5]]]];
// res.flat(Infinity); //[1,2,3,4,5] Infinity代表扁平化最大层 不传参默认一层
function reduceFlatten(arr) {
return arr.reduce((result, item) => {
return result.concat(Array.isArray(item) ? reduceFlatten(item) : item);
}, []);
}
console.log(reduceFlatten(res)); //[ 1, 2, 3, 4, 5 ]
function myFlatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(myFlatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
console.log(myFlatten(res)); //[ 1, 2, 3, 4, 5 ]
/**
* 3.取数组最大值
*/
let arr2 = [1, 2, 3, 4, 5];
Math.max(...arr2);
arr2.sort((a, b) => a - b);
console.log('arr2[arr2.length - 1]: ', arr2[arr2.length - 1]); //5
/**
* 4.数组是否包含指定元素
*/
let animals = ['A', 'B', 'C', 'D'];
animals.includes('A'); // true
animals.indexOf('A') != -1;
/**
* 5.删除数组中的指定元素
*/
let selectData = [
{ depId: 44, name: '西安' },
{ depId: 33, name: '北京' },
{ depId: 23, name: '上海' },
{ depId: 35, name: '广州' },
{ depId: 64, name: '深圳' },
];
// 要在数组中删除的数据
let data = { depId: 23, name: '上海' };
let sub;
selectData.forEach((ele, index) => {
if (ele.depId == data.depId) {
sub = index;
}
});
selectData.splice(sub, 1);
console.log('selectData: ', selectData);
14.深拷贝
1.JSON.parse(JSON.stringify(oldObj))
const oldObj = {
a:1,
b:['e','f','g'],
c:{
h:{
i:2
}
}
}
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h,oldObj.c.h);
console.log(newObj.c.h === oldObj.c.h);
newObj.c.h.i = 3;
console.log(newObj.c.h,oldObj.c.h);
递归
const oldObj = {
a:1,
b:['e','f','g'],
c:{
h:{
i:2
}
}
}
function deepClone(obj){
if(typeof obj !== 'object' && typeof obj !== 'function'){
return obj;//基本类型直接返回
}
var o = Array.isArray(obj) ? []:{};
for(let key in obj){
if(obj.hasOwnProperty(key)){
o[key] = typeof obj[key] === 'object' ? deepClone(obj[key]):obj[key]
}
}
return o;
}
15.防抖和节流
防抖(debounce)
所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。
在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
ES6
1.ES6有哪些新增特性?
● let/const ● 模块化
● Class类 ● 箭头函数
● 解构赋值 ● rest参数
● Promise ● Set/Map
● 模板字符串 ● 扩展运算符
● Async/Await ● 迭代器/生成器● Symbol
模块化:
导入import '模块名称' from '路径';
导出let name = 'te',age = 12; export {name,age}
模块化优点:防止命名冲突,复用性强
Class类:
通过class关键字定义类,使用extends继承,子类必须在构造函数中通过super调用父类参数
箭头函数:
箭头函数内部没有arguments
,也没有prototype
,所以不能用new
关键字调用箭头函数
箭头函数的this指向其父级对象的this
解构赋值:
/**
* 数组的解构赋值
*/
//同时赋值多个变量
let a = 1;
let b = 2;
let c = 3;
let d = 4;
console.log(a, b, c, d); //1 2 3 4
let [e, f, g, h] = [1, 2, 3, 4];
console.log(e, f, g, h); //1 2 3 4
// 解构嵌套数组
const arr = [1, [2, 3, [4, 5, 6]]];
const [a, [b, c, [d, e, f]]] = arr;
console.log(a, b, c, d, e, f); //1 2 3 4 5 6
// 不完全解构
const [a, b, c] = [1, 2];
console.log(a, b, c); //1 2 undefined
const [d, e, [f, g]] = [3, 4, [5]];
console.log(d, e, f, g); //3 4 5 undefined
// 解构的默认值
let [a = true] = [];
console.log(a); //true
/**
* 对象的解构赋值
*/
const { foo, bar } = { foo: 'aaa', bar: 'bbb' };
console.log(foo, bar); //aaa bbb
/**
* 函数参数的解构赋值
*/
function f(options) {
let name = options.name;
let age = options.age;
let sex = options.sex;
console.log(name, age, sex); //n1 a1 s1
}
function f2({ name, age, sex }) {
// 经历了 let {name,age,sex} = options
console.log(name, age, sex); //n2 a2 s2
}
f({ name: 'n1', age: 'a1', sex: 's1' });
f2({ name: 'n2', age: 'a2', sex: 's2' });
// 解构数组类型参数
const arr = [
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
];
arr.map(([x, y]) => {
console.log(x + y); //2 4 6 8 10
});
// 为参数设定默认值
function func({ name = 'test', age = 'test', sex = 'test' }) {
console.log(name, age, sex); //张三 test test
}
func({ name: '张三' });
/**
* 常见使用场景
*/
// 交换变量
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
// 取函数返回值-数组
function raf() {
return [1, 2, 3];
}
let [a, b, c] = raf();
console.log(a, b, c); //1 2 3
// 取函数返回值-对象
function rof() {
return { d: 5, e: 6 };
}
let { d, e } = rof();
console.log(d, e); //5 6
// 函数参数的定义-参数与变量名一一对应
function f1([x, y, z]) {
console.log(x, y, z); //1 2 3
}
f1([1, 2, 3]);
function f2({ x, y, z }) {
console.log(x, y, z); //2 1 3
}
f2({ z: 3, x: 2, y: 1 });
// 提取JSON数据
let jsonData = {
id: 42,
code: 200,
data: {
num: 15,
price: 32,
pname: 'tst',
},
};
let {
id,
code,
data: { num, price, pname },
} = jsonData;
console.log(id, code, num, price, pname); //42 200 15 32 tst
rest参数:
rest用于获取函数参数中可能存在的多余参数,rest参数必须放在最后的位置
function createV() {
console.log(arguments); //500, 200, 'test'
}
createV(500, 200, 'test');
function createB(width, height, color, ...args) {
let message = `盒子的宽度=${width},高度=${height},颜色=${color}`;
console.log(message); //盒子的宽度=200,高度=100,颜色=red
console.log(args); //[ '测试消息1', '测试消息2' ]
}
createB(200, 100, 'red', '测试消息1', '测试消息2');
Promise:
详见后续
Map/Set:
Map和Set属于ES6新增加的对象
Map 对象用于保存键值对,任何JS支持的值都可以作为key或value
与对象不同的是:
- Object的键只能是字符串或者ES6的symbol值,而Map可以是任意值
- Map对象有一个size值,存储了键值对的个数,而Object对象没有类似属性
// 添加、删除成员
const m = new Map();
const o = { p: 'Hello world' };
m.set(o, 'content');
console.log('m: ', m); // Map(1) { { p: 'Hello world' } => 'content' }
console.log(m.get(o)); //content
console.log(m.has(o)); // true;
console.log(m.delete(o)); // true;
console.log(m.has(o)); // false
//接收一个数组作为参数,运行原理
const map = new Map([
['name', 'zhangsan'],
['age', '18'],
]);
console.log('map:', map); //map: Map(2) { 'name' => 'zhangsan', 'age' => '18' }
console.log(map.has('name')); //true
let arr = [
['name', 'zhangsan'],
['age', '18'],
];
const map1 = new Map();
arr.forEach(([key, value]) => map1.set(key, value));
console.log('map1: ', map1); //map1: Map(2) { 'name' => 'zhangsan', 'age' => '18' }
// map的键和内存地址绑定, k1 k2 值相同但是内存地址不同
let k1 = ['a'];
let k2 = ['a'];
const map3 = new Map();
map3.set(k1, 111).set(k2, 222);
console.log(map3.get(k1)); // 111
console.log(map3.get(k2)); // 222
Map实例的属性
Map.prototype.size()
:返回Map结构的成员总数
Map实例的方法分为两大类,操作方法和遍历方法
操作方法:
Map.prototype.set(key, value)
:set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
Map.prototype.get(key)
:get方法读取key对应的键值,如果找不到key,返回undefined。
Map.prototype.has(key)
:has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
Map.prototype.delete(key)
:delete方法删除某个键,返回true。如果删除失败,返回false。
Map.prototype.clear()
:clear方法清除所有成员,没有返回值。
遍历方法:
Map.prototype.keys()
:返回键名的遍历器。
Map.prototype.values()
:返回键值的遍历器。
Map.prototype.entries()
:返回所有成员的遍历器。
Map.prototype.forEach()
:遍历 Map 的所有成员。
Map的遍历顺序就是插入顺序
// 操作方法
const map = new Map();
map.set('foo', true);
map.set('bar', false);
console.log(map.size); //2
const m = new Map();
m.set('edition', 6); //键是字符串
m.set(262, 'standard'); //键是数字
m.set(undefined, 'nah'); //键是undefined
// 链式写法
let map2 = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
const m2 = new Map();
const hello = () => {
console.log('hello');
};
m2.set(hello, '123'); //键是函数
console.log(m2.get(hello)); //123
const m3 = new Map();
m3.set('edition', 6);
m3.set(262, 'standard');
m3.set(undefined, 'nah');
console.log(m3.has('edition')); //true
console.log(m3.has(262)); //true
console.log(m3.has(undefined)); //true
console.log(m3.has('123')); //false
m3.delete(262);
console.log(m3.has(262)); //false
console.log(m3.size); //2
m3.clear();
console.log(m3.size); //0
// 遍历方法
const map4 = new Map([
['F', 'no'],
['T', 'yes'],
]);
console.log('map4: ', map4);
for (let key of map4.keys()) {
console.log(key); // F T
}
for (let value of map4.values()) {
console.log(value); // no yes
}
for (let item of map4.entries()) {
console.log(item[0], item[1]); // F no, T yes
}
// 或者
for (let [key, value] of map4.entries()) {
console.log(key, value); // F no, T yes
}
// map转数组
const map5 = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
console.log([...map5.keys()]); //[ 1, 2, 3 ]
console.log([...map5.values()]); //['one', 'two', 'three']
console.log([...map5.entries()]); //[ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]
console.log([...map5]); //[ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]
const map6 = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
let newMap1 = new Map(
[...map6].filter(([key, value]) => {
return key < 3 && value != 'b';
})
);
console.log('newMap1: ', newMap1); //newMap1: Map(1) { 1 => 'a' }
let newMap = new Map(
[...map6].map(([key, value]) => {
return [key * 2, '_' + value];
})
);
console.log('newMap: ', newMap); //newMap: Map(3) { 2 => '_a', 4 => '_b', 6 => '_c' }
map6.forEach((value, key, mapS) => {
console.log('Key: %s, Value: %s', key, value);
// Key: 1, Value: a
// Key: 2, Value: b
// Key: 3, Value: c
});
// map转换为数组
const map5 = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
console.log('[...map5]: ', [...map5]); //[...map5]: [ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]
let newArr = [];
for (let [key, value] of map5.entries()) {
newArr.push(key);
newArr.push(value);
}
console.log('newArr: ', newArr); //[ 1, 'one', 2, 'two', 3, 'three' ]
// 数组转为Map
const map6 = new Map([
[1, 2],
[3, 4],
]);
console.log('map6: ', map6); //map6: Map(2) { 1 => 2, 3 => 4 }
// Map转为对象 如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [key, value] of strMap) {
obj[key] = value;
}
return obj;
}
const myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap);
console.log('myMap: ', myMap); //myMap: Map(2) { 'yes' => true, 'no' => false }
Set 类似于数组,但其成员值都是唯一的,没有重复的值,Set内部值判断类似于===所以5和’5’是两个不同值,在Set内部 NaN等于自身,所以Set实例内只会存在一个NaN。
// 数组去重
const s = new Set();
[2,3,4,5,5,2,2,].forEach(x => s.add(x))
console.log(s)
for(let i of s){
console.log(i);//2 3 4 5
}
let arr = [1,2,2,3,4,5,5,4,4,2,1]
let t = new Set(arr);
console.log('t: ', t);//t: Set(5) {1, 2, 3, 4, 5}
// 字符串去重
let str = 'aaabbcscdsa';
let s1 = [...new Set(str)].join('');
console.log('s1: ', s1);//s1: abcsd
// NaN
let set3 = new Set();
let a = NaN;
let b = NaN;
set3.add(a);
set3.add(b);
console.log(set3);//Set(1) {NaN}
Set实例的属性
Set.prototype.constructor
:构造函数,默认就是Set函数
Set.prototype.size
:返回Set实例的成员总数
Set的实例方法分为两大类,操作方法和遍历方法
操作方法:
Set.prototype.add()
:添加某个值,返回Set结构本身。
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。(会改变原有结构)
Set.prototype.has(value)
:返回一个布尔值,代表该值是否为Set的成员。
Set.prototype.clear()
:清除所有成员,没有返回值。
遍历方法:
Set.prototype.keys()
:返回键名的遍历器
Set.prototype.values()
:返回键值的遍历器
Set.prototype.entries()
:返回键值对的遍历器
Set.prototype.forEach()
:使用回调函数遍历每个成员
因为set是类似数组的结构,所以keys和values返回值其实一样。
let set = new Set(['red', 'red', 'green', 'yellow']);
for (let value of set.keys()) {
console.log(value); // red green yellow
}
for (let value of set.values()) {
console.log(value); // red green yellow
}
for (let en of set.entries()) {
console.log(en); // [ 'red', 'red' ]['green', 'green'][('yellow', 'yellow')];
}
set.forEach((ele) => console.log(ele)); // red green yellow
// 遍历的应用
let set1 = new Set(['red', 'green', 'yellow']);
let arr = [...set1];
console.log(arr); //[ 'red', 'green', 'yellow' ]
// 去重
let arr2 = [1, 3, 4, 5, 2, 31, 31, 31, 1, 2, 3];
let unique = new Set(arr2);
console.log(unique); //Set(6) { 1, 3, 4, 5, 2, 31 }
模板字符串:
this is ${value}
扩展运算符:
rest用于函数形参 扩展运算符用于实参
const str = ['a', 'b', 'c'];
function printS() {
console.log(arguments);
}
printS(str); //参数为一个数组,其中包含三个元素
printS(...str); //参数为三个元素
// 数组合并
const s1 = ['a', 'b', 'c'];
const s2 = ['d', 'e'];
let es5 = s1.concat(s2);
console.log('es5: ', es5); //es5: [ 'a', 'b', 'c', 'd', 'e' ]
let es6 = [...s1, ...s2];
console.log('es6: ', es6); //es6: ['a', 'b', 'c', 'd', 'e']
// 数组的克隆
const s3 = [...s2];
console.log('s3: ', s3); //s3: [ 'd', 'e' ]
// 将伪数组转化为真正的数组
function argToArray() {
let arr = [...arguments];
return arr;
}
let s4 = argToArray(1, 2, 3, 4);
console.log('s4: ', s4); //s4: [ 1, 2, 3, 4 ]
Async/Await:
详见Async/Await
迭代器/生成器:
未完待续
Symbol:
未完待续
2.let const var 的区别
let是块级作用域,var变量在函数外声明是全局作用域,let在变量未声明之前使用会直接报错,var不会;let禁止重复声明,var可以重复声明;变量在函数内部是局部作用域,变量仅在函数内生效。
const 一.const为常量声明方式,声明变量时必须初始化,后面出现的代码不能再修改该常量的值。 二.const实际上保证的并不是变量的值不得改动,而是指向变量的那个内存地址不得改动,所以定义的对象属性值可以改变。
3.如果new一个箭头函数会怎样
箭头函数是es6中提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能new一个箭头函数
new操作符的实现步骤如下:
1.创建一个对象
2.将构造函数的作用域赋值给新对象
(也就是将对象的__proto__属性指向构造函数的prototype属性)
3.指向构造函数中的代码,构造函数的this指向该对象
(也就是为这个对象添加属性和方法)
4.返回新对象
function student(name,age){
this.name = name;
this.age = age;
}
function createStu(constructor,...args){
let obj = Object.create(null);
Object.setPrototypeOf(obj,constructor.prototype);
let result = constructor.apply(obj,args);
return result instanceof Object ? result : obj;
}
4.箭头函数和普通函数的区别
- 箭头函数比普通函数更加简洁, 如果没有参数直接写一个()即可,如果有一个参数可以省去参数的括号
- 箭头函数没有自己的this 箭头函数中的this指向自己作用域的上一层
- 箭头函数继承来的this指向不会改变,call() apply() bind()不能改变箭头函数中this的指向
- 箭头函数不能作为构造函数使用 因为箭头函数没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当作构造函数使用。
- 箭头函数没有prototype
5.for in和for of的区别
for in 用于遍历对象时,得到的值为对象的key;遍历字符串或者数组时,得到的值为下标。
for of 不能遍历对象;遍历数组或者字符串时,得到的每一个值
for of 用于获取键值对的——值
for in 用于获取键值对的——键 value of key in 获取值用of 获取下标、键用in
const obj = {
a: 1,
b: 2,
c: 3,
};
for (let item in obj) {
console.log(item); // a b c
}
// for (let value of obj) {
// console.log(value);//报错
// }
const arr = [4, 5, 6];
for (let item in arr) {
console.log(item); //0 1 2
}
for (let item of arr) {
console.log(item); //4 5 6
}
const str = 'test';
for (let item in str) {
console.log(item); //0 1 2 3
}
for (let item of str) {
console.log(item); //t e s t
}
JavaScript基础
1.new操作符的实现原理
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型(protp)设置为函数的prototype对象
(3)让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,就返回这个引用类型的对象。
function student(name, age) {
this.name = name;
this.age = age;
}
function createStu(constructor, ...args) {
// 创建空对象
let obj = Object.create(null);
// 将空对象的__proto__ 指向构造函数的prototype
Object.setPrototypeOf(obj, costructor.prototype);
// 将obj绑定到构造函数上,便可以访问构造函数中的属性
let result = constructor.apply(obj, args);//obj.constructor(args);
// 如果result是一个对象则返回 否则返回obj
return result instanceof Object ? result : obj;
}
let stu1 = createStu(student, 'rte', '12');
console.log('stu1: ', stu1); //stu1: student { name: 'rte', age: '12' }
2.数组有哪些原生方法
数组转字符串:toString() join() join可以指定转换为字符串时的分隔符
操作数组 尾部 pop() push() 头部 shift() unshit()
数组连接 concat() 返回的是拼接好的数组 不影响原数组
数组截取 slice() 截取数组一部分返回 不影响原数组
数组插入 splice() 影响原数组
查找特定项索引的方法 indexOf() lastIndexOf()
遍历的 map() forEach() filter()
3.什么是DOM和BOM
DOM是指文档对象模型,它指的是把文档当作一个对象,这个对象主要定义了处理网页内容的方法和接口,DOM的最根本对象的document。
BOM是指浏览器对象模型,它指的是把浏览器当作一个对象来看,这个对象主要定义了与浏览器交互的方法和接口,BOM的核心是window。
window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
4.为什么函数的arguments参数是类数组不是数组?如何遍历类数组?
arguments是一个对象,它的属性是从0开始依次递增的数字,还有callee和length等属性,与数组相似,但是它却没有数组常见的方法属性,比如forEach,所以它们叫类数组
遍历类数组有三种方法
//一 使用call 将数组的方法应用到类数组上,
function foo() {
Array.prototype.forEach.call(arguments, (a) => console.log(a));
}
foo(1, 23, 4, 45, 6); // 1 23 4 45 6
// 二 使用 Aarry.form()将类数组转换为数组
function foo() {
const arrArgs = Array.from(arguments);
arrArgs.forEach((a) => console.log(a));
}
foo(1, 23, 4, 45, 6); // 1 23 4 45 6
// 三 使用展开运算符将类数组转化为数组
function foo() {
const arrArgs = [...arguments];
arrArgs.forEach((a) => {
console.log('a: ', a);
});
}
foo(1, 23, 4, 45, 6); // 1 23 4 45 6
5.对类数组的理解,如何转化为数组
// 通过call调用数组的slice方法
Array.prototype.slice.call(arrayLike); //效果相当于arrayLike.slice();
// 通过call调用数组的splice方法
Array.prototype.splice.call(arrayLike,0);
// 通过apply调用数组的concat方法
Array.prototype.concat.apply([],arrayLike);
Array.from(arrayLike);
6.对Ajax的理解,实现一个Ajax请求?
Ajax指的是通过JavaScript的异步通信,从服务器获取的XML文档中提取数据,再更新到当前网页的对应部分,从而不用刷新整个页面。
一 创建一个XMLHttpRequest对象
二 在这个对象上使用open方法创建一个HTTP请求,open方法所需的参数是,请求的方法、请求的地址、是否异步和用户的认证信息。
三 在发起请求前,可以为这个对象添加一些信息和监听函数,比如说通过setRequestHeader方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个XMLHttpRequest对象一共有5个状态,当它的状态变化时会触发onreadystatechange事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的readyState变为4时,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是2××或者304的话打包返回正常。这个时候就可以通过response中的数据来对页面进行更新了。
四 当对象的属性监听函数设置完成后,最后调用sent方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVEL_URL = '/server';
let xhr = new XMLHttpRequest();
// 创建http请求
xhr.open('GET','www.baidu.com',true);
// 设置监听状态函数
xhr.onreadystatechange = function(){
if(this.readyState !== 4){
return;
}
if(this.status === 200){
handle(this.response);
}else{
console.log(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function(){
console.log(this.statusText);
}
// 设置请求头信息
xhr.responseType = 'json';
xhr.setRequestHeader('Accept','application/json');
// 发崧http请求
xhr.send(null);
###7.JavaScript为什么要进行变量提升,它导致了什么问题?
解析和预编译过程中的声明可以提高性能,让函数可以在执行时预先为变量分配栈空间。
声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。
var tmp = new Date();
function fn() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
fn(); //undefined
//在这个函数中,原本是打印外层的tmp变量,但是由于变量提升的问题
//内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以输出undefined
原型与原型链
1.对原型、原型链的理解
原型:在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数内容都有一个prototype属性,这个属性是一个对象,包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的prototype属性对应的值。
原型链:当访问一个对象的属性时,如果对象内部不存在这个属性,那么它就会去它的原型对象里赵这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是Object.prototype._ _ proto_ _ 是null。这也是为什么新建的对象能够使用toString()等方法的原因。
// 这是一个构造函数
function Foo(name, age) {
this.name = name;
this.age = age;
}
// 修改构造函数的prototype属性
Foo.prototype = {
showName: function () {
console.log('I am ' + this.name); //this是什么要看执行的时候谁调用了这个函数
},
showAge: function () {
console.log('I am ' + this.age); //this是什么要看执行的时候谁调用了这个函数
},
};
let stu = new Foo('小明', '19');
/* 当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,
那么就会去它构造函数的prototype属性中找 */
stu.showName(); //I am 小明
stu.showAge(); //I am 19
// 实例的__proto__指向构造函数的prototype
console.log(stu.toString === Foo.prototype.__proto__.toString); // true 最后调用的都是Object的toString
console.log(stu.__proto__ == Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true Foo是object的实例
//
console.log(Object.prototype.__proto__); //null Object.prototype一般是原型链的终点
2.原型链的终点是什么?如何打印出原型链的终点?
Object.prototype.__ proto__
null 原型链上所有原型都是对象, 所有的对象最终都是由Object构造的
3.如何获得对象非原型链上的属性
function iterate(Obj) {
var res = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
res.push(key + ': ' + obj[key]);
}
}
return res;
}
// obj.hasOwnProperty(key);
执行上下文/作用域链/闭包
1.对闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数。创建闭包最常见 的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途
闭包的第一个用途是我们在函数外部能够访问到函数内部的变量。
闭包的另一个用途是 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量的对象的引用,所以这个变量对象不会被回收。
闭包的缺点:
滥用闭包会使得函数中的变量都被保存在内存中,导致内存泄漏。解决方案是退出函数之前,将不使用的局部变量全部删除。
闭包的本质就是,上级作用域内变量的生命周期,因为被下级作用域内引用,而没有释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
function f1() {
var n = 999;
nAdd = function () {
console.log('n+=', (n += 1));
};
function f2() {
console.log('f2', n);
}
return f2;
}
var result = f1();
result(); //f2 999
nAdd(); //n+= 1000
result(); //f2 1000
/**
* f1是f2的父函数,而f2被赋值给了一个全局变量,
* 这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终存在内存中。
* 不会在调用结束后,被垃圾回收机制回收
* nAdd没有使用var,所以它也是一个全局变量
*/
在js中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决var定义函数的问题。
for(var i = 1 ; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}// 6 6 6 6 6 6
//首先是因为setTimeout是个异步函数,所以会先把循环全部执行完毕,这时候i就是6了,所以会输出五个6
解决方案有三种
//一 闭包
for(var i = 1 ; i <= 5 ; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000)
})(i)
}// 1 2 3 4 5
// 在上述代码中,首先使用了立即执行函数将i传入函数内部,这个时候值就被固定在了参数j上面不会改变,
// 当下次执行timer这个闭包的时候,就可以使用外部函数的变量j,从而达到目的
//二 使用settimeout的第三个参数
for(var i=1; i<=5;i++){
setTimeout(
function timer(j){
console.log(j);
},
i*1000,
i
)
}//1 2 3 4 5
//第三个参数会被当做timer的参数传入
//三 使用let
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}//1 2 3 4 5
2.对作用域、作用域链的理解
1)全局作用域和函数作用域
(1)全局作用域
最外层函数 和 最外层函数外面定义的变量拥有全局作用域
所有未定义直接赋值的变量自动声明为全局作用域
所有window对象的属性拥有全局作用域
全局作用域的弊端是,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
(2)函数作用域
函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
使用let和const指令可以声明块级作用域,也可以由{}创建
在循环中比较适合绑定块级作用域,这样可以把声明的计数器变量限制在循环内部。
3)作用域链
如果在当前作用域查找不到所需要的变量,就去父级作用域查找,依次向上级,直到window对象终止,这一层层的关系就是作用域链。
作用域链的作用是 保证 对执行环境有权访问的所有变量和函数的 有序访问,通过作用域链,可以访问到外层环境的变量和函数 。
3.对执行上下文的理解
执行上下文分为,全局执行上下文和函数执行上下文,
当js执行代码时,首先会遇到全局代码,会创建一个全局执行上下文并且压入执行栈,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,js会执行位于栈顶的函数,函数执行完后,当前函数执行上下文从栈中弹出,继续执行下一个上下文。当所有代码执行完毕之后,从栈中弹出全局执行上下文。
1.执行上下文类型
(1)全局执行上下文
任何不在函数内的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
2.执行上下文栈
js引擎使用上下文栈管理上下文
当js执行代码时,首先会遇到全局代码,会创建一个全局执行上下文并且压入执行栈,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,js会执行位于栈顶的函数,函数执行完后,当前函数执行上下文从栈中弹出,继续执行下一个上下文。当所有代码执行完毕之后,从栈中弹出全局执行上下文。
3.创建执行上下文
创建执行上下文有两个阶段:创建阶段和执行阶段
4.当程序调用一个函数时,会发生什么?
以下几个步骤:
- JavaScript创建一个新的执行上下文,我们叫作本地执行上下文。
- 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
- 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。
函数什么时候结束?当它遇到一个return语句或一个结束括号}。
5.当一个函数结束时,会发生以下情况:
- 这个本地执行上下文从执行堆栈中弹出。
- 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有return语句,则返回undefined。
- 这个本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么 称为本地执行上下文中自有的变量。
this/call/apply/bind
1.对this的理解
this是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中this的指向可以通过几种调用模式来判断。
this就是函数运行时所在的对象(环境)
一是作为普通函数执行时,this指向window
二是作当函数作为对象的方法被调用时,this指向该对象
三是构造器调用 this指向返回的这个对象
四是箭头函数 箭头函数的this绑定 取决于this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,this绑定到最近的一层对象上。
五是apply、call和bind调用模式,这三个方法都可以显式的调用函数的this指向。apply接收的参数是数组,call接收参数列表,bind方法通过传入一个对象,返回一个this绑定了传入对象的新函数,这个函数的this指向除了使用new时会改变,其他情况下都不会改变。若为空默认是指向全局对象window
this就是函数运行时所在的对象(环境)
2.call appy bind的作用和区别
作用
call bind apply都是用于改变this指向的,所传入的第一个参数都是this要指向的对象,都可以利用后续参数传参
区别
call bind 的参数是依次传参 一一对应的
但是apply只有两个参数,第二个参数为数组
call和apply都是对函数进行直接调用,而bind方法返回的仍是一个函数
function changeStyle(attr, value) {
this.style[attr] = value;
}
var box = document.getElementById('box');
// “偷”(继承) changestyle的方法
window.changeStyle.call(box, 'height', '200px');//box style的height为200px
window.changeStyle.apply(box, ['height', '200px']);
var a = {
name: 'girl',
age: 23,
say: function (name, age) {
console.log('name: ', name, 'age: ', age);
},
};
var b = {
name: 'boy',
age: 24,
};
a.say.call(b, 'zhangsan', '15'); //name: zhangsan age: 15
a.say.apply(b, ['lisi', 16]); //name: lisi age: 16
// 因为bind返回值是函数 所以需要再执行一下
a.say.bind(b, 'wangwu', '16')(); //name: wangwu age: 16
异步编程
1.对promise的理解
Promise是一种异步编程解决方案,Promise有三种状态 pending(等待态) fulfilled(成功态) rejected(失败态),Promise的构造函数接收一个函数作为参数, 且这个函数又有两个参数,resolve和reject;
resolve函数是将promise对象状态改变为成功态且在异步操作成功时调用,将异步操作的结果作为参数传递出去。
reject函数是将Promise对象的状态改变为失败态,且在异步操作失败时调用,将异步操作报出的错误作为参数传递出去。
Promise 是异步编程的一种解决方案: 从语法上讲,promise是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。 promise有三种状态: pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。创造promise实例后,它会立即执行。
2.promise的基本用法
/*
promise的构造函数接收一个参数:函数,并且这个函数需要传入两个参数,
resolve:异步执行操作成功后的回调函数
reject:异步操作失败后的回调函数
*/
let p = new Promise((resolve, reject) => {
//做一些异步操作
setTimeout(() => {
console.log('执行完成');
}, 2000);
});
then链式操作的用法
p.then((data) => {
console.log(data);
})
.then((data) => {
console.log(data);
})
.then((data) => {
console.log(data);
});
reject的用法 catch的用法
let p = new Promise((resolve, reject) => {
setTimeout(() => {
let num = Math.ceil(Math.random() * 10);
if (num >= 5) {
resolve('大于五 成功');
} else {
reject('小于五 失败');
}
}, 1000);
});
//then中传了两个参数,then方法可以接受两个参数,
// 第一个对应resolve的回调,
// 第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据
p.then((res) => {
console.log('res: ', res);//大于五 成功
},(err) => {
console.log('rejected',err);
})
// catch的用法 其实它和then的第二个参数一样,用来指定reject的回调
p.then((data) => {
console.log('resolved',data);
}).catch((err) => {
console.log('rejected',err);
});
/*
resolve函数的目的是将Promise对象状态变成成功状态,
在异步操作成功时调用,将异步操作的结果,作为参数传递出去。
reject函数的目的是将Promise对象的状态变成失败状态,
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
*/
all的用法:谁跑的慢,以谁为准 执行回调。all接收一个数组参数,里面的值最终都算返回Promise对象
let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
// all的用法:谁跑的慢,以谁为准 执行回调。
// all接收一个数组参数,里面的值最终都算返回Promise对象
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
// 三个都成功则成功
}, function(){
// 只要有失败,则失败
})
race的用法:谁跑的快,以谁为准执行回调
// 请求某个图片资源
function requestImg() {
let p = new Promise((resolve, reject) => {
let img = new Image();
img.onload = function () {
resolve(img);
};
img.src = '图片的路径';
});
return p;
}
function timeout() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()])//哪个先回来 以哪个为准执行回调
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log('err: ', err);
});
finally
// promise实例无论状态是什么都会执行的函数。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
//在执行完then和catch后,仍然执行finally()
3.对async/await的理解
async/await是基于promise提出的解决异步的方案
async是一个加在函数前的修饰符,被async定义的函数会默认返回一个promise对象resolve的值。因此对async函数可以直接then,返回值就是then方法传入的函数。
async function fun0() {
console.log(1);
return 1;
}
fun0().then((val) => {
console.log(val); // 1 1
});
async function fun1() {
console.log('promise');
return new Promise(function (resolve, reject) {
resolve('promise');
});
}
fun1().then((val) => {
console.log(val); //promise promise
});
await也是一个修饰符,只能放在async函数内,可以理解为等待
await修饰的如果是promise对象,可以获取promise中返回的内容(resolve或reject的参数),且取到值后语句才会往下执行;如果不是promise对象,则把这个非promise的内容当作await表达式的结果
async function fun() {
let a = await 1;
let b = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('setTimeout');
}, 3000);
});
let c = await (function () {
return 'function';
})();
console.log(a, b, c);
}
fun();// 3秒后输出: 1 "setTimeout" "function"
4.async/await对比promise的优势
首先它的写法比较简单,promise虽然摆脱了回调地狱的问题,但是then的链式调用读起来还是比较麻烦
其次Promise传递中间值比较麻烦,async await的写法比较优雅
最后是处理错误更友好,async await可以使用try catch包裹
面向对象
1.js创建对象的方式有哪些
一 创建Object实例
二 利用字面量创建对象 就是{}里面采用键值对的形式
三 工厂模式
四 利用构造函数创建对象
五 原型模式
六 class
一 创建Object实例
var obj = new Object();
obj.name = '张三';
obj.age = 18;
obj.sex = '男';
obj.sayHi = function(){
console.log('Hi~');
}
// 缺点:
// 1.比较麻烦,创建实例后还要一个个添加属性和方法
// 2.容易和其他代码混合在一起,不利于代码的阅读和管理
二 利用字面量创建对象 就是{}里面采用键值对的形式
var obj = {
name:'张三疯',
age:18,
sex:'男',
sayHi:function(){
console.log('hi~');
}
} //对象字面量:就是花括号{}里面包含了表达这个具体事物(对象)的属性和方法。 var obj = {};
// 优点:比直接用Object创建实例更加直观,也符合对象的基本概念
// 缺点:不好复用
三 工厂模式
function createObj(name, age) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.sayHi = function () {
console.log(obj.name);
};
return obj;
}
let per = createObj('思腾', 200);
console.log(per.age); //200
per.sayHi(); //思腾
// 优点:解决了创建多个相似对象时 代码复用的问题
// 缺点:使用工厂模式创建的对象,没有建立起对象和类型间的关联。
四 利用构造函数创建对象
function moveStart(name, age, sex, move) {
this.name = name;
this.age = age;
this.sex = sex;
this.move = function () {
console.log(`主演了:${move}`);
};
}
let l = new moveStart('刘德华', '50', '男', '赌神');
console.log('l: ', l); //l: moveStart {
// name: '刘德华',
// age: '50',
// sex: '男',
// move: [Function (anonymous)]
// }
l.move();//主演了:赌神
// 优点:解决了工厂模式中对象类型无法识别的问题;(moveStart类型
// 缺点:每创建一个对象,都会创建一个say函数的实例,但是内容是一样的,导致内存的浪费
五 原型模式
function Person() {}
Person.prototype.name = 'james';
Person.prototype.age = 9;
Person.prototype.job = 'FrontEnd Software Engineer';
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //james
var person2 = new Person();
person2.sayName();//james
// 优点:解决了构造函数模式中多次创建相同函数对象的问题,所有实例可以共享同一组属性和函数
// 缺点:
// 1.原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,
// 在一定程度上造成不方便。
// 2.所有实例都是共享一组属性,对引用类型进厂操作,那么属性的操作就不是独立, 最后导致读写的混乱。
六 class
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
Say() {
return `我的名字是 ${this.name}`;
}
}
let cat1 = new Cat('有鱼', 2);
console.log(cat1.Say()); //我的名字是 有鱼
// 这是创建自定义类最常见的方式。通过构造函数来初始化对象的属性,
// 通过new关键字来创建实例对象实现函数方法的复用
七 动态原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function () {
console.log(this.name);
};
}
}
let person1 = new Person('james', 9, 'student');
person1.sayName();
// 优点:解决构造函数模式中 重复创建函数内存浪费的问题
八 寄生构造函数模式
function Person(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
// 优点:不需要修改原来的构造函数,达到了扩展对象的目的;
// 缺点:和工厂模式一样,不能依赖instanceof操作符来确定对象的类型
2.对象继承的方式有哪些
1.原型链继承
function parent() {
this.data = '111';
}
function child() {}
child.prototype = new parent();
let c = new child();
console.log(c.data);// 111
// 通过b函数的原型(b.prototype)指向a的实例(new a())来实现,这种继承的方法就称为原型链继承
// 缺点:存在引用类型值共享的问题,在b中修改某个继承来的引用类型数据,其他实例中也会被修改
2.构造函数继承
function parent() {
this.data = '111';
}
function child() {
// 继承
parent.call(this); //相当于child可以访问parent下面所有this能取到的内容
}
var c = new child();
console.log(c.data); //111
// 这种方式同样存在缺点:
// 使用这种方法b实例没有办法拿到a函数原型上的属性和方法。
3.extends类的继承
class parent {
constructor(a) {
this.filed1 = a;
}
filed1 = 2;
func1 = function () {};
}
class child extends parent {
constructor(a, b) {
super(a);
this.filed3 = b;
}
filed4 = 1;
func2 = function () {};
}