记一次面试经历

236 阅读11分钟

今天经历了一次比较正式的面试,说实话面试过程是在难受,自身做的准备也不充分,感觉像裸考,从开始投简历到现在面试大概来回就用了1周时间开始复习前端...基础也很不过关,不过面试过程还是值得让我好好学习的!

下面是一些谈及的面试点,回答就不放上了...基本上没答上几句,但就想跟着这几个面试题目+笔试题,顺便把相关的知识点复习一下,主要问了CSS、JS、React这三大块的一些常见相关知识点(下面是个人总结,如果有什么错误之处还请各位多多指出!):

CSS

1. 水平和垂直布局的方法

面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高) 参考上述链接,我个人比较常用的3种手段是:

  • Flex布局 通过设置justify-content: center 和 align-item: center来实现居中;

  • absolute + transform 一般来说,Flex布局更像是用作整体的布局调整,而在平常一些子元素需要实现居中时,absolute也是我比较常用的手段(此时父元素需要是相对定位的状态)。

position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
  • 宽度确定的情况下,直接设置margin: auto。

JS

1. js的基本数组类型有哪些?

js的数据类型一共有7种,其中包括6种原始数据类型和1种复杂数据类型:

6种原始数据类型如下:

  • null
  • undefined
  • number
  • string
  • boolean
  • symbol

1种复杂数据类型:

  • object

在js中,除了原始数据类型外,其他都是对象类型了。

原始数据类型中存储的值,而复杂数据类型存储的是地址(指针)。当你创建一个对象时,计算机会在内存中开辟一个空间来存放值。为了让我们找到这个空间,这个空间就拥有了一个地址(指针)。

区分原始数据类型与复杂数据类型的赋值:
// 按值访问
let xPrimary = 1;
let yPrimary = xPrimary;
yPrimary = 2;
console.log(xPrimary); // 1 
console.log(yPrimary); // 2

// 按引用访问
let xObj = {};
let yObjy = xObjy;
yObjy.name = “test";
console.log(xObj); // { name: 'test' }
console.log(yObj); // { name: 'test' }
ECMAScript中所有函数的参数都是按值传递的。 ——《JavaScript高级程序设计》
此处目前仍不是很理解,一开始打算按照赋值的例子分按值和按引用两种情况进行讨论,后来查阅资料才发现红宝书有这样一解释~
// Test1
function primaryTest(primaryVal) {
    primaryVal = 2;
    console.log(primaryVal); // 2

    return primaryVal;    
}
let val = 1;
primaryTest(val);
console.log(val); // 1

// Test2
function refTest(refObj) {
    refObj.name = 'test';
    console.log(refObj); // {name: "test"}

    return refObj;
}
let obj = {};
refTest(obj);
console.log(obj); // {name: "test"}

// Test3
function valTest(obj) {
    obj.name = 'test';
    console.log(obj); // {name: "test"}

    obj = new Object();
    obj = {
        name: 'demo',
        age: 18
    }
    console.log(obj); // {name: "demo", age: 18}
    
    return obj;
}
let objTest = {};
valTest(objTest);
console.log(objTest); // {name: "test"}

参考JavaScript深入之参数按值传递和红宝书的解释,我目前的理解是:

对于函数传参,如果传入类型是原始数据类型,则是按值传递;

关键是如何理解传入参数是对象的时候呢?当函数传入的参数是对象时,其实传入的对象地址的副本,所以它们会指向同一个地址,而当函数对这个对象进行修改时(即修改或添加属性),则会共同修改了同一个对象;可是当给对象重新初始化时(即重新赋值原始数据类型或者重新进行new操作),就相当于对象的指向发生了变化,此时它与副本的联系已经断开,所以后面的操作都与原对象无关(如Test3所示)。

要获取变量值当前的数据类型,可使用typeof运算符。

2. typeOf的用法?它不可以用来判断什么?

typeof对于原始数据类型来说,除了null都可以显示正确的类型。

console.log(typeof undefined); // undefined
console.log(typeof "stirng"); // string
console.log(typeof 1); // numver
console.log(typeof true); // boolean
console.log(typeof Symbol()); // symbol
console.log(typeof null); // object

