数据类型
Javascript
世界里将数据类型分为了两种:原始数据类型 与 引用数据类型。
共有以下八种数据类型:Number
、String
、Boolean
、Null
、Undefined
、Object
、Symbol(ES6)
、BigInt(ES10)
。
原始数据类型:String
、Number
、Boolean
、Null
、Undefined
、Symbol
、BigInt
。
引用数据类型:Object
。
不同类型的存储方式:
原始数据类型:原始数据类型的值在内存中占据固定大小,保存在栈内存中。
引用数据类型:引用数据类型的值是对象,在栈内存中只是保存对象的变量标识符以及对象在堆内存中的储存地址,其内容是保存中堆内存中的。
var
、let
与 const
有什么区别
var
具有变量提升性质,let
与const
没有。
所谓 变量提升 指的是
JS
在预编译阶段,函数和以var
声明的变量会被JS
引擎提升至当前作用域顶端。
// 编译前
function sayName() {
console.log(name);
var name = '橙某人';
}
// 编译后
function sayName() {
var name;
console.log(name);
name = '橙某人';
}
复制代码
var
不具块级作用域,let
与const
具有块级作用域性质。
// 例子一
{
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1
console.log(b); // 报错
console.log(c); // 报错
// 例子二
function fn() {
if(true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1
console.log(c); // 报错
console.log(c); // 报错
}
fn()
复制代码
暂时性死区:
JS
引擎在预编译代码的时候,如果遇到var
声明会将它提升至当前作用域顶端,如果遇到let
和const
会将它们放入暂时性死区(Temporal Dead Zone),简称TDZ
, 它的性质是会形成一个封闭的作用域,任何访问TDZ
中的变量就会报错。只有在执行过变量的声明语句后,变量才会从TDZ
中移除,才能进行正常的变量访问。
var
能重复声明,let
与const
重复声明会报错。var
全局声明变量会挂载在window
,let
与const
不会。
var a = a;
console.log(window.a); // 1
let b = 2;
console.log(window.b); // undefined
const c = 3;
console.log(window.c); // undefined
复制代码
了解一下 JS
的工作过程,有利于更好的理解问题哦:
JS
引擎的执行过程分为三个阶段:语法分析阶段、预编译阶段、执行阶段。语法分析阶段:检查书写的
JS
语法有没有错误,如是否少写个'{‘等。
预编译阶段:分为全局预编译、局部预编译。全局预编译:
- 创建GO对象。
- 找变量声明,将变量声明作为GO对象的属性名,并赋值undefined。
- 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体。
局部预编译:
- 创建一个AO对象。
- 找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined。
- 将实参和形参统一。
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。
执行阶段:从上到下,逐行执行,变量赋值阶段也在此完成。
判断数据类型的四种方式
typeof
,区分不了细致的Object
类型,如Array
、Date
、RegExp
都只是返回object
。
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof null); // object, null 在 typeof 下被标记为 object, 这是JS一个历史bug了
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof Symbol()); // symbol
console.log(typeof 1n); // bigint
复制代码
创建一个
BigInt
类型的方式有两种:在一个整数字面量后面加n
或者调用BigInt
函数。
const a = BigInt(1);
const b = 1n
;
Object.prototype.toString.call()
,基本能满足对各种数据类型的判断。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call('1')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(1n)); // [object BigInt]
复制代码
它对于其他一些内置引用类型的判断也很适合。
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(/a/g)); // [object RegExp]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
console.log(Object.prototype.toString.call(new Error())); // [object Error]
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
function fn() {
console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
fn();
复制代码
constructor
,是根据原型链原来来判断的,因为每一个实例对象都可以通过constructor
来访问它的构造函数。
console.log((1).constructor === Number);
console.log('1'.constructor === String);
console.log(true.constructor === Boolean);
// console.log(null.constructor);
// console.log(undefined.constructor);
console.log({}.constructor === Object);
console.log(Symbol().constructor === Symbol);
console.log(1n.constructor === BigInt);
复制代码
由于 undefined
和 null
是无效的对象,并不具备 constructor
而且 null
是作为原型链的末端结尾。
instanceof
,内部机制是通过检查构造函数的原型对象(prototype
)是否出现在被检测对象的原型链上来判断的。
console.log(1 instanceof Number); // false
console.log('1' instanceof String); // false
console.log(true instanceof Boolean); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
复制代码
instanceof
强调的是对拥有原型链的引用类型对象的判断,所以对于原生数据类型就束手无策(字面量方式创建的不能检测)。我们对于它更多的是在这种场景中使用:
function Person(){};
function Student(){};
var p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Student); // false
复制代码
如何快速判断一个 window
类型
这是小编在一次面试中面试官问我的题目,当时很懵,因为这个问题是建立在我刚答完上面说过的 “判断数据类型的四种方式” 的时候,当时我随口就说了分别能利用 Object.prototype.toString.call()
、constructor
、instanceof
三种方式来判断。
console.log(Object.prototype.toString.call(window)); // [object Window]
console.log(window.constructor === Window); // true
console.log(window instanceof Window); // true
复制代码
面试官一脸严肃的表情问我“除了这三种,还有吗?”
我……心想…还有吗???这还不够吗?脑袋想不出来东西了,我傻笑着抓着脑袋说:“暂时我没想到其他方式了,这还有其他快的方式吗?求指教”。
面试官:“很简单,window
它有一个 window属性
指向自身,可以利用这个特性来判断。”
console.log(window.window === window); // true
复制代码
好家伙,细,真的细。
闭包
一个万年常考题,MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
??? 懵逼?会不会有小伙伴学了很久的闭包,也经常使用,但就是死活说不清楚它是啥,能干什么?在网上关于它的文章,那是数不胜数,基本一搜索就一大把,各篇文章说得五花八门。
但看了那么多文章学习,你又是否学会了呢?在我这,我觉得学会一个知识点,就是你能用大白话把它说出来,而不是背它的概念,只有这样才能说明自己消化了,剩下的就是不断去应用加强印象就可以了,慢慢就记牢了。
这次不对闭包深究了,没意思,就简单说说几个问答题。
面试官:说说你对闭包的理解?
这是一个客观性非常强的问法,我们直追本质,面试官想知道什么呢?他其实只是想听听你对闭包是否有自己的个人理解,对于概念的他每天不知道听过多少次了。
答:我们知道函数内部能直接读取函数外部定义的变量,但是函数外部无法读取函数内部的变量,闭包在我看来,就是提供一种访问函数内部变量的桥梁。
面试官:说说闭包的好处和坏处?
答:使用闭包的好处是提供了访问函数内部变量的方式与避免造成全局变量污染。使用闭包的坏处是会造成内存消耗大,滥用闭包会导致内存泄露,造成页面奔溃。
面试官:你能说说为什么使用闭包会造成内存泄露吗?
答:函数的作用域链是在函数定义的时候创建的,在函数运行完成,销毁的时候消亡,这时它内部的变量就应该被销毁,内存被回收,但是闭包能让其继续延续下去,不被垃圾回收机制回收。由于变量都是维护在内存中的,这些变量数据就会一直占用着内存,最后超载使用内存,造成内存泄露。
面试官:手写一个闭包的例子吧?
手写例子一般也是必不可少的,闭包的例子非常非常的多,我们只要记住一些简单、容易记住的小例子防身,我觉得就可以了。
一道经典的闭包例子:
var btn = document.getElementById('btn');
var inputs = document.getElementsByTagName('input');
for(var i = 0;i<inputs.length;i++) {
(function(i) {
inputs[i].onclick = function() {
console.log(i)
}
})(i)
}
复制代码
防抖节流
这也是一个老题目了,就在我写这篇文章的前两天,刚好就被问过。(T_T) 这是两个容易记混淆的概念,这里我想了两个例子,你且看看妥不妥当。
相信各位对王者荣耀(赶紧上号,峡谷见!!!)不陌生了,我们直接来看:
- 防抖:有点像英雄在回城的时候,每次点击回城要一定时间才能传送,在这个时间内如果移动英雄,则要重新开始。
- 节流:有点像射手英雄,不管你按得多快,只要攻速没增加,也只会一下一下射出攻击而已。
嘿嘿,这两个例子如何?有没有帮你记住了他俩呢?(  ̄ ▽ ̄)
防抖(debounce)
在触发事件后 N 秒后才执行函数,如果在 N 秒内又触发了函数,则重新进行计时。
-
应用场景:
输入框进行输入实时搜索、页面触发resize事件的时候。 -
手写:
function debounce(fn, wait) { var timer = null return () => { clearTimeout(timer); timer = setTimeout(fn, wait) } } 复制代码
节流(throttle)
在规定的一个单位时间内,只触发一次函数,如果单位时间内触发多次函数,只有一次生效。
-
应用场景:
页面滚动事件。 -
手写:
function throttle(fn, wait) { var timer = null; return () => { if(!timer) { timer = setTimeout(() => { timer = null; fn() }, wait) } } } 复制代码
上面列举了两个的简单手写过程,现在去面试,基本都有手写代码题了,这都成为一个潮流了,这导致了很多时候我们要记住很多代码的书写过程,这真的让人难受想哭(︶︿︶)。
我自己的方法是,我会记住最简单的代码结构,剔除那些非主流的功能设计,只留一个能实现主要功能的代码构架就行,剩下的如果真遇到手写代码的时候,再自己慢慢推导出来就行。(虽然很多时候可能也推导不出来,哈哈哈)
原型与原型链
- 原型
什么是原型呢?它是一个对象,我们也称它为原型对象,代码中用 prototype
来表示。
- 原型链
那什么又是原型链呢?原型与原型层层相链接的过程即为原型链。
原型与原型链 在前端是一个非常基础的 JS
概念了,相信你也或多或少会有所听过了,我们且来看看下面这张图你是否看得懂:
要理解好 原型与原型链 我们要记住五个很重要的东西,这是我们每次回顾它们的时候都要想起来的:
JS
把对象(除null
)分为普通对象与函数对象,不管是什么对象都会有一个__proto__
属性。- 函数对象还会有一个
prototype
属性,也就是说函数默认拥有__proto__
属性与prototype
属性。 - 普通对象的
__proto__
属性与函数对象的prototype
属性都会指向它们对应的原型对象。 - 函数对象另一个
__proto__
属性会指向Function.prototype
原型,原型链的末端为null
。 - 原型对象 它会拥有一个
constructor
属性指向它的构造函数。
一般面试聊到 原型与原型链 能讲清楚这五个点基本也就及格了,那么如何记住这些东西?
答案:画图,按着自己的理解,自己画两天你就印象深刻,不骗你,略略略。
如何推导出这个图,可以看看小编前面写过这篇文章:看完,你会发现,原型、原型链原来如此简单!
call与apply与bind
call/apply/bind
的面试题无非逃不过的就是手写代码实现了(︶︿︶),我们就不聊它的应用了,简单聊聊它会涉及的问题不是。
面试官:它们三者有什么作用?
答:它们三者的主要作用都是为了改变函数的
this
指向,目前大部分关于它们的应用都是围绕这一点来进行的。
面试官:那它们之间有什么区别?
答: 有三点不同:
- 参数不同,
call
与bind
参数是通过一个一个传递的,apply
只接收一个参数并且是以数组的形式传递,后续参数是无效的。- 执行时机不同,
call
与apply
立即调用立即执行,bind
调用后会返回一个函数,调用该函数才执行。bind
返回的函数能作为构造函数使用,传递的参数依然有效,但绑定的this
将失效。
(记忆apply
传递的参数为数组,可以根据开头 a
等同于 Array
记忆哦(-^〇^-))
有时候适当的总结会让面试官很舒心,你上面哔哩啪啦讲一大堆,讲完自己也忘了,对比你直接说: “有xx点不同,第一点是…第二点是…” ,相信后者的方式更能博得面试官的好感。
面试官:手写实现bind()方法吧?
只是能说出它们三者的作用区别,并不能让我们脱颖而出,只有理解够深我们才能卷得过别人,手写必不可少!特别是 bind
的,大厂的面试中基本是高频出现。
关于 call
与 apply
的实现你可以看小编之前看的文章了解一下。 Call与Apply函数的分析及手写实现
而 bind
的实现如下:
Function.prototype.myBind = function(context) {
// 保存原函数
var _fn = this;
// 获取第一次传递的参数
var params = Array.prototype.slice.call(arguments, 1);
var Controller = function() {};
// 返回的函数, 可能会被当作 new调用
var bound = function() {
// 获取第二次传递的参数
var secondParams = Array.prototype.slice.call(arguments);
/** 考虑返回的bound函数是被当成 普通的调用 还是 new调用:
* new调用: 绑定的 this 失效, bound函数中的this指向自身
* 普通的调用: 正常改变执行函数的 this 指向, 把它指向 context
*/
var targetThis = this instanceof Controller ? this : context;
return _fn.apply(targetThis, params.concat(secondParams));
}
/**
* 1. 返回的函数应该具有原函数的原型。
* 2. 修改返回函数的原型不能影响原函数的原型。
*/
Controller.prototype = _fn.prototype;
bound.prototype = new Controller();
return bound;
}
复制代码
测试代码:
function fn(val1, val2, val3) {console.log(this, val1, val2, val3)}
var res = fn.myBind(obj, 1, 2)
res(3); // {name: "橙某人"} 1 2 3
复制代码
深浅拷贝
如果你学过一些前端知识,知道栈空间与堆空间,那么我相信你对于理解这个概念一定没啥问题了,涉及这个知识点的面试题一般可能会是代码层面上,如:实现一个浅拷贝函数?或者递归实现一下深拷贝函数等。
浅拷贝
浅拷贝操作会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,如果属性是基本类型,拷贝的就是基本数据类型的值,如果属性是引用类型,拷贝的就是内存地址。
- 手写
function copy(original) { var o = {}; for(var key in original) { o[key] = original[key]; } return o; } 复制代码
深拷贝
深拷贝操作会将一个对象从内存中完整拷贝一份出来,从堆内存中开辟一个新的区域放新对象,且修改新对象不会影响原对象。
- 手写
function deepCopy(original) { if(typeof original !== 'object') return; var o = {} for(let key in original) { o[key] = typeof original[key] === 'object' ? deepCopy(original[key]) : original[key] } return o; } 复制代码
如果你了解深浅拷贝的基本概念,相信上面的代码对你没什么难度的。(^▽^)
面试官:你觉得赋值与浅拷贝有什么区别? (这是在网上冲浪的时候偶然看到的一题)
- 把一个对象赋值给另一个新变量时,赋值的是该对象在栈中的地址,两个对象指向的是同一个堆空间。
- 浅拷贝是重新在堆空间中创建一块空间,拷贝后的基本数据类型不相互影响,拷贝后的对象引用类型会相互影响。
(说白了就是:是否在堆内存中创建新空间。)
New关键字
关于 new
关键字相信用法你已经非常熟了。
function Person() {};
var p = new Person();
复制代码
但是,我需要你记住它干了三件事件:
- 新建了一个空对象并返回。
- 新对象的原型(
__proto__
)指向构造函数的原型(prototype
)。 - 构造函数的
this
指向新对象。
记住了这三件事情基本也就不用担心和它相关的面试题了,即使是让你来模拟它的实现,也是非常简单的。
function myNew(constructor) {
// 1. 创建新对象
const newObject = new Object();
// 2. 改变新对象的原型指向
newObject.__proto__ = constructor.prototype;
// 3. 构造函数的 this 指向新对象
constructor.apply(newObject, Array.prototype.slice.call(arguments, 1))
return newObject;
}
复制代码
是不是完全没有难度? 更多详情
柯里化函数
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
这概念很懵吧?没错,我也懵。不过没有关系,我们记住它的例子就行,它的例子很有特点,看完,你再来回头想想可能就能懂了噢。
我们先来看一个很简单的三个数累加示例:
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
复制代码
这是一个很简单的操作,但有时我们希望 add
函数能够灵活一点,像是这样子的形式也能实现:
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1)(2)(3)); // 6
console.log(add(4, 5, 6)); // 15
复制代码
这个时候就会用到柯里化的概念了,下面我们来直接看它的代码,带很详细的解释,实际的代码没有多少行的,完全没有负担的,哈哈。
/**
* 柯里化函数: 延迟接收参数, 延迟执行, 返回一个函数继续接收剩余的参数
* call: 收集参数
* apply: 注入参数, 参数变成了数组, 借用apply能依次注入参数
*/
function curry(fn) {
// 获取原函数的参数长度
var argsLength = fn.length;
return function curried() {
// 获取调用 curried 函数的参数
var args1 = Array.prototype.slice.call(arguments);
// 判断收集的参数是否满足原函数的参数长度: 满足-调用原函数返回结果 不满足-继续柯里化(递归)
if(args1.length >= argsLength) {
// 调用原函数返回结果
return fn.apply(this, args1);
}else {
// 不满足继续返回一个函数收集参数
return function() {
var args2 = Array.prototype.slice.call(arguments);
// 继续柯里化
return curried.apply(this, args1.concat(args2));
}
}
}
}
复制代码
测试代码
function add(a, b, c) {
return a + b + c;
}
var newAdd = curry(add);
console.log(newAdd(1, 2, 3)); // 6
console.log(newAdd(1, 2)(3)); // 6
console.log(newAdd(1)(2, 3)); // 6
console.log(newAdd(1)(2)(3)); // 6
console.log(newAdd(4, 5, 6)); // 15
复制代码
柯里化函数在我看来就是,延迟接收参数,延迟执行,返回一个函数继续接收剩余的参数,当函数参数接收满足条件的时候,就执行原函数返回结果。