整理整理最近面试过程中遇到的部分问题-js/css系列
前言
JavaScript系列
请你说一下数组中常用的方法有哪些?
你知道js中有哪些类型吗?你是如何判断它们的?
我们都知道在js中有着基础类型和引用类型的概念,基础类型又分为:undefined
、null
、boolean
、string
、number
、symbol
。引用类型中常见的就是:[]
、{}
、function
等类型。对于这几种类型,我一般是使用typeof
和instanceof
来对他们进行判断的。
typeof
typeof
操作符返回一个字符串,表示未经计算的操作数的类型,对于typeof来说
,它可以判断除了null
之外的原始类型,而为什么不能判断出null
,这是一个历史遗留下来的问题,我们都知道所有的数值都会被转换为二进制进行比较,因此在一开始时,比较原始类型和引用类型是通过它们转换为二进制后,前面是否有三个 0 来区分的,三个全是 0 ,则是引用类型,三个不全为 0 ,则是引用类型。而null
在一开始就是用全 0 来表示的,因此就会将他认为是引用类型。
示例
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上,并返回一个布尔值,因此他可以准确地判断复杂引用数据类型,但是不能正确判断原始数据类型,因为原始类型中没有原型链。通常在面试过程中会要求我们去实现一个instanceof
,下面是实现方法:
let c = {};
function instance_of(L, R) {
L = L.__proto__;
R = R.prototype;
while (L !== R) {
if (L === null) return false;
L = L.__proto__;
}
return true;
}
console.log(instance_of(c, Array));
console.log(instance_of(c, Object));
实现一个instanceof
方法还是较为简单的,主要就是对原型进行递归或者循环比较就可以了。
请你说说你对js中原型的理解
因为前面提到了instanceof
是通过原型来判断是否是引用类型的,所以面试官就顺势往下问了原型的概念。
在js中每个对象拥有一个原型对象,当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。原型又分为显示原型
和隐式原型
。
显示原型
显示原型指的是函数身上自带的prototype
属性 ,通常可以将一些属性和方法添加在显示原型上,可供实例对象继承
隐式原型
隐式原型 __proto__
是对象这种结构上的一个属性,其中包含了创建该对象时隐式继承到的属性
原型链
既然讲到了原型,那就不得不讲一讲原型链了。原型链就是:创建一个实例对象时,实例对象的隐式原型===创建该对象的构造函数的显示原型,在js中,对象的查找规则是现在对象中查找,如果没有找到,再去对象的隐式原型上查找,顺着隐式原型一层层往上找,直到找到Object的原型对象,Object的原型对象的原型对象是null。这种查找规则就是原型链。
既然你提到了可以实现继承,那你讲讲你是如何用它实现继承的
首先在js中继承指的是,让一个子类可以访问父类的属性和方法,而通过原型链来实现继承的方法叫做:原型链继承,原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。实现方法如下:
function Car(color, speed) {
this.color = color;
this.speed = speed;
this.seat = [1, 2];
}
Truck.prototype = new Car('red', 200);
function Truck() {
this.container = true;
}
let truck = new Truck()
let truck2 = new Truck()
// truck.color = "white";
truck.seat = [5,6]
console.log(truck2);
在继承中有着以下几种方式:
- 原型链继承:
-
- 无法给父类传参
-
- 多个实例对象共用了同一个原型对象,存在属性相互影响
-
- 构造函数继承:
-
- 可以给父类传参
-
- 只能继承到父类身上的属性,无法继承父类原型上的属性和方法
-
- 组合继承:
-
- 既可以继承到父类的属性,也可以继承到父类原型上的属性和方法
-
- 但是存在父类构造函数被调用两次的问题,多造成了性能开销
-
- 原型式继承:
-
- 通过Object.create()方法创建一个新对象,然后将新对象的隐式原型指向父类的实例对象,子类无法添加默认属性
-
- 因为是浅拷贝,父类中的引用类型在子类之间共用了,所以会相互影响
-
- 寄生式继承:1. 同上
- 寄生组合式继承 :
-
- 通过Object.create()方法创建一个新对象,然后将新对象的隐式原型指向父类的原型对象,然后将子类的构造函数指向父类的构造函数
-
- 通过这种方式,既可以继承到父类的属性,也可以继承到父类原型上的属性和方法,而且父类构造函数只会被调用一次,不存在性能开销
-
- 但是存在父类的原型对象上的属性会被子类的原型对象共用的问题
-
通常来说,我们都是使用寄生组合式继承,因为寄生组合式继承,借助解决普通对象的继承问题的Object.create
方法,在几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式。实现方法如下:
Parent.prototype.say = 'hello'
function Parent(like) {
this.name = 'parent';
this.like = like;
}
Child.prototype = Object.create(Parent.prototype); //隐式继承了Parent.prototype的属性和方法
Child.prototype.constructor = Child; //显式继承了Parent的属性和方法
function Child(like) {
Parent.call(this,like);
this.type = 'child';
}
let c1 = new Child('coding');
console.log(c1);
你了解过ES6吗,讲讲var、let和const的区别
一、var
在ES5中,顶层对象的属性和全局变量是等价的,用var
声明的变量既是全局变量,也是顶层变量
注意:顶层对象,在浏览器环境指的是window
对象,在 Node
指的是global
对象
var a = 10;
console.log(window.a) // 10
使用var
声明的变量存在变量提升的情况
console.log(a) // undefined
var a = 20
在编译阶段,编译器会将其变成以下执行
var a
console.log(a)
a = 20
使用var
,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
var a = 20
var a = 30
console.log(a) // 30
在函数中使用使用var
声明变量时候,该变量是局部的
var a = 20
function change(){
var a = 30
}
change()
console.log(a) // 20
而如果在函数内不使用var
,该变量是全局的
var a = 20
function change(){
a = 30
}
change()
console.log(a) // 30
二、let
let
是ES6
新增的命令,用来声明变量
用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效
{
let a = 20
}
console.log(a) // a is not defined.
不存在变量提升
console.log(a) // 报错ReferenceError
let a = 2
这表示在声明它之前,变量a
是不存在的,这时如果用到它,就会抛出一个错误
只要块级作用域内存在let
命令,这个区域就不再受外部影响
var a = 123
if (true) {
a = 'abc' // ReferenceError
let a;
}
使用let
声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
最后,let
不允许在相同作用域中重复声明
let a = 20
let a = 30
// Identifier 'a' has already been declared
注意的是相同作用域,下面这种情况是不会报错的
let a = 20
{
let a = 30
}
因此,我们不能在函数内部重新声明参数
function func(arg) {
let arg;
}
func()
//Identifier 'arg' has already been declared
三、const
const
声明一个只读的常量,一旦声明,常量的值就不能改变
const a = 1
a = 3
// TypeError: Assignment to constant variable.
这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值
const a;
// SyntaxError: Missing initializer in const declaration
如果之前用var
或let
声明过变量,再用const
声明同样会报错
var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错
const
实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的,并不能确保改变量的结构不变
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // "foo" is read-only
其它情况,const
与let
一致
四、区别
var
、let
、const
三者区别可以围绕下面五点展开:
- 变量提升
- 暂时性死区
- 块级作用域
- 重复声明
- 修改声明的变量
- 使用
变量提升
var
声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
// var
console.log(a) // undefined
var a = 10
// let
console.log(b) // Cannot access 'b' before initialization
let b = 10
// const
console.log(c) // Cannot access 'c' before initialization
const c = 10
暂时性死区
var
不存在暂时性死区
let
和const
存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
// var
console.log(a) // undefined
var a = 10
// let
console.log(b) // Cannot access 'b' before initialization
let b = 10
// const
console.log(c) // Cannot access 'c' before initialization
const c = 10
块级作用域
var
不存在块级作用域
let
和const
存在块级作用域
// var
{
var a = 20
}
console.log(a) // 20
// let
{
let b = 20
}
console.log(b) // b is not defined
// const
{
const c = 20
}
console.log(c) // c is not defined
重复声明
var
允许重复声明变量
let
和const
在同一作用域不允许重复声明变量
// var
var a = 10
var a = 20 // 20
// let
let b = 10
let b = 20 // Identifier 'b' has already been declared
// const
const c = 10
const c = 20 // Identifier 'c' has already been declared
修改声明的变量
var
和let
可以
const
声明一个只读的常量。一旦声明,常量的值就不能改变
// var
var a = 10
a = 20
console.log(a) // 20
//let
let b = 10
b = 20
console.log(b) // 20
// const
const c = 10
c = 20
console.log(c) // Assignment to constant variable
请你谈谈你对深浅拷贝的理解,如何实现一个深拷贝
这个题目是常考题,基本问js的面试官都会问这个问题。首先我们都知道js中有原始类型
和引用类型
,而对于它们的储存方式来说,原始类型数据保存在在栈内存中,引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
浅拷贝
只拷贝一层对象,复制这一层对象中的原始值,如果有引用类型的话,就复制它的引用地址,常用的方法:Object.assign()
、 [...arr]
、 slice()
、 concat()
。
实现方法如下:
// Object.prototype.abc = 123
let obj = {
a: 1,
b: 2
}
// for (let key in obj) {
// console.log(key,obj[key]);
// }
console.log(obj.hasOwnProperty('a'));
// 判断对象上是否显示具有某个属性
// 实例对象的隐式原型是构造函数的显示原型
let newObj = Object.assign({}, obj)
console.log(newObj);
// Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
let obj2 = Object.create(obj) // 创建一个新对象,新对象的隐式原型是obj
console.log(obj2);
// Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
let arr = [1, 2, 3]
let newArr = [].concat(arr)
深拷贝
深拷贝指的是:层层拷贝,所有的类型的属性值都会被复制,原对象的修改不会影响拷贝后的对象。常用的方法如下:
- 递归
- JSON.parse(JSON.stringify(obj)) 但是无法处理函数、正则、Date等特殊对象,还有循环引用的问题
- structedClone() 无法处理函数、正则、Date等特殊对象,还有循环引用的问题
- MessageChannel() 有两个线程,一个线程发送消息,一个线程接收消息,这样就可以实现深拷贝,并且可以实现循环引用的问题
使用递归和JSON.parse(JSON.stringify(obj))的实现
let obj = {
a: 1,
b: {
c: 2
}
}
// let newObj = JSON.parse(JSON.stringify(obj))
let obj2 = deepCopy(obj)
function deepCopy(obj) {
let newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) { //hashOWNProperty是用来判断一个对象是否有你给出名称的属性或对象。如果有则返回true,没有则返回false。
if (typeof obj[key] !== 'object' || obj[key] === null) {
newObj[key] = obj[key];
} else {
newObj[key] = deepCopy(obj[key]);
}
}
}
return newObj;
}
console.log(obj2);
使用 structedClone()和MessageChannel()来实现
let obj = {
a: 1,
b: {
c: 2
},
d:undefined,
}
// const obj2 = structuredClone(obj);
// structuredClone方法是一个异步方法,所以我们需要使用Promise来获取返回值,返回的是一个新的对象,不会受原对象的影响。
function deepClone(obj) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel();
port1.postMessage(obj);
port2.onmessage = (msg) => {
resolve(msg.data);
}
})
}
let obj2 = null;
deepClone(obj).then((res) => {
console.log(res);
obj2 = res;
obj.b.c = 3;
console.log(obj2);
})
讲讲==
和===
的区别
我所遇到的面试中被问到js的问题并不多,好像现在是更注重问项目和vue了,反而八股文问的不多。
CSS系列
在面试中,问CSS的真的很少很少,我也就遇到过两家问css的考题的。
你在项目中有实现过水平垂直居中吗,你是如何实现的
在项目中,我对于水平垂直居中是通过使用 display: flex + justify-content: center + align-items: center
来实现的。实现水平垂直居中的方式除了以上这种,还有如下几种:
- position: absolute + transform: translate(-50%, -50%) || margin负值
- display: grid
- table: text-align: center + vertical-align: middle(子容器不能是块级)
- margin(已知宽高)
说说flexbox(弹性盒布局模型)
Flexible Box
简称 flex
,意为”弹性布局”,可以简便、完整、响应式地实现各种页面布局。flexbox是一种一维的布局模型,可以实现弹性布局,可以简便完整响应式的实现页面布局,容器中默认存在两根轴:主轴和交叉轴.默认x轴为主轴,y轴为交叉轴,可以使用flex-direction属性来改变主轴的方向。
特点:
- 可以控制子元素在主轴上的对齐方式
- 可以控制子元素在交叉轴上的对齐方式
- 可以控制子元素 缩放比例、排列方式、换行方式、间距等
- 可以控制子元素在主轴上的对齐方式
- 可以控制子元素在交叉轴上的对齐方式
- 可以控制子元素 缩放比例、排列方式、换行方式、间距等
容器属性有:
- flex-direction
- flex-wrap
- flex-flow
- justify-content
- align-items
- align-content
flex-direction
决定主轴的方向(即项目的排列方向),属性对应如下:
- row(默认值):主轴为水平方向,起点在左端
- row-reverse:主轴为水平方向,起点在右端
- column:主轴为垂直方向,起点在上沿。
- column-reverse:主轴为垂直方向,起点在下沿
flex-wrap
弹性元素永远沿主轴排列,那么如果主轴排不下,通过flex-wrap
决定容器内项目是否可换行,属性对应如下:
- nowrap(默认值):不换行
- wrap:换行,第一行在上方
- wrap-reverse:换行,第一行在下方
默认情况是不换行,但这里也不会任由元素直接溢出容器,会涉及到元素的弹性伸缩
flex-flow
是flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrap
justify-content
定义了项目在主轴上的对齐方式,属性对应如下:
- flex-start(默认值):左对齐
- flex-end:右对齐
- center:居中
- space-between:两端对齐,项目之间的间隔都相等
- space-around:两个项目两侧间隔相等
align-items
定义项目在交叉轴上如何对齐,属性对应如下:
- flex-start:交叉轴的起点对齐
- flex-end:交叉轴的终点对齐
- center:交叉轴的中点对齐
- baseline: 项目的第一行文字的基线对齐
- stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度
align-content
定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用,属性对应如下:
- flex-start:与交叉轴的起点对齐
- flex-end:与交叉轴的终点对齐
- center:与交叉轴的中点对齐
- space-between:与交叉轴两端对齐,轴线之间的间隔平均分布
- space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍
- stretch(默认值):轴线占满整个交叉轴
结语
以上就是本人在这几个星期面试过程中所遇到的js
和css
的相关问题。在面试中,对于vue和项目相关的问题会更多一点,反而现在八股文问的不是那么多了,因此我们还是要将自己的项目整理清楚,这样才能有充分的准备去应对面试。
如果您也和我一样准备春招,只为TOP20大厂
,欢迎加我微信shunwuyu
,一起交流面经,一起屡败屡战。欢迎大家一起来探讨面试相关的经验或者进行一些技术的交流。