Q: 为什么null会显示object

JS存在的一个悠长的Bug, 在JS最初的版本中使用的是32位系统,为了性能考虑使用低位存储变量的类型信息,000开头代表对象,而null表示全为零,因此被误判为object

typeof对于对象来说,除了函数都会显示object

3. this对象

参考如下文章:

JS基礎篇之作用域、執行上下文、this、閉包

从这两套题,重新认识JS的this、作用域、闭包、对象

深入理解 JS 中的 this

掘金小册 —— 前端面试之道


在学习this之前,其实它涉及的知识点很多,所以总让人有种一直学不会的感觉,下面会先讲解一些我认为相关的前置知识点。

作用域

js中的作用域是词法作用域,是由函数声明时所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标签符的访问规则; 作用域链本质是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是作用域概念的延伸。作用域链定义了变量在当前上下文访问不到的时候如何沿用作用域链继续查询的一套规则。

作用域分为三种:全局作用域、函数作用域和块级作用域,其中块级作用域概念是在ES6中出现的,当使用let、const关键字声明变量时,就会产生块级作用域,此时不会发生变量提升。

/**
 * 这里一共存在三个作用域,分别是全局作用域、fn函数作用域和innetFn作用域。
 * 其中内层作用域可以访问外层作用域,反之则不可。
 */
var a = 0;
function fn() {
    var b = 1;
    
    function innetFn() {
        var c = 2;
        console.log(a,b,c);
    }
    console.log(a,b);
//     console.log(c); // ReferenceError: c is not defined

    return innetFn();
}
fn();
console.log(a);
// console.log(b); // ReferenceError: b is not defined
// console.log(c); // ReferenceError: c is not defined
执行上下文

我所理解的执行上下文就是:函数进行时所处的环境。

它和作用域的区别就像是作用域是静态的,它是在代码编译时已经形成的一个作用域,是不可变的;而执行上下文则是一个动态的作用域,它是根据当前代码的执行环境所决定的。

其中,执行上下文分为两个阶段:创建阶段和执行阶段

创建阶段会进行如下操作:

① 创建this对象;

② 创建词法环境;

③ 创建变量环境;

执行阶段会进行如下操作:

① 分配变量;

② 执行代码.

闭包

当函数能够记住并访问所在的词法作用域时,就产生了闭包。

下面主要通过一个例子说明:

function fn() {
    var b = 0;

    return {
        add() {
            b++;
            return b;
        },
        minus() {
            b--;
            return b;
        }
    }
}
var bVal = fn();
console.log(bVal.add()); // 1
console.log(bVal.add()); // 2
console.log(bVal.minus()); // 1
console.log(b); // ReferenceError: b is not defined

从例子中,可以看到,通过fn()函数就可以对该函数内部的变量进行操作,这本来是一个在函数作用域内的变量,而在全局作用域中按理是无法进行访问的,正如最后那个console.log(b)会报错,但如果通过该函数,则可以正常访问b了,因此此时就形成了闭包。

call、apply、bind函数

这三个函数都可以用于指定改变函数中this的指向。

call(object, arg1, agr2), 其第一个参数是函数中this重新指向的对象,剩下的参数是传入该参数的形参。 call允许在一个对象上调用该对象没有定义的方法,并在高这个方法可以访问该对象中的属性。

var person = {
    name: 'Tom',
    age: 20,
    say: function() {
        console.log('Hi, this is person',this.name);
    }
}

function fn(name) {
    console.log('post Params: '+ name); // post Params: Stella
    console.log('I am ' + this.name); // I am Tom
    this.say(); // Hi, this is person Tom
}

fn.call(person, 'Stella');

apply(object, [arg1, arg2]), apply方法的第一个参数是函数中this重新指向的对象,第二个参数数组是传入该函数的形参;和call的唯一区别是第二参数的传递形式是数组

function fn(x, y, z){
    console.log(x, y ,z);
}
// apply() 方法接收的参数是数组形式,但是传递给调用的参数时调用的是参数列别的形式传递的
fn.call(null, [1, 2, 3]); // 1 2 3 ,这里等同于windows对象调用fn方法并使用参数[1, 2, 3]

