笔者最近在对原生JS的知识作系统梳理,由于我以为JS做为前端工程师的根本技术,学再多遍都不为过。打算来作一个系列,一共分三次发,以一系列的问题为驱动,固然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提高,高级选手也会获得复习和巩固。敬请你们关注!html
在 JS 中,存在着 7 种原始值,分别是:前端
引用数据类型: 对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)golang
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
复制代码
结果:面试
p1:{name: “fyq”, age: 26}
p2:{name: “hzj”, age: 18}
复制代码
缘由: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,经过调用person.age = 26确实改变了p1的值,但随后person变成了另外一块内存空间的地址,而且在最后将这另一分内存空间的地址返回,赋给了p2。chrome
结论: null不是对象。编程
解释: 虽然 typeof null 会输出 object,可是这只是 JS 存在的一个悠久 Bug。在 JS 的最第一版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头表明是对象然而 null 表示为全零,因此将它错误的判断为 object 。segmentfault
其实在这个语句运行的过程当中作了这样几件事情:api
var s = new Object('1');
s.toString();
s = null;
复制代码
第一步: 建立Object类实例。注意为何不是String ? 因为Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来建立基本类型的包装类。数组
第二步: 调用实例方法。浏览器
第三步: 执行完方法当即销毁这个实例。
整个过程体现了基本包装类型
的性质,而基本包装类型偏偏属于基本数据类型,包括Boolean, Number和String。
参考:《JavaScript高级程序设计(第三版)》P118
0.1和0.2在转换成二进制后会无限循环,因为标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。
BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型容许咱们安全地对
大整数
执行算术操做,表示高分辨率的时间戳,使用大整数id,等等,而不须要使用库。
在JS中,全部的数字都以双精度64位浮点格式表示,那这会带来什么问题呢?
这致使JS中的Number没法精确表示很是大的整数,它会将很是大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此范围的整数值均可能失去精度。
console.log(999999999999999); //=>10000000000000000
复制代码
同时也会有必定的安全性问题:
9007199254740992 === 9007199254740993; // → true 竟然是true!
复制代码
要建立BigInt,只须要在数字末尾追加n便可。
console.log( 9007199254740995n ); // → 9007199254740995n
console.log( 9007199254740995 ); // → 9007199254740996
复制代码
另外一种建立BigInt的方法是用BigInt()构造函数、
BigInt("9007199254740995"); // → 9007199254740995n
复制代码
简单使用以下:
10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n
const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"
复制代码
BigInt不支持一元加号运算符, 这多是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js代码。
由于隐式类型转换可能丢失信息,因此不容许在bigint和 Number 之间进行混合操做。当混合使用大整数和浮点数时,结果值可能没法由BigInt或Number精确表示。
10 + 10n; // → TypeError
复制代码
Math.max(2n, 4n, 6n); // → TypeError
复制代码
if(0n){//条件判断为false
}
if(3n){//条件为true
}
复制代码
元素都为BigInt的数组能够进行sort。
BigInt能够正常地进行位运算,如|、&、<<、>>和^
caniuse的结果:
其实如今的兼容性并不怎么好,只有chrome6七、firefox、Opera这些主流实现,要正式成为规范,其实还有很长的路要走。
咱们期待BigInt的光明前途!
对于原始类型来讲,除了 null 均可以调用typeof显示正确的类型。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
复制代码
但对于引用数据类型,除了函数以外,都会显示"object"。
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
复制代码
所以采用typeof判断对象数据类型是不合适的,采用instanceof会更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
复制代码
能。好比下面这种方式:
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
复制代码
若是你不知道Symbol,能够看看MDN上关于hasInstance的解释。
其实就是自定义instanceof行为的一种方式,这里将原有的instanceof方法重定义,换成了typeof,所以可以判断基本数据类型。
核心: 原型链的向上查找。
function myInstanceof(left, right) {
//基本数据类型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object对象自带的一个方法,可以拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到尽头,还没找到
if(proto == null) return false;
//找到相同的原型对象
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
复制代码
测试:
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
复制代码
Object在严格等于的基础上修复了一些特殊状况下的失误,具体来讲就是+0和-0,NaN和NaN。 源码以下:
function is(x, y) {
if (x === y) {
//运行到1/x === 1/y的时候x和y都为0,可是1/+0 = +Infinity, 1/-0 = -Infinity, 是不同的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN===NaN是false,这是不对的,咱们在这里作一个拦截,x !== x,那么必定是 NaN, y 同理
//两个都是NaN的时候返回true
return x !== x && y !== y;
}
复制代码
解析:
== 中,左右两边都须要转换为数字而后进行比较。
[]转换为数字为0。
![] 首先是转换为布尔值,因为[]做为一个引用类型转换为布尔值为true,
所以![]为false,进而在转换成数字,变为0。
0 == 0 , 结果为true
JS中,类型转换只有三种:
转换具体规则以下:
注意"Boolean 转字符串"这行结果指的是 true 转字符串的例子
===叫作严格相等,是指:左右两边不只值要相等,类型也要相等,例如'1'===1的结果是false,由于一边是string,另外一边是number。
==不像===那样严格,对于通常状况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则以下:
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true
复制代码
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑以下:
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 输出7
复制代码
其实就是上一个问题的应用。
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true
复制代码
红宝书(p178)上对于闭包的定义:闭包是指有权访问另一个函数做用域中的变量的函数,
MDN 对闭包的定义为:闭包是指那些可以访问自由变量的函数。 (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另一个函数做用域中的变量。)
首先要明白做用域链的概念,其实很简单,在ES5中只存在两种做用域————全局做用域和函数做用域,当访问一个变量时,解释器会首先在当前做用域查找标示符,若是没有找到,就去父做用域找,直到找到该变量的标示符或者不在父做用域中,这就是做用域链
,值得注意的是,每个子函数都会拷贝上级的做用域,造成一个做用域的链条。 好比:
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}
复制代码
在这段代码中,f1的做用域指向有全局做用域(window)和它自己,而f2的做用域指向全局做用域(window)、f1和它自己。并且做用域是从最底层向上找,直到找到全局做用域window为止,若是全局尚未的话就会报错。就这么简单一件事情!
闭包产生的本质就是,当前环境中存在指向父级做用域的引用。仍是举上面的例子:
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();
复制代码
这里x会拿到父级做用域中的变量,输出2。由于在当前环境中,含有对f2的引用,f2偏偏引用了window、f1和f2的做用域。所以f2能够访问到f1的做用域的变量。
那是否是只有返回函数才算是产生了闭包呢?、
回到闭包的本质,咱们只须要让父级做用域的引用存在便可,所以咱们还能够这么作:
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
复制代码
让f1执行,给f3赋值后,等于说如今f3拥有了window、f1和f3自己这几个做用域的访问权限
,仍是自底向上查找,最近是在f1
中找到了a,所以输出2。
在这里是外面的变量f3存在着父级做用域的引用
,所以产生了闭包,形式变了,本质没有改变。
明白了本质以后,咱们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
复制代码
如下的闭包保存的仅仅是window和当前做用域。
// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})
复制代码
全局做用域window
和当前函数的做用域
,所以能够全局的变量。var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
复制代码
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
复制代码
为何会所有输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)
由于setTimeout为宏任务,因为JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,所以循环结束后setTimeout中的回调才依次执行,但输出i的时候当前做用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。所以会所有输出6。
解决方法:
一、利用IIFE(当即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
复制代码
二、给定时器传入第三个参数, 做为timer函数的第一个函数参数
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}
复制代码
三、使用ES6中的let
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}
复制代码
let使JS发生革命性的变化,让JS有函数做用域变为了块级做用域,用let后做用域链不复存在。代码的做用域以块级为单位,以上面代码为例:
// i = 1
{
setTimeout(function timer(){
console.log(1)
},0)
}
// i = 2
{
setTimeout(function timer(){
console.log(2)
},0)
}
// i = 3
...
复制代码
所以能输出正确的结果。
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
当函数通过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
JavaScript对象经过prototype指向父类对象,直到指向Object对象为止,这样就造成了一个原型指向的链条, 即原型链。
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);
复制代码
这样写的时候子类虽然可以拿到父类的属性值,可是问题是父类原型对象中一旦存在方法那么子类没法继承。那么引出下面的方法。
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();
console.log(new Child2());
复制代码
看似没有问题,父类的方法和属性都可以访问,但实际上有一个潜在的不足。举个例子:
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);
复制代码
能够看到控制台:
明明我只改变了s1的play属性,为何s2也跟着变了呢?很简单,由于两个实例使用的是同一个原型对象。
那么还有更好的方式么?
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
复制代码
能够看到控制台:
以前的问题都得以解决。可是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是咱们不肯看到的。那么如何解决这个问题?
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
复制代码
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,并且父类属性和方法均能访问,可是咱们来测试一下:
var s3 = new Child4();
var s4 = new Child4();
console.log(s3)
复制代码
子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
复制代码
这是最推荐的一种方式,接近完美的继承,它的名字也叫作寄生组合继承。
ES6的代码最后都是要在浏览器上可以跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。
那最后编译成了什么样子呢?
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// ...
//看到没有
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// 验证是不是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
复制代码
核心是_inherits函数,能够看到它采用的依然也是第五种方式————寄生组合继承方式,同时证实了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。
追问: 面向对象的设计必定是好的设计吗?
不必定。从继承的角度说,这一设计是存在巨大隐患的。
假如如今有不一样品牌的车,每辆车都有drive、music、addOil这三个方法。
class Car{
constructor(id) {
this.id = id;
}
drive(){
console.log("wuwuwu!");
}
music(){
console.log("lalala!")
}
addOil(){
console.log("哦哟!")
}
}
class otherCar extends Car{}
复制代码
如今能够实现车的功能,而且以此去扩展不一样的车。
可是问题来了,新能源汽车也是车,可是它并不须要addOil(加油)。
若是让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,可是我如今明明只须要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我如今是不须要的,可是因为继承的缘由,也给到子类了。
继承的最大问题在于:没法决定继承哪些属性,全部属性都得继承。
固然你可能会说,能够再建立一个父类啊,把加油的方法给去掉,可是这也是有问题的,一方面父类是没法描述全部子类的细节状况的,为了避免同的子类特性去增长不一样的父类,代码势必会大量重复
,另外一方面一旦子类有所变更,父类也要进行相应的更新,代码的耦合性过高
,维护性很差。
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,好比golang彻底采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,而后将这些零件进行拼装,来造成不一样的实例或者类。
function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦哟!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
复制代码
代码干净,复用性也很好。这就是面向组合的设计方式。
参考出处:
更多有趣内容见微信公众号: