从事前端行业到现在,感觉自己进步最大的时候就是去年打算换工作开始学习的那段时间,特别是看 yck 大佬的掘金小册《前端面试之道》的那段时间。正是那段时间的学习,慢慢对前端知识体系有了个模糊的轮廓,而且也开始接触掘金这个有意思的技术平台。如今工作尘埃落定,倒开始懒散了,通勤路上又开始玩游戏了,晚上回家又开始玩游戏不看书了,闲的时候开始在微信群QQ群注水了。可是距离30岁越来越近,眼前的路却越来越模糊。我知道留给我补课的时间不多了。工作的前三年已经被我挥霍掉,如果这两年不把失去的时间补回来,我可能永远都只能停留在初中级程序员的水平。谨记我还是一个半路出家的非科班出身的大龄初级前端开发工程师,自勉!
js中数据类型分为基本类型和引用类型,基本类型有六种:
number
string
boolean
null
undefined
symbol
(es6)引用类型包括对象object
、数组array
、函数function
等,统称对象类型:
object
string
类型即字符串,除了单引号双引号,es6 中引入了新的反引号 ` 来包含字符串。反引号的扩展功能是可以用
${…}`将变量和表达式嵌入到字符串中。使用如下:
let n = 3
let m = () => 4
let str = `m + n = ${m() + n}` // "m + n = 7"
number
类型值包括整数、浮点数、NaN
、Infinity
等。其中NaN
类型是js中唯一不等于自身的类型,当发生未定义的数学操作的时候,就会返回NaN
,如:1+'asdf'
、Number('asdf')
。浮点数的运算可能会出现如0.1 + 0.2 !== 0.3
的问题,这是由于浮点运算的精度的问题,一般采用toFixed(10)
便可以解决此类问题。
boolean
、string
和number
类型作为基本类型,按理说应该是没有函数可以调用的,因为基本类型没有原型链可以提供方法。但是,这三种类型却能调用toString
等对象原型上的方法。不信?
true.toString() // 'true'
`asdf`.toString() // 'asdf'
NaN.toString() // 'NaN'
你可能会说,那为什么数字1
不能调用toString
方法呢?其实,不是不能调用:
1 .toString()
1..toString()
(1).toString()
以上三种调用都是可以的,数字后面的第一个点会被解释为小数点,而不是点调用。只不过不推荐这种使用方法,而且这样做也没什么意义。
为什么基本类型却可以直接调用引用类型的方法呢?其实是js引擎在解析上面的语句的时候,会把这三种基本类型解析为包装对象(就是下面的new String()
),而包装对象是引用类型可以调用Object.prototype
上的方法。大概过程如下:
'asdf'.toString() -> new String('asdf').toString() -> 'asdf'
null
含义为“无”、“空”或“值未知”的特殊值。
undefined
的含义是“未被赋值”。除了变量已声明未赋值的情况下是undefined
,若对象的属性不存在也是undefined
。所以应该尽量避免使用var a = undefined; var o = {b: undefined}
这样的写法,取而代之用var a = null; var o = {b: null}
,以与“未被赋值”默认undefined
的情况相区分。
Symbol
值表示唯一的标识符。可以用Symbol()
函数创建:
var a = Symbol('asdf')
var b = Symbol('asdf')
a === b // false
还可以创建全局标识符,这样可以在访问相同的名称的时候都得到同一个标识符。如下:
var a = Symbol.for('asdf')
var b = Symbol.for('asdf')
a === b // true
还可以用做对象的属性,但此时是不能被for...in
遍历的:
let id = Symbol('id')
let obj = {
[id]: 'ksadf2sdf3lsdflsdjf090sld',
a: 'a',
b: 'b'
}
for(let key in obj){ console.log(key) } // a b
obj[id] // "ksadf2sdf3lsdflsdjf090sld"
还存在很多系统内置的Symbol
,如Symbol.toPrimitive``Symbol.iterator
等。当发生引用类型强制转基本类型的操作时,就会触发内置的Symbol.toPrimitive
函数,当然也可以给对象手动添加Symbol.toPrimitive
函数来覆盖默认的强制类型转换行为。
object
是引用类型,引用类型和基本类型不同的是,原始类型存储的是值,引用类型存储的是一个指向对象真实内存地址的指针。在 js 中,对象包括Array Object Function RegExp Math
等。
js 所有的函数语句都是在执行栈中执行的,所有的变量也在执行栈中保存着值或引用。基本类型就存储在栈内存中,保存的是实际值;引用类型存储在堆内存中,在栈中只保存着变量指向内存地址的指针。
var o = {
a: 'a',
b: 'b'
}
var o2 = o // 变量o2复制了变量o的指针,现在他们都指向同一个内存地址,现在开始他们的增删改其实是在同一个内存地址上的操作
o2.c = 'c' // (增)现在o.c也是'c'
delete o2.b // (删)现在o.b也不存在了
o2.a = 'a2' // (改)现在o.a也是'a2'
o2 = 'o2' // 现在变量o2被赋值'o2',已经和原来的内存地址断绝了关系,但变量 o 仍然指向老地址
```var o = {
a: 'a',
b: 'b'
}
var o2 = o // 变量o2复制了变量o的指针,现在他们都指向同一个内存地址,现在开始他们的增删改其实是在同一个内存地址上的操作
o2.c = 'c' // (增)现在o.c也是'c'
delete o2.b // (删)现在o.b也不存在了
o2.a = 'a2' // (改)现在o.a也是'a2'
o2 = 'o2' // 现在变量o2被赋值'o2',已经和原来的内存地址断绝了关系,但变量 o 仍然指向老地址
判断引用类型和基本类型的类型是不同的,判断基本类型可以用typeof
:
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
可以看到除了null
其他基本类型的判断都是正常的,typeof(null) === 'object'
是一个历史悠久的 bug,就是在 JS 的最初版本中null
的内存存储信息是000
开头的,而000
开头的会被判断为object
类型。虽然现在内部类型判断代码已经改变了,但是这个 bug 却不得不随着版本保留了下来,因为修改这个 bug 会导致巨多的网站出现 bug 。
typeof
对引用类型,除了函数返回function
,其他都返回object
。但我们开发中数组肯定是要返回array
类型的,所以typeof
对引用类型来说并不是很适用。判断引用类型一般用instanceof
:
var obj = {}
var arr = []
var fun = () => {}
typeof obj // 'object'
typeof arr // 'object'
typeof fun // 'function'
obj instanceof Object // true
arr instanceof Array // true
fun instanceof Function // true
可以看到instanceof
操作符可以正确判断出引用类型的类型。instanceof
本质上是判断右边的构造函数的prototype
对象是否存在于左边的原型链上,是的话返回true。所以不论数组、对象还是函数,... instanceof Object
都返回true
。
最后来一种全能型判断类型方法:Object.prototype.toString.call(...)
,可以自行尝试。
JS 是弱类型语言,不同类型之间在一定情况下会发生强制类型转换,比如在相等性比较的时候。
基本类型的相等性比较的是值是否一样,对象相等性比较的是内存地址是否相同。下面来看一个有意思的比较把:
[] == [] // ?
[] == ![] // ?
对于[]``{}``function (){}
这样的没有被赋值给变量的引用类型来说,他们只在当前语句中有效,而且不相等于其他任何对象。因为根本无法找到他们的内存地址的指针。所以[] == []
是false
。
对于[] == ![]
,因为涉及到强制类型转换,所以复杂的多了。想要更加详细了解强制类型转换可以看我这篇文章 。
在 JS 中类型转换只有三种情况:toNumber
、 toString
、 toBoolean
。正常情况下转换规则如下:
|原始值/类型|目标类型:number|结果|
|-|-|-|-|
|null|number|0
|
|symbol|number|抛错|
|string|number|'1'=>1``'1a'=>NaN
,含非数字则为NaN
|
|数组|number|[]=>0``['1']=>1``['1', '2']=>NaN
|
|object/function/undefined|number|NaN
|
|原始值/类型|目标类型:string|结果|
|-|-|-|-|
|number|string|1=>'1'
|
|array|string|[1, 2]=>'1,2'
|
|布尔值/函数/symbol|string|原始值直接加上引号,如:'true'
|
|object|string|{}=>'[object Object]'
|
|原始值/类型|目标类型:boolean|结果|
|-|-|-|-|
|number|boolean|除了0
、NaN
为false
,其他都是true
|
|string|boolean|除了空字符串为false
,其他都为true
|
|null/undefined|boolean|false|
|引用类型|boolean|true|
现在来揭开 [] == ![]
返回true
的真相把:
[] == ![] // true
/*
* 首先,布尔操作符!优先级更高,所以被转变为:[] == false
* 其次,操作数存在布尔值false,将布尔值转为数字:[] == 0
* 再次,操作数[]是对象,转为原始类型(先调用valueOf(),得到的还是[],再调用toString(),得到空字符串''):'' == 0
* 最后,字符串和数字比较,转为数字:0 == 0
*/
NaN == NaN // false NaN不等于任何值
null == undefined // true
null == 0 // false
undefined == 0 // false
js 中的作用域是词法作用域,是由 函数声明时 所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标识符的访问规则。说到底js的作用域只是一个“空地盘”,其中并没有真实的变量,但是却定义了变量如何访问的规则。(词法作用域是在编译阶段就确认的,区别于词法作用域,动态作用域是在函数执行的时候确认的,js的没有动态作用域,但js的this
很像动态作用域,后面会提到。语言也分为静态语言和动态语言,静态语言是指数据类型在编译阶段就确定的语言如 java,动态语言是指在运行阶段才确定数据类型的语言如 javascript。)
作用域链本质上是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是作用域概念的延申。作用域链定义了在当前上下文访问不到变量的时候如何沿作用域链继续查询变量的一套规则。
执行上下文是指 函数调用时 在执行栈中产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量、this
对象等。例如:
let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
let b = 5;
fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
let c = 5;
console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境
每次函数调用时,执行栈栈顶都会产生一个新的执行上下文环境,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象(running execution context,图中蓝色的块),区别与底下被挂起的变量对象(执行上下文)。
函数的执行过程分成两部分,一部分用来生成执行上下文环境,确定this的指向、声明变量以及生成作用域链;另一部分则是按顺序逐行执行代码。
建立执行上下文阶段:(发生在 函数被调用时 && 函数体内的代码执行前 )
函数执行阶段:
4 . 逐行执行代码,这个阶段会完成变量赋值,函数引用,以及执行其他代码。
let fn = function(){
alert(this.name)
}
let obj = {
name: '',
fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
fn()
,这种看着像光杆司令的调用方式,this
指向window
(严格模式下是undefined
)。obj.fn()
,此时this
指向obj
对象。点调用中this
指的是点前面的对象。call
函数把fn
中的this
指向了第一个参数,这里是obj
。即利用call
、apply
、bind
函数可以把函数的this
变量指向第一个参数。new
实例化了一个对象instance
,这时fn
中的this
就指向了实例instance
。如果同时发生了多个规则怎么办?其实上面四条规则的优先级是递增的:
fn() < obj.fn() < fn.call(obj) < new fn()
首先,new
调用的优先级最高,只要有new
关键字,this
就指向实例本身;接下来如果没有new
关键字,有call、apply、bind
函数,那么this
就指向第一个参数;然后如果没有new、call、apply、bind
,只有obj.foo()
这种点调用方式,this
指向点前面的对象;最后是光杆司令foo()
这种调用方式,this
指向window
(严格模式下是undefined
)。
es6中新增了箭头函数,而箭头函数最大的特色就是没有自己的this、arguments、super、new.target
,并且箭头函数没有原型对象prototype
不能用作构造函数(new
一个箭头函数会报错)。因为没有自己的this
,所以箭头函数中的this
其实指的是包含函数中的this
。无论是点调用,还是call
调用,都无法改变箭头函数中的this
。
很长时间以来我对闭包都停留在“定义在一个函数内部的函数”这样肤浅的理解上。事实上这只是闭包形成的必要条件之一。直到后来看了kyle大佬的《你不知道的javascript》上册关于闭包的定义,我才豁然开朗:
当函数能够记住并访问所在的词法作用域时,就产生了闭包。
let single = (function(){
let count = 0
return {
plus(){
count++
return count
},
minus(){
count--
return count
}
}
})()
single.plus() // 1
single.minus() // 0
这是个单例模式,这个模式返回了一个对象并赋值给变量single
,变量single
中包含两个函数plus
和minus
,而这两个函数都用到了所在词法作用域中的变量count
。正常情况下count
和所在的执行上下文会在函数执行结束时被销毁,但是由于count
还在被外部环境使用,所以在函数执行结束时count
和所在的执行上下文不会被销毁,这就产生了闭包。每次调用single.plus()
或者single.minus()
,就会对闭包中的count
变量进行修改,这两个函数就保持住了对所在的词法作用域的引用。
闭包其实是一种特殊的函数,它可以访问函数内部的变量,还可以让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。
看个经典安利:
// 方法1
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
方法1中,循环设置了五个定时器,一秒后定时器中回调函数将执行,打印变量i
的值。毋庸置疑,一秒之后i
已经递增到了5,所以定时器打印了五次5 。(定时器中并没有找到当前作用域的变量i
,所以沿作用域链找到了全局作用域中的i
)
方法2中,由于es6的let
会创建局部作用域,所以循环设置了五个作用域,而五个作用域中的变量i
分布是1-5,每个作用域中又设置了一个定时器,打印一秒后变量i
的值。一秒后,定时器从各自父作用域中分别找到的变量i
是1-5 。这是个利用闭包解决循环中变量发生异常的新方法。
js 中的几乎所有对象都有一个特殊的[[Prototype]]
内置属性,用来指定对象的原型对象,这个属性实质上是对其他对象的引用。在浏览器中一般都会暴露一个私有属性 __proto__
,其实就是[[Prototype]]
的浏览器实现。假如有一个对象var obj = {}
,那么可以通过obj.__proto__
访问到其原型对象Object.prototype
,即obj.__proto__ === Object.prototype
。对象有[[Prototype]]
指向一个原型对象,原型对象本身也是对象也有自己的[[Prototype]]
指向别的原型对象,这样串接起来,就组成了原型链。
var obj = [1, 2, 3]
obj.__proto__ === Array.prototype // true
Number.prototype.__proto__ === Object.prototype // true
Array.prototype.__proto__ === null // true
obj.toString()
可以看出,上例中存在一个从obj
到null
的原型链,如下:
obj----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
上例中最后一行调用obj.toString()
方法的时候,js 引擎就是沿着这条原型链查找toString
方法的。js 首先在obj
对象自身上查找toString
方法;未找到,继续沿着原型链查找Array.prototype
上有没有toString
;未找到,继续沿着原型链在Object.prototype
上查找。最终在Object.prototype
上找到了toString
方法,于是泪流满面的调用该方法。这就是原型链最基本的作用。原型链还是 js 实现继承的本质所在,下一小节再讲。
上面我说“js 中的几乎所有对象都有一个特殊的[[Prototype]]
内置属性”,为什么不是全部呢?因为 js 可以创建没有内置属性[[Prototype]]
的对象:
var o = Object.create(null)
o.__proto__ // undefined
Object.create
是 es5 的方法,所有浏览器都已支持。该方法创建并返回一个新对象,并将新对象的原型对象赋值为第一个参数。在上例中,Object.create(null)
创建了一个新对象并将对象的原型对象赋值为null
。此时对象 o
是没有内置属性[[Prototype]]
的(不知道为什么o.__proto__
不是null
,希望知道的大佬评论解释下,万分感激)。
js 的继承是通过原型链实现的,具体可以参考我的这篇文章,这里我只讲一讲大家可能比较陌生的“行为委托”。行为委托是《你不知道的JavaScript》系列作者 kyle 大佬推荐的一种代替继承的方式,该模式主要利用setPrototypeOf
方法把一个对象的内置原型[[Protytype]]关联到另一个对象上,从而达到继承的目的。
let SuperType = {
initSuper(name) {
this.name = name
this.color = [1,2,3]
},
sayName() {
alert(this.name)
}
}
let SubType = {
initSub(age) {
this.age = age
},
sayAge() {
alert(this.age)
}
}
Object.setPrototypeOf(SubType,SuperType)
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // 'gim'
SubType.sayName() // '17'
上例就是把父对象SuperType
关联到子对象SubType
的内置原型上,这样就可以在子对象上直接调用父对象上的方法。行为委托生成的原型链比class继承生成的原型链的关系简单清晰,一目了然。
kyle大佬倡导的行为委托
js 是单线程的,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),js 不可能等待IO设备执行完成才继续执行下一个的任务,这样就失去了这门语言的意义。所以 js 的任务分为同步任务和异步任务。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),堆用来存放数组对象等引用类型,栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
任务队列中有两种异步任务,一种是宏任务,包括script setTimeout setInterval
等,另一种是微任务,包括Promise process.nextTick MutationObserver
等。每当一个 js 脚本运行的时候,都会先执行script
中的整体代码;当执行栈中的同步任务执行完毕,就会执行微任务中的第一个任务并推入执行栈执行,当执行栈为空,则再次读取执行微任务,循环重复直到微任务列表为空。等到微任务列表为空,才会读取宏任务中的第一个任务并推入执行栈执行,当执行栈为空则再读取执行微任务,微任务为空才再读取执行宏任务,如此循环。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/bHa-1KSQUh8XiqIT5CHP9w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。