bind(object, arg1, arg2), bind方法是ES5新增的一个方法,传参和call方法一致。与call、apply方法的区别是,call和apply方法会对目标函数进行自动执行,并返回一个新的函数。call和apply无法在事件绑定函数中使用。而bind弥补了这个缺陷。

需要注意的是,一旦函数通过bind绑定了有效的this对象,那么在函数执行过程中this会指向该对象,即使使用call、apply也不能改变this的指向。

new运算符

引用红宝书的一段话:

使用new操作符调用构造函数,实际上会经历一下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象
箭头函数

箭头函数是ES6新增部分,引用MDN的介绍:

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this、argument、super或new.target。这些函数表达式最适合用于非方法(non-method functions),并且它们不能用作构造函数。

method: A method is a function which is a property of an object.

对象属性中的函数被称为method,那么non-method function就是指不被用作对象属性中的函数了。下面看一个例子:

var obj = {
    x: 10,
    fn1: () => console.log(this, this.x),
    fn2: function() {
        console.log(this, this.i)
    }
}
obj.fn1(); // window undefined
obj.fn2(); // {x: 10, fn1: ƒ, fn2: ƒ} 10

这里我的理解是箭头函数没有this,导致这里的指向不清晰,所以箭头函数不适合用于对象的方法。


this

回到this,很多文章都会提及到,this指向调用它的对象。它简单可以分为四种情况(这里讨论的是非严格模式下):

  1. 函数本身直接调用,此时this指向windows;
  2. 通过对象调用,此时this指向对象本身;
  3. 利用call、apply、bind这类函数将this指向第一个参数;
  4. 通过new实例化一个对象instance,此时的this指向实例。
let fn = function() {
    console.log(this.name);
}
let obj = {
    name: 'objName',
    fn
}
fn();
obj.fn();
fn.call(obj);
let instance = new fn();

下面这个例子主要针对涉及箭头函数时this指向:

var obj = {
    fn1: function() {
        console.log('fn1: ', this);
    },
    fn2: () => console.log('fn2: ', this),
    fn3: function() {
        return function() {
            console.log('fn3: ', this);
        }
    },
    fn4: function() {
       return () => console.log('fn4: ', this);
    }
}
obj.fn1(); // obj
obj.fn2(); // Window
obj.fn3()(); // Window
obj.fn4()(); // obj

fn1: 即对象的调用,因此this指向对象;

fn2: 箭头函数没有this指向,因此this指向Window;

fn3: 这里的fn3属于高阶函数(一个函数将作为参数或者返回值的函数),这里分步走如下:

var func = obj.fn3()
func()

从而导致最终调用函数的执行环境的window。因此this此时指向Window。

fn4: 这里的箭头函数被外层function包裹,因此此时this指向该对象。

关于箭头函数: 如果箭头函数本身没有外层函数,那么此时则会指向Window; 而如果它被对个函数包裹,则会沿着作用域链寻找离它最近的作用域作为指向。

4. 数组常用的方法

有点多,后续补充...

React

1. 纯函数

对于纯函数,主要有以下两条性质:

  • 对于相同的参数,其返回值是相同的;
  • 不会引起任何副作用.(什么是副作用?例如:修改了全局对象或通过引用传递的参数) Ex:
/**
 * 对于参数radius = 10 和 PI = 3.14,始终会得到相同的结果:314.0;
 * 对于radius = 10 和 PI = 42,总是得到相同的结果:4200.
 */
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
calculateArea(10, PI);  // return 314.0

2. React生命周期

以下内容来源React官网 组件的生命周期,如下图所示:

组件的生命周期分为三个阶段,分别是:挂载时、更新时和卸载时。

  • 挂载

    当组件实例被创建并插入DOM中时,其生命周期调用顺序如下:

    • constructor()
    • static getDerviedStateFromProps()
    • render()
    • componentDidMount()
  • 更新

    当组件的props或state发生变化时会触发更新。组件更细你的生命周期调用顺序如下:

    • static getDerviedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  • 卸载

    当组件从DOM中移除时会调用如下方法:

    • componentWillUnmount()
  • 错误处理

    当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

    • static getDerviedStateFromError()
    • componentDidCatch()

3. React-Hook