页 面 正 在 赶 来 的 路 上。

前端常备知识总结(一)——基础

javascript

 唐益达

date:  2019-03-04 23:59


view 373 comment 2

前端常备知识总结(一)——基础


写在前面

最近过年前辞职了,所以时间空下来了,修整了半个多月的样子。还是不能放松啊。

之前的React试写一个先放一放,得准备一下过年后去杭州面试的事情。顺便絮叨一下现在前端,我也是意外加入前端这个行业的,如果让我回到大一,我肯定会好好学习Python,ml和AI,哈哈因为它们太酷炫了,而且在工作后越来越明白学习的珍贵,但是那时候没有明确努力的方向嘛,不过也无妨,现在前端我也很喜欢,享受做出一个作品的感觉。

简述前端

前端这个行业说门槛高也不高,低也不低。不高在于起点低,很多人来个html+css就可以说入门前端了,不低在于精通前端还是有点难的,需要大量的知识和实践,网络基础、计算机基础以及操作系统知识、JS基础,跨域,移动端,数据结构等等知识。

不断学习是现在前端的标签,但是很多人又抱怨,什么node之父又搞了一个Deno,哪还有时间学,但其实很多语言都是共通的,我们不应该抱怨语言的更替,语言的新起出现都有它的应用需求,我们需要做的是精通一门语言,然后这样学习其他语言的时候有很多相似的地方(比如数据类型,set和map等等)都能比很多人更快的理解,就比如我第一个学的语言是C语言,后面学了java,而不是JS,但是因为有c和java的基础所以学起js来也不会特别费劲。

在我看来,语言只是一门工具,每种语言有它适合的应用场景,无高低贵贱之分,我们要做的,就是在你工作领域的需求下,学好你需要的那门语言!

正文

说了这么多,不愿意看我的心得的,可以直接跳到这一章节来。接下去的文章都是我从网上一些大厂面试合集的题目,我作以梳理,有些简单的就不放上去了,可以深入研究以及少见的问题,可以加以记录。

js基础

js和css一同实现一个持续的动画效果。

比如一个很简单的:正方形从0的边长匀速变到300px大小的效果:

<div class="div-container test-animate"></div>
<div class="div-container" id="test-div"></div>

上述节点第一个是css实现,第二个是js实现

.div-container {
    background-color: red;
}
.test-animate {
    animation: 5s test infinite linear; // infinite表示循环,linear表示线性变化即匀速
    margin-bottom: 100px;
}
@keyframes test {
    from {
        height: 0px;
        width: 0px;
    }
    to {
        height: 300px;
        width: 300px;
    }
}

可以实现, 那么用js如何实现呢?很多人肯定是用setinterval持续执行来实现效果,其实殊不知js已经提供了一个优化的方法requestAnimationFrame方法来实现优化的动画,mdn的链接,可以看到这是浏览器提供的,除了bug的IE,10版本以下都不支持(需要另写),其余都几乎支持,上述动画是做一个0px到300px边长的动画,持续时间是5s,匀速,用js实现:

var count = 0;
var ele = document.getElementById('test-div'); // 找到节点
var afun = function (){
    if (count >= 300) count = 0; // 如果边长宽度大于300,那么就重头开始
    ele.style.height = count + 'px';
    ele.style.width = count + 'px';
    count ++;
    window.requestAnimationFrame(afun); // 重要,这里递归循环执行,间隔1/60秒执行一次
}
window.requestAnimationFrame(afun);

我们知道人眼能识别帧数不卡大约是60帧,也就是说每秒执行变化60次,window.requestAnimationFrame就是遵循这个理念,你递归循环,是1/60秒时间间隔执行一次。300px的变化自然就需要5s,所以这个实现和css的实现效果是一模一样的。

那么用js实现的好处是什么呢?你从上面的代码可以看到count++的意思就是每1/60秒执行一次,也就是说精确的点非常的准。其次就是扩展性较好,能够实现更复杂的动画,同时因为是浏览器封装过的原生方法,自然性能也是很好的,更优化的重排,更流畅的效果。

最重要的一点,就是他们的区别:你会发现,你切换tab页或者缩小浏览器或者切换别的程序,css的动画会继续执行(在后台消耗内存),而js实现的动画会暂停!,优化了过度渲染的缺点,防止后台过度消耗性能,这是js执行这个方法独特的地方。在你需要停止动画的时候,他还能利用cancelAnimationFrame方法来清除动画。

闭包实现点击列表输出下标(可动态)

<ul id="ul-test"></ul>

这个问题出现得比较普遍,为什么我还要放上去?因为很多人都看过这个问题,但是实际让你从零写,可能会存在一些问题。

原版是这样,循环输出一个列表li,然后通过点击事件实现点击哪个li就输出它的序列下标。通过下面程序执行(需要大家知道原生的创建方法,用得比较少)

for(var i = 0;i < 5; i++) {
    var ele = document.createElement('li');
    var textNode = document.createTextNode(i);
    ele.appendChild(textNode);
    ele.onclick = function () { // 这里实现重要逻辑
        alert(i)
    }
    document.getElementById('ul-test').appendChild(ele)
}

你会发现这个是错的,无论你点击哪个li,输出都是5。这是因为每次循环的i都是指向同一个i地址,也就是说i是一个具体累加的过程,并不是每次循环的i都是独立的,而i循环执行完数值是5,那么既然都是指向同个地址,最后你无论点击哪个li都是输出5。

首先用let可以实现:

for(let i = 0;i < 5;i++){...}

在循环内,每次循环的i都是独立的变量,不存在指向同一个地址。当然,我们也需要知道实现闭包的情况实现这种效果,闭包会临时保存变量不会被销毁(注意内存泄漏)

ele.onclick = (function () {
    var j = i; // 这里单独引用出来
    return function () { // 返回一个function
        alert(i)
    }
})() // 立即执行

闭包不展开了,js的基础,不懂得再多看一下书。

this指向问题

面试中也会经常提到this指向的问题。this指向一般有三种情况:1、**处于函数中,指向是当前函数的运行环境。**2、如果此函数被某个对象当做属性引用,那么指向这个对象,3、匿名函数也处于全局环境,在匿名函数中指向一般都是全局window

我们来看几个例子:

name = 'window'; // 全局变量
var obj = {
    name: 'my object',
    getName: function () {
        console.log(this.name);
    }
}
obj.getName(); //输出my object

上述输出为my object,我们可以这样看,this在函数getName中,而getName是属于上述第二种情况,所以this指向此obj,输出为my object

name = 'window'; // 全局变量
var obj = {
    name: 'my object',
    getName: function () {
        return function () {
            console.log(this.name);
        }
    }
}
obj.getName()(); //输出window

上述输出为window,为什么呢?因为obj.getName()输出的是匿名函数function () {console.log(this.name);},相当于执行了匿名函数,那么为什么这个匿名函数返回的是全局变量呢?我们聚焦于getName这个属性:

getName: function () {
    return function () {
        console.log(this.name);
    }
}

上述getName函数中,返回的是一个匿名函数,我们都知道,函数在被创建的时候,都会自动创建两个固有属性:thisarguments,前者就是我们讨论的this,后者是参数集合。

那么在这里,很明显是属性方法getName里面又套了一个匿名函数,那么为什么匿名函数的this没有指向obj的name中去呢?很明显getName函数和匿名函数是两个函数,有各自独有的this属性,而作用域链使我们知道,嵌套函数是无法直接访问上层函数的this值的。

那么在这里被执行后,匿名函数被剥离出来后执行的时候,运行环境是window,所以this指向的是函数运行环境window。

那么如何访问上层函数的变量呢?

getName: function () {
    var that = this; // 在这里用变量保存当前函数的this
    return function () {
        console.log(that.name); // 可以访问到了
    }
}

看看更复杂的情况:

name = 'window';
function test() {
	this.name = 'changed'
    var obj = {
        name: 'my object',
        getName: function () {
            return function () {
                console.log(this.name);
            }
        }
    }
    obj.getName()(); //输出changed
}
test();

这个显而易见是getName执行后在test环境中,而根据作用域链,test又在window环境中,所以最终还是只想window的全局环境。this.name = 'changed'代码在这里和name = 'changed'是同一个本质,name的值已经被改变了。

而需要注意一种情况:

name = 'window';
function test() {
	this.name = 'changed'
    this.getName = function () {
        console.log(this.name);
    }
}
var t = new test();
t.getName(); //changed

这种情况下输出的是changed,因为在new一个对象的同时,this的指向也跟这个新对象绑定了,指向是这个新对象,是上述理由的第二种情况(不确定的可以看js高级程序设计工厂模式那一章节)。

那如果把上述的this.getName改成如下的形式。

name = 'window';
function test() {
	this.name = 'changed'
    this.getName = function () {
        return function () {
            console.log(this.name);
        }
    }
}
var t = new test();
t.getName()();

那么输出是什么呢?那么再举一反三,把上述的test的函数放在一个对象Object的属性环境中,运行结果又是什么呢?大家可以试试。

上述情况下,你在文件头加入'use strict'使用严格模式,结果有没有变化?一定要注意这些细节。

apply和call下的this指向问题

this指向问题在js中较为灵活,一般所有匿名函数的this都指向全局对象的window,而this指向的改变往往发生在实例化new或call和apply执行后,这在面试中都是较为被面试官考察的问题

至于call和apply的用法我不赘述了,博客里有相关内容,我们这里举一个简单的例子诠释改变this的过程。

var name = 'window';
function changeName (name) {
    console.log(name);
    console.log(this.name);
    console.log(this);
}
changeName('function'); // 输出分别是 function && window && [object Window]
var obj = {
    name: 'I am obj'
};
changeName.call(obj, 'I am argument') 
// 输出分别是 'I am argument' && 'I am obj' && {name: "I am obj"}

可以看到因为call的作用改变了this指向(通常是第一个参数表示this的指向,这里指向obj = { name: 'I am obj'});详见call方法的mdn,如果还想深入了解,我推荐这篇博文:javascript技术难点(三)之this、new、apply和call详解。相似的还有bind,这里不多赘述,提醒一下需要注意。

说说模块标准

js模块标准通常有5种:IIFE,AMD,CJS,UMD,ES标准。

  1. IIFE是立即执行函数标准,解决了变量污染,块级作用域的作用。

  2. AMD是从requireJS发展而来,解决了模块化依赖的问题,和cjs一样是社区指定的,并不官方(用在浏览器端)。

  3. CJS(CommonJS)通常是nodeJS用得最多,用在服务器端的模块化机制(同步的),通过module.exports和require来实现模块化。

  4. UMD是一个兼容版本,不属于正规规范,是IIFE+ amd + cjs的结合体。

  5. ES是ECMAScript是Javascript官方的规范,以前是ES5(ECMAScript 2015),现在通常是ES6。常用import和export

现在用得最多的就是module.exports/requireexport/import了,两者的区别简单说前者在于模块的整体引用加载,通常是整一个对象的引用,无法动态加载,且是同步的,而后者在于是ES规范,可以动态加载,可以按需引用。

两者本质区别在于,require/exports运行时加载执行,且本质是一个对象赋值变量的过程。import就比较高大上,因为是官方的,所以它的本质是在编译时引用,确定模块关系,是一个解构的过程。

变量提升

js的var语句是存在变量提升的情况的,什么是变量提升,就是在你在使用后声明的变量的时候,变量通常会被偷偷提升到方法体的顶部执行(不管是否在这之前声明)。举个例子更加形象:

console.log('result is ' + name) // result is undefined
var name = 'I am name';

上述情况并不会报错,而是把name这个变量偷偷提升到方法体顶部执行变成了undefined(未定义),这其实是很不规范的,但是这就是存在的。

由于变量提升的存在,上述实际执行顺序是这样的:

var name;
console.log('result is ' + name) // result is undefined
name = 'I am name';

我们用es6的let,const语句可以有效避免这种情况的发生。这就是为什么要推出es6的原因(更规范化)。

判断两个对象是否相等

众所周知,判断两个对象是否相等难点在于如何判断属性相等。我看了一些资料以后,总结出一个简单版本的判断,如果想考虑多种情况的后面再说。

function isObjectValueEqual(a, b) {
	// 假定都是对象Object,getOwnPropertyNames获取他们的属性名返回一个数组
    var aProps = Object.getOwnPropertyNames(a);
    var bProps = Object.getOwnPropertyNames(b);

   	// 如果对象属性的数组长度不同,说明两个对象肯定不相同
    if (aProps.length != bProps.length) {
        return false;
    }
	
    // 循环对象属性
    for (var i = 0; i < aProps.length; i++) {
        var propName = aProps[i];
		// 如果对象属性的值还是对象,进行递归调用自身,再深度判断
        if (a[propName].constructor === Object) {
            return isObjectValueEqual(a[propName], b[propName]);
        }
        
        // 这里做简单的判断
        if (a[propName] !== b[propName]) {
            return false;
        }
    }
    
    return true;
}

通常简单的情况,我们还可以通过序列化对象成字符串进行比较:

JSON.stringify(obj1) === JSON.stringify(obj2);

但是了解过的人都知道,仅仅这样肯定是不够的:

  1. NaN不等于它自身
  2. 'string' 不等于new String('string')
  3. 1 / +0不等于1 / -0(特殊情况)

等等很多方面都没有考虑到,想要深究的可以看underscore的eq源码或者这篇博文:JavaScript专题之如何判断两个对象相等

一句话实现去重

利用Set结构中没有重复元素的特性:

[...new Set([1,2,3,1,2)]

js的__ proto__ ,prototype和constructor

这个是基础,但是平时用得少,如果你不知道,那基础可就需要补补课了。

创建的函数都有一个prototype属性,它是一个指针,指向一个对象,而这个对象里的属性方法或者属性值,是由此构造函数创建所有实例可以有共享的属性与方法

__ proto__属性是存在于每个对象(万物皆对象)中,确切的说,是在对象的共享对象(prototype)中的属性,它的作用是提供访问此对象的原型对象的作用

那么上面是什么意思呢?我们来看一个例子:

var Person = function (name) {
    this.name = name
};
var mike = new Person('mike');
console.log(mike.__proto__); // {constructor: ƒ}
console.log(Person.prototype); // {constructor: ƒ}
console.log(mike.__proto__ === Person.prototype) // true

可以看到实例mike__proto__可以访问到mike的原型对象Person.prototype,因为他们本质是相同的:mike.__proto__ = Person.prototype这是实例化中进行的操作。

再往上的层级找,Person的原型对象是谁呢?其实就是Function了

console.log(Person.__proto__); 
// ƒ () { [native code] }

其实上述结果就等于:

Person.__proto__ === Function.prototype // true

也就是说我们的Person函数的老爸(原型对象)就是Function

我们把以上的__proto__prototype共同配合所导致的现象,叫做原型链。如果想深入了解,建议看《JAVASCRIPT高级程序设计》的“原型模式”章节,或者看这篇博客

不过__proto__也不建议被写在代码中,mdn中已经标明准备废弃了,即使浏览器支持。可以用最新的ES6的Object.getPrototypeOf/Reflect.getPrototypeOfObject.setPrototypeOf/Reflect.setPrototypeOf

constructor指向的是此对象的构造函数。

JS中的继承

  1. 原型链继承
function Father () {
    this.name = 'father'
}

function Son () {
    
}
Son.prototype = new Father(); // 这里实现原型链继承
Son.prototype.getName = function () {
    return this.name;
}
var s1 = new Son();
console.log(s1.getName()); // father

缺点在如果第一个实例改变了属性值后,会导致共享属性改变:

function Father () {
    this.name = ['father']
}

function Son () {}
Son.prototype = new Father();
var s1 = new Son();
console.log(s1.name); // ["father"]
s1.name.push('new insert');
var s2 = new Son();
console.log(s2.name); // ["father", "new insert"]
  1. 借用构造函数继承
function Father (name) {
    this.name = name
}

function Son () {
    // 这里继承
    Father.call(this, ['arguments']);
}
var s1 = new Son();
console.log(s1.name); // ["arguments"]
s1.name.push('new insert');
console.log(s1.name); // ["arguments", "new insert"]
var s2 = new Son();
console.log(s2.name) // ["arguments"]

解决了上述继承带来的缺点。我们利用Father.call(this);代码利用了call或者apply将以后即将创建的Son实例提供了Father函数的环境。还可以带参数过去(通过this或者apply

结合上述两种方式——组合继承,也是融合了两者的优点,下文有介绍到。

  1. 原型式继承
// 基于原有对象创建新对象
function createObject(o) {
	var F = function () {}    
    F.prototype = o;
    return new F();
}

var father = {
    name: 'son1',
    age: 12,
    list: ['1','2','3']
};
var son1 = createObject(father);
console.log(son1.list); // ['1','2','3']
son1.list.push('4');
var son2 = createObject(father);
console.log(father.list); // ['1','2','3','4'] 改变了原来的值

可以看到也有上面类似的问题:属性不被father对象独享,而是他的两个son共享的,在son中改变值也会同时改变其他副本。ES标准为了规范化继承,推出了Object.create()方法,其实就是这里的createObject()

在你想创建一个与原对象有相似结构的情况下,不想那么兴师动众,那么这是一种非常好的方法继承。

  1. 寄生式继承

基于上面的代码:

function _create (o) {
    var o_copy = createObject(o);
    o_copy.sayHi = function () {
        alert('hello');
    }
    return o_copy;
}
var son3 = _create(father);
son3.sayHi(); // alert "hello"

有了自己定义的sayHi方法。

  1. 寄生组合式继承(最优解)

综合以上,寄生+组合式继承是最好的解,话不多说,直接上代码:

先看一个组合继承(构造函数+原型链形式):

i = 0;
function Father (name) {
    this.name = name
    this.list = [1,2,3,4]
    i++;
}

function Son (name) {
    // 这里继承 (执行+1),每次new的时候执行一次
    Father.call(this, name);
}

// (执行+1)
Son.prototype = new Father(); 
Son.prototype.constuctor = Father; // 指向构造函数
Son.prototype.sayName = function () {
    return this.name
}
var s1 = new Son('参数1');
console.log(s1.sayName()); // 参数1
s1.list.push(5);
console.log(s1.list); // (5) [1, 2, 3, 4, 5]
console.log('原型构造函数执行了', i, '次'); // 注意这里!!!!是2次!!!
var s2 = new Son('参数2');
console.log(s2.sayName()); // 参数2
console.log(s2.list) // (5) [1, 2, 3, 4]
console.log('原型构造函数执行了', i, '次'); // 注意这里!!!!是3次!!!

可以看到,我们创建了两个实例s1s2但是原型构造函数却执行了3次,也就是说多了一次执行

为了解决这种尴尬的事,我们用上面的寄生方式+现在的组合方式=寄生组合方式来实现最优的寄生组合式继承。

function createObject(o) {
	var F = function () {}    
    F.prototype = o;
    return new F();
}

function inheritPrototype (_son, _father) {
    var prototype = createObject(_father); // 新的对象
    prototype.constructor = _father;
    _son.prototype = prototype;
}

把之前代码中的两句改成如下:

i = 0;
//.....这里省略
// (执行+1)
inheritPrototype(Son, Father);
Son.prototype.sayName = function () {
    return this.name
}
var s1 = new Son('参数1');
console.log(s1.sayName()); // 参数1
var s2 = new Son('参数2');
console.log(s2.sayName()); // 参数2
console.log('原型构造函数执行了', i, '次'); // 是2次

可以总结出来,最终的寄生组合方式是现在继承的最优解,还想深入了解的,可以专门查询一下js的基础。

后记

先写到这里….未完待续,因为整理东西实在太麻烦了=。=。

上述我不太清楚的大概是继承这一块,所以继承这一块整理得比较多,当然ES6也很重要,还有一些什么作用域的问题太基础了,我这边就不一一解释了。

后续会继续整理各个前端面试中可能被问到的问题,有基础也有提高的,后续会更新上去。

至于前端中的浏览器BOM与CSS,缓存,重排与网页性能表现,还有各个譬如gulp,webpack,rollup等等打包软件,还有框架vue与raect,后续会再写个《前端常备知识总结——浏览器》和《前端常备知识总结——框架(react,vue与各个打包软件)》,尽请期待…...





评论列表 (共2条评论)
  • T.T尴尬,似乎没人评论......
  • {{comment.nickname}}  博主{{commentList.length-$index}}楼  {{comment.date}}
    引用 {{comment.responseName}}发表时间:{{comment.responseTime}}
    {{comment.response}}
精选文章