前端面试知识点总结(一)
js数据类型
基本数据类型:null, undefined, boolean, number, string, symbol
引用数据类型:Object(包含Function, Date, Array等)
基本数据类型存放于栈内存中,引用数据类型在栈内存和堆内存中均有,其中栈中存储了指针,指向堆中该实体的起始位置,当访问引用数据类型时,会首先在栈内存中查找器指针,然后到堆中获得实体
js数据类型转换
// 转化为字符串
String(false) // 'false'
String(function(){}) // 'function(){}'
String([1,2,3]) // '1,2,3'
String({}) // '[Object, Object]'
// 转化为数字
Number('1x') // NaN
Number([]) // 0
Number([1]) // 1
Number(null) // 0
Number({}) // NaN
Number(Symbol()) // Error报错
判断数据类型
typeof instanceof constructor Object.prototype.toString.call()
对于对象来说,除了函数之外,其余对象的typeof都会返回object
typeof [] // object
typeof function(){} // function
typeof null // object
instanceof
也可以用来判断对象的数据类型,但是不能判断基本数据类型
[] instanceof Array
function(){} instanceof Function
{} instanceof Object
constructor
可以判断基本数据类型和引用数据类型的类型
(2).constructor === Number;
(true).constructor === Boolean;
('str').constructor === String;
[].constructor === Array;
function(){}.constructor === Function;
{}.constructor === Object;
Object.prototype.toString.call
也可以用来判断类型,也是开发中运用最多的一种
Object.prototype.toString.call(2) // '[Object Number]'
Object.prototype.toString.call('') // '[Object String]'
Object.prototype.toString.call(true) // '[Object Boolean]'
Object.prototype.toString.call([]) // '[Object Array]'
Object.prototype.toString.call(function() {}) // '[Object Function]'
全局内置对象
- 值属性 Infinity NaN undefined null
- 函数属性 eval() parseInt() parseFloat()
- 基本对象 Object Function Boolean Error
- 数字和日期 Number Math Date
- 字符串 String RegExp
- 可索引的集合对象 Array
- 可使用键的集合对象 Map Set WeakMap WeakSet
- 结构化数据 JSON
- 控制抽象对象 Promise Generator
- 反射 Reflect Proxy
undefined undeclared
变量声明未赋值为undefined, 变量未声明为undeclared , 对二者使用typeof 都会返回undefiend
为什么typeof null === object
在js最初版本中使用32位操作系统,用低位存储变量的类型信息,000开头表示对象,而null为全0,所以误判为object,虽然现在的内部类型判断代码已经发生改变,但这个bug依然流传下来了
{}和[]使用valueOf和toString的结果
{}.valueOf(); // {}
[].valueOf(); // []
{}.toString(); // '[Object Object]'
[].toString(); // ''
创建对象的几种形式
- 工厂模式,只做简单的类型封装,无法与某个类型关联
function person() {
return {
name: 'lznism',
age: 10
}
}
var p = person()
- 构造函数模式,使用时使用new来新建某个对象,缺点无法是公用某些的属性和方法,比如下面例子中的
sayName
显然是可以公用的,但是我们使用new来创建时,每次都会创建一个新的sayName
方法,造成不必要的空间上的浪费
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function () {
alert(this.name)
}
}
var p1 = new Person('a', 10)
var p2 = new Person('b', 20)
p1.sayName === p2.sayName // false
- 原型模式,因为每个函数都会有一个prototype属性,该属性是一个对象,这个对象上所有的属性都是公用的,同样也是通过new来新建,缺点是,共享了所有的属性和方法,无法针对单个实例做特殊化
function Person() {}
Person.prototype.name = 'aaa';
Person.prototype.sayName = function() {
alert(this.name);
}
var p1 = new Person();
var p2 = new Person();
p1.name = 'bbb';
console.log(p1.name); // 'bbb'
p1.sayName === p2.sayName; // true
- 使用原型模式+构造函数模式。将对象独有的属性和方法使用构造函数创建,将所有对象共有的属性和方法放在prototype上
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
alert(this.name);
}
var p1 = new Person('a', 10);
var p2 = new Person('b', 20);
p2.sayName === p1.sayName; // true
- 动态原型模式,将prototype上的属性和方法在构造函数内部创建,通过判断该属性是否存在,可以实现原型上的属性和方法仅创建一次的效果
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this.prototype); // undefiend
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function() {
alert(this.name);
}
}
}
var p1 = new Person('a', 10);
var p2 = new Person('b', 20);
p1.sayName === p2.sayName; // true;
- 寄生构造函数模式,基本和工厂模式相同,但是使用new来新建对象,缺点是也无法确定返回对象的类型
// 构造函数在不返回值的情况下,默认会返回新创建的对象
// 而通过构造函数末尾添加一个return语句,则可以重写构造函数的返回值
function Person(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
}
return o;
}
var p = new Person('a', 10)
js继承的几种方法
- 借用构造函数继承。该模式的优点:可以在构造函数中向父类构造函数传递参数;缺点:无法做到对方法的复用,父类prototype上的属性,对子类也是不可见的。
function Super() {
this.colors = ['red', 'blue', 'green'];
}
function Sub() {
Super.call(this);
}
var i1 = new Super();
is.colors.push('black');
console.log(i1.colors); // 'red', 'blue', 'green', 'black'
var i2 = new Sub();
console.log(i2.colors); // 'red', 'blue', 'green'
- 组合继承,这种继承方式,既可以通过在原型上定义方法实现函数复用,又能够保证每个实例都有自己的属性。这种模式是js中最常用的继承模式,而且可以使用instanceof来识别给予组合继承创建的对象,缺点是调用了两次超类的构造函数,导致基类原型对象中添加了不少不必要的超类实例的属
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age; // SubType独有的属性
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
alert(this.age);
}
var i1 = new SuperType('a');
i1.sayName(); // 'a'
var i2 = new SubType('b', 10);
i2.sayName(); // 'b'
i2.sayAge(); // 10
- 原型式继承,简单来说就是传入一个对象,将这个对象赋给内部函数F的prototype,最后返回内部函数F的实例。优点:可以实现基于一个对象的简单继承,不必创建构造函数。缺点:与原型中提到的缺点相同,一个是传参问题,一个是属性共享问题
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
- 寄生式继承,创建一个仅用于封装继承的函数,该函数在内部以某种方式增强对象,最后返回这个对象
function createAnother(original) {
var clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function () { // 某种方式增强这个对象
console.log('hi');
}
return clone;
}
var p = { name: 'aaa' };
var anotherP = createAnother(p);
anotherP.sayHi(); // 'hi'
- 寄生式组合继承,与组合继承不同的地方主要是,在继承原型时,我们继承的不是父类的实例对象,而是父类原型对象的一个实例对象,这样就解决了基类原型对象中添加了不必要的父类实例对象的属性的问题
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
寄生组合继承的具体实现
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function () {
console.log(this.grade);
}
this指向
- 在浏览器全局环境中this指向window
- 在函数中,this永远指向最后调用它的那个对象
- 构造函数中,this指向new出来的那个对象
- call, apply, bind中的this被强绑在指定的那个对象上
- 箭头函数中this指向父作用域的this,不是调用时的this
获取对象原型的方法
- p.proto
- p.constructor.prototype
- Object.getPrototypeOf(p)
闭包
通过闭包,可以让让函数外部访问到函数内部的局部变量,同时闭包函数运行结束之后,其内部被引用的变量将继续保存在内存中,不会被垃圾回收机制回收
冒泡,捕获
冒泡:从内层的元素开始,事件会逐渐向外层元素传播
捕获:从外层的元素开始,时间会逐渐向内层元素传播
时间会经历3个阶段:捕获阶段->目标元素阶段(按照事件定义顺序执行)->冒泡阶段
手写ajax,jsonp请求
class Ajax {
constructor(params = {}) {
this.params = params;
this.data = params.data || {};
}
/**
* ajax请求
* @param {Object} params 传入的参数
*/
ajax(params = {}) {
params.type = (params.type || "GET").toUpperCase();
params.data = this.formatParams(params.data);
let xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject("Microsoft.XMLHTTP");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const status = xhr.status;
if (status >= 200 && status < 300) {
let response = null;
const type = xhr.getResponseHeader("Content-Type");
if (type.indexOf("xml") !== -1 && xhr.responseXML) {
response = xhr.responseXML;
} else if (type === "application/json") {
response = JSON.parse(xhr.responseText);
} else {
response = xhr.responseText;
}
typeof params.success === "function" && params.success(response);
} else {
typeof params.error === "function" && params.error(xhr.status);
}
}
};
if (params.type === "GET") {
xhr.open(params.type, params.url + "?" + params.data, true);
xhr.send(null);
} else {
xhr.open(params.type, params.url, true);
xhr.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8"
);
xhr.send(params.data);
}
}
/**
* jsonp请求
* @param {Object} params 传入的参数
*/
jsonp(params = {}) {
const callbackName = params.jsonp;
const head = document.getElementsByTagName("head")[0];
params.data["callback"] = callbackName;
const data = this.formatParams(params.data);
const script = document.createElement("script");
head.appendChild(script);
// 创建回调函数
window[callbackName] = function(json) {
head.removeChild(script);
clearTimeout(script.timer);
window[callbackName] = null;
typeof params.success === "function" && params.success(json);
};
script.src = params.url + "?" + data;
// 超时处理
if (params.time) {
script.timer = setTimeout(() => {
window[callbackName] = null;
head.removeChild(script);
typeof params.success === "function" &&
params.success({
message: "超时"
});
}, params.time);
}
}
formatParams(data = {}) {
const arr = [];
for (let name in data) {
arr.push(`${encodeURIComponent(name)}=${encodeURIComponent(data[name])}`);
}
arr.push(`v=${this.getRandom()}`);
}
getRandom() {
return Math.floor(Math.random() * 10000 + 500);
}
}
js延迟加载的方式
- 放在文档底部
- defer 立即下载,但是延迟执行。脚本不会影响页面的构造,会被延迟到整个页面都解析完毕之后再执行。规范要求它们是按照出现的先后顺序执行,但是实际上有些浏览器并不是如此。
- async 异步加载,不会阻塞页面的解析过程,但是脚本加载完成后立即执行js脚本,这个时候如果文档解析没有完成同样会阻塞,一般不会按照顺序执行。
- 动态创建script标签
js模块化
- CommonJS, NodeJS是CommonJS规范的主要实践者,CommonJS使用同步的方式加载模块。在服务端由于模块文件都是本地保存,读取速度非常快,所以这样做不会有问题。
- AMD 该规范采用异步方式加载模块,模块的加载不影响后面语句的运行,所有依赖这个模块的语句都定义在一个回调函数中,等到加载完成之后这个回调函数才会运行。
- CMD
seajs
。它与AMD很相似,不同点在于,AMD推崇依赖前置,提前执行;CMD推崇依赖就近,延迟执行。 - ES6在语言标准层面上,实现了模块的功能,而且实现的相当简单。ES6的模块不是对象,import命令会被js引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个使得静态分析成为可能。
CommonJS模块输出的是一个值的拷贝,ES6输出的是值的引用。
CommonJS是运行时加载,ES6是编译时输出接口
ES6与CommonJS的差异
1.CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
手写map
// callback接受3个参数
// 1. 当前遍历到的arr[i]
// 2. 当前遍历到的索引i
// 3. 当前遍历的数组
function map(arr = [], callback) {
if (
!Array.isArray(arr) ||
typeof callback !== "function" ||
arr.length === 0
) {
return [];
}
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = callback(arr[i], i, arr);
result.push(item);
}
return result;
}
手写filter
function filter(arr = [], callback) {
if (
!Array.isArray(arr) ||
arr.length === 0 ||
typeof callback !== "function"
) {
return [];
}
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = callback(arr[i], i, arr);
item && result.push(arr[i]);
}
return result;
}
手写new
function objectFactory() {
const obj = {};
const ctor = Array.prototype.shift.apply(arguments);
obj.__proto__ = ctor.prototype;
const ret = ctor.apply(obj, arguments);
return typeof ret === "object" ? ret : obj;
}
function Person(name) {
this.name = name;
}
var obj = objectFactory(Person, "aaa");
console.log(obj); // true
console.log(obj instanceof Person); // true
函数柯里化
function subCurry(fn) {
const slice = Array.prototype.slice;
// 获取第一次传递进来的参数
const args = slice.call(arguments, 1);
return function() {
// 将第一次传递进来的参数和第二次传递进来的参数合并
const newArgs = args.concat(slice.call(arguments));
return fn.apply(this, newArgs);
};
}
function curry2(fn, length) {
length = length || fn.length;
const slice = Array.prototype.slice;
return function() {
if (arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
return curry2(subCurry.apply(this, combined), length - arguments.length);
} else {
return fn.apply(this, arguments);
}
};
}
function demo(a, b, c) {
return [a, b, c];
}
var demoCurry = curry2(demo);
console.log(demoCurry(1, 2, 3));
console.log(demoCurry(1, 2)(3));
console.log(demoCurry(1)(2)(3));
console.log(demoCurry(1)(2, 3));
bind/call/apply手写
Function.prototype.myCall = function(context = window) {
if (typeof this !== "function") {
throw new Error("Only function can call this method!");
}
const args = [...arguments].slice(1);
context.fn = this;
const result = context.fn(...args);
delete context;
return result;
};
Function.prototype.myBind = function(context = window) {
if (typeof this !== "function") {
throw new Error("Only function can call this method!");
}
const args = [...arguments].slice(1);
return function() {
return fn.apply(context, args.concat(...arguments));
};
};
Function.prototype.myApply = function(context = window) {
if (typeof this !== "function") {
throw new Error("Only function can call this method!");
}
let result;
context.fn = this;
if (arguments[1]) {
result = context.fn(arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
};
手写reduce
function reduce(arr, callback, initialVal) {
if (
!Array.isArray(arr) ||
typeof callback !== "function" ||
arr.length === 0
) {
return [];
}
// 如果没有将initialVal传递给该函数,默认使用数组第一项作为initialVal
const hasInitialVal = initialVal !== undefined;
let value = hasInitialVal ? initialVal : arr[0];
// 如果有传递initialVal,则索引从1开始,否则从0开始
for (let i = hasInitialVal ? 0 : 1; i < arr.length; i++) {
value = callback(value, arr[i], i, arr);
}
return value;
}
深拷贝
- 深拷贝:将对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改不会影响源对象
- 浅拷贝:创建一个新对象,这个对象有着原始属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用数据类型,拷贝的就是这个地址,修改这个对象,也会影响到原对象。浅拷贝的方式通常有
扩展运算符,Object.assign(), Array.prototype.slice()
深拷贝方式1:JSON.parse(JSON.stringify(obj)),这种方式有很多缺点,会忽略undefined, symbol, 函数, 不能解决循环引用的问题,不能处理正则,new Date()
// 简易版深拷贝
function deepClone(target, set = new WeakSet()) {
if (typeof target === 'object') {
const cloneTarget = Array.isArray(target) ? [] : {};
if (set.has(target)) {
return target;
}
set.add(target);
for (let key in target) {
cloneTarget[key] = deepClone(target[key], set);
}
return cloneTarget;
} else {
return target;
}
}
var/let/const 区别
- var声明的变量会挂载到window上,let和const不会
- var声明的变量可以提升,let和const不会
- let/const会形成块级作用域
if (true) {
var a = 100;
let b = 101;
}
console.log(a); // 100
console.log(b); // b is not defined
- 同一作用域下,let和const不能声明同名变量,var可以
- let和const有暂时性死区
var a = 100;
if (true) {
a = 101;
// 在当前块作用域中存在a使用let生命的情况下,给a赋值101,只会在当前作用域查找变量a
// 这时a还没有声明,因此会报a is not defined
let a = 1;
}
防抖、节流
函数防抖:最后一个事件结束后n秒后,再执行回调。如果这n秒内再有事件被触发,则重新执行
function debounce(fn, wait) {
let timer = null;
return function() {
const context = this;
// 如果此时存在定时器,则清掉定时器,重新计时
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(context, arguments);
}, wait);
}
}
函数节流:单位时间内,只能有一次回调函数执行,如果在同一个单位时间内某事件被多次触发,只能有一次生效
function throttle(fn, delay) {
let prev = Date.now();
return function() {
const context = this;
const curr = Date.now()
if (curr - prev >= delay) {
prev = curr;
return fn.apply(context, arguments);
}
}
}