zaizizaizai

a dream of a child


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 搜索

理解JavaScript--原型

发表于 2018-02-25 | 分类于 JavaScript | 阅读次数

四、原型


1.原型对象

绝大部分的函数(少数内建函数除外)都有一个prototype属性,这个属性是原型对象用来创建新对象实例,而所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性,例如hasOwnProperty()方法存在于Obejct原型对象中,它便可以被任何对象当做自己的方法使用.

object.hasOwnProperty( propertyName )
hasOwnProperty()函数的返回值为Boolean类型。如果对象object具有名称为propertyName的属性,则返回true,否则返回false。

1
2
3
4
5
6
7
8
 var person = {
name: "Messi",
age: 29,
profession: "football player"
};
console.log(person.hasOwnProperty("name")); //true
console.log(person.hasOwnProperty("hasOwnProperty")); //false
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true

由以上代码可知,hasOwnProperty()并不存在于person对象中,但是person依然可以拥有此方法.

很多人此时会好奇,person对象是如何找到Object对象中的方法的呢?这其中的内部机制是什么?
这便是我们接下来要说的原型链.


2.__proto__与[[Prototype]]

上一篇我们的示意图中曾经出现过__proto__,在ES6之前这个__proto__是大部分主流浏览器(IE除外)引擎提供的,还尚属非ECMA标准,在解析一个对象实例的时候为对象实例添加一个__proto__属性,此属性指向原型对象,我们便可以通过此属性找到原型对象.

1
2
3
4
5
6
7
8
function person(pname, page) {
this.name = pname;
this.age = page;
}
person.prototype.profession = "football player";
var person1 = new person("Messi", 29); //person1 = {name:"Messi", age: 29, profession: "football player"};
var person2 = new person("Bale", 28); //person2 = {name:"Bale", age: 28, profession: "football player"};
console.log(person1.__proto__ === person.prototype); //true

__proto__除了被主流浏览器支持外,还被Node.js支持,在ES2015进入到规范附录部分,算是被正式纳入了标准.

而在标准的语法里,实例对象是通过内置的内部属性[[Prototype]]来追踪原型对象的,这个[[Prototype]]的指针始终指向原型对象,此属性通常情况下是不可见的,我们需要用getPrototypeOf()来读取[[Prototype]]属性值(这个值就是原型对象).

1
2
var obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); //true

同时我们也可以用isPrototypeOf来检验某个对象是否是另一个对象的原型对象.

1
2
var obj = {};
console.log(Object.prototype.isPrototypeOf(obj)); //true

3.原型链

在我们了解了__proto__与[[Prototype]]之后,就可以相对容易理解原型链了,由于__proto__与[[Prototype]]功能相似,但是__proto__更容易测试方便学习,我们选择__proto__来进行原型链的讲解.

1
2
3
4
5
6
7
8
9
10
11
12
13
function person(pname, page) {
this.name = pname;
this.age = page;
}
person.prototype.profession = "football player";
var person1 = new person("Messi", 29); //person1 = {name:"Messi", age: 29, profession: "football player"};
var person2 = new person("Bale", 28); //person2 = {name:"Bale", age: 28, profession: "football player"};
person1.hasOwnProperty("name");
console.log(person1.hasOwnProperty("hasOwnProperty")); //fasle
console.log(person1.__proto__ === person.prototype); //true
console.log(person.prototype.hasOwnProperty("hasOwnProperty")); //false
console.log(person1.__proto__.__proto__ === person.prototype.__proto__); // true
console.log(person.prototype.__proto__.hasOwnProperty("hasOwnProperty")); //true

我们可以分析这个例子,看看person1对象实例是如何调用hasOwnProperty()这个方法的.

  1. 首先person1对象实例中寻找hasOwnProperty()方法,person1.hasOwnProperty("hasOwnProperty")返回false,发现不存在此方法,这时通过__proto__找到person1的原型对象.
  2. 在person1的原型对象person1.__proto__即person.prototype中寻找hasOwnProperty()方法,person.prototype.hasOwnProperty("hasOwnProperty")返回false,依然没有找到,此时顺着person.prototype的__proto__找到其原型对象.
  3. 在person.prototype原型对象person.prototype.__proto__即Object.prototype中寻找hasOwnProperty()方法,Object.prototype.hasOwnProperty("hasOwnProperty")返回true,由于hasOwnProperty()为Object.prototype内置方法,因此person1顺利找到此方法并调用.

总而言之,实例对象方法调用,是现在实力对象内部找,如果找到则立即返回调用,如果没有找到就顺着__proto__向上寻找,如果找到该方法则调用,没有找到会直接报错,这便是原型链.

如图所示,会更加直观.


4.ES6中的__proto__

虽然__proto__在最新的ECMA标准中被纳入了规范,但是由于__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API.
标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)代替。

1
2
3
4
5
6
7
8
9
10
function person(pname, page) {
this.name = pname;
this.age = page;
}
person.prototype.profession = "football player";
var person1 = new person("Messi", 29);

console.log(Object.getPrototypeOf(person1) === person.prototype); //true
Object.setPrototypeOf(person1, {League: "La Liga"});
console.log(person1.League); //La Liga

以上为具体用法,但是值得注意的是Object.setPrototypeOf()在使用中有一个坑,如代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
function person(pname, page) {
this.name = pname;
this.age = page;
}
person.prototype.profession = "football player";
var person1 = new person("Messi", 29);
var person2 = new person("Bale", 28);

Object.setPrototypeOf(person1, { League: "La Liga"});

console.log(person1.League); //La Liga
console.log(person2.League); //undefind

也就是说不同于直接用person1.__proto__.League = "La Liga";会使得两个实例同时生效,Object.setPrototypeOf()只能生效一个实例对象.

理解JavaScript--作用域

发表于 2018-02-25 | 分类于 JavaScript | 阅读次数

三、作用域


1. 为什么要理解作用域

原因很简单,JavaScript中最重要的一个概念闭包的理解就建立在对作用域的理解之上,而一个对象的的构成往往离不开闭包以及作用域.


2. 动态作用域or静态作用域?

首先我们要搞清楚JavaScript的作用域类型,这有助于我们在分析作用域时的判断.

静态作用域:静态作用域是指声明的作用域是根据程序正文在编译时就确定的,有时也称为词法作用域。

动态作用域:程序中某个变量所引用的对象是在程序运行时刻根据程序的控制流信息来确定的。

大多数现代编程语言都采用的静态作用域,即代码在写出来的时候就已经确定的,并非在执行时再确定,我们可以根据以下代码一探究竟.

1
2
3
4
5
6
7
8
function f() {
console.log(a);
}
function g() {
var a = 7;
f();
}
g(); // a is not defined

这段代码在执行时候会报错,很明显,如果JavaScript采用了动态作用域,a在执行时确定的话,那么以上代码相当于这样:

1
2
3
4
5
6
7
function g() {
var a = 7;
function f() {
console.log(a);
}
}
g(); //undefind

因此,我们可以判断出JavaScript属于静态作用域.


3.函数作用域

函数是存在自身作用域的,在创建函数之初,函数体内就产生了作用域,为了方便理解,我们引用了《你不知道的JavaScript》书中的代码及图例,他会很清晰地帮助我们理解函数作用域.

1
2
3
4
5
6
7
8
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12

由于JavaScript是采用静态作用域,作用域是在函数创建的时候就确定下来的.


4.作用域链

那么,我们可以仔细分析一下这个作用域链.

我们可以看到scope chain通过指向本函数的变量对象,并通过本函数的变量对象与整个父级函数变量对象联系在一起,这就是作用域链.

所以说,作用域链与一个执行上下文相关,是内部上下文所有变量对象(包括父变量对象)的列表,用于变量查询。


5. 块级作用域

在ES2015之前,JavaScript中实际上是没有语法层面的块级作用域,这就造成了很多意外的产生.

1
2
3
4
for (var i = 0; i<3; i++) {

}
console.log(i); //3

如果是在有块级作用域的语言中,i是不会被打印出来的,但是在JavaScript中却被打印出来,这就是变量泄露的情况,也就是说看似在块级作用域的变量泄漏到全局作用域中,这也就造成了全局污染.

在ES5中,人们为了解决这个问题,一般采用立即执行函数IIFE来模拟块级作用域,但是这种写法不易读也不优雅,因此,在ES2015中引入了let,通过let可以创建块级作用域.

let与var在使用上基本是类似的,但是let有三个主要的特点

  • 可创建块级作用域
  • 不存在变量提升
  • 存在暂时性死区

例如上面的代码如果改用let声明,就不存在变量污染全局的情况

1
2
3
4
for (let i = 0; i<3; i++) {

}
console.log(i); //i is not defind

至于其它let的具体用法,可以直接参考《ES6入门教程》.


6.什么是闭包

我们先简单地描述一下闭包:闭包是一个函数读取其它函数变量的桥梁.

我们先从上面这个简单的例子开始

1
2
3
4
5
6
7
8
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12

根据前面所学作用域的概念,函数f2将引用函数f1的变量a并打印,这个嵌套函数中,子函数对父函数中的变量进行了引用,而使得这个引用得以成行的桥梁就是‘闭包’.

  • 很多讲解闭包的文章都用return做实例,值得注意的是,闭包的形成并不一定要有return,只要对其它函数变量产生了引用,就会产生闭包,而return的作用是方便外部访问.

可以看到bar通过作用域链向上寻找到变量,我理解的闭包是一个对象,包含了函数本身以及它引用的上下文环境,本实例函数的闭包可以用这段代码来示意下:
{Funtion:bar, bar.variableObject:{c=12, ...}, foo.variableObject:{b=4, ...},window/global.variableObject:{a=2, ...}}
具体地说,函数A包含函数B,函数B可以获取函数A中的数据,而函数A不能获取函数B中的数据,则函数A作用域以外可以通过调用函数B改变函数A的数据。

闭包只是javascript函数作用域产生的附属品

理解JavaScript--预解释

发表于 2018-02-25 | 分类于 JavaScript | 阅读次数

二、预解释


1.从一个实例开始

仔细阅读以下JavaScript代码,你觉得运行结果会是什么呢?是 1 还是2?

1
2
3
4
5
6
var a= 1;
function f() {
console.log(a);
var a = 2;
}
f();

答案是undefined.

那么到底是什么原因导致了这个让人意外的结果呢?这就要从JavaScript解释阶段说起。


2.JavaScript预解释

我们可以大致把JavaScript在浏览器中运行的过程分为两个阶段预解释阶段(有人说准确的说法是应该是Parser,我们以预解释方便理解) 执行阶段,在JavaScript引擎对JavaScript代码进行执行之前,需要进行预先处理,然后再对处理后的代码进行执行。

我们平时书写的JavaScript代码并不是JavaScript执行的代码(V8引擎读取一行执行一行这种理解是错误的),它需要预解释后,再由引擎进行执行.

具体的解释过程涉及到浏览器内核的技术不属于前端领域,不过我们可以浅显的理解一下V8在处理JavaScript的一般过程:

以上例中的var a = 2;为例,我们一般人的理解为声明了一个值为2的变量a,但是在JavaScript引擎处理时却分为了两个步骤:

  1. 读取var a后,在当前作用域中查找是否有相同声明,如果没有就在当前作用域集合中创建一个名为a的变量,否则忽略此声明继续进行解析.

  2. 接下来,V8引擎会处理a = 2的赋值操作,首先会询问当前作用域中是否有名为a的变量,如果有进行赋值,否则继续向上级作用域询问.


3.JavaScript执行环境

我们上面提到的所谓javascript预解释正是创建函数的执行环境(又称“执行上下文”),只有搞定了javascript的执行环境我们才能搞清楚一段代码在执行过后为什么产生这样的结果。

我们用一段伪代码表示创立的执行环境

1
2
3
4
5
executionContextObj = {
'scopeChain': { /* 变量对象 + 所有父级执行上下文中的变量对象 */ },
'variableObject': { /* 函数参数 / 参数, 内部变量以及函数声明 */ },
'this': {}
}

作用域链(scopeChain)包括下面提到的变量对象(variableObject)和所有父级执行上下文中的变量对象.

变量对象(variableObject)是与执行上下文相关的数据作用域,一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明:
变量;
函数声明;
函数的形参

在有了这些基板概念之后我们可以梳理一下js引擎创建执行的过程:

  • 创建阶段
    • 创建Scope chain
    • 创建variableObject
    • 设置this
  • 执行阶段
    • 变量的值、函数的引用
    • 执行代码

而变量对象的创建细节如下:

  • 根据函数的参数,创建并初始化arguments object
  • 扫描函数内部代码,查找函数声明(Function declaration)
    • 对于所有找到的函数声明,将函数名和函数引用存入变量对象中
    • 如果变量对象中已经有同名的函数,那么就进行覆盖
  • 扫描函数内部代码,查找变量声明(Variable declaration)
    • 对于所有找到的变量声明,将变量名存入变量对象中,并初始化为”undefined”
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

4.变量提升

正是由于以上的处理,产生了大家熟知的JavaScript中的变量提升,具体以上代码的执行过程如以下伪代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// global context
executionContextObj = {
'scopeChain': { ... },
'variableObject': { a: undefined, f: pointer to function f() },
'this': {...}
}
...
}//首先在全局执行环境中声明了变量a以及函数f,此时a虽然被声明,但是尚未赋值
x = 1;
function f() {
executionContextObj {
'scopeChain': { ... },
'variableObject': {
arguments: {},
a: undefined
},
'this': {...}
}
//内部词法环境中声明了变量a,此时a虽然被声明,但是尚未赋值
console.log(a);//此时a需要被被打印出来,在作用域内寻找a变量赋值,于是被赋值undefined
a = 2;
}

我们可以明显看到,a变量在预解释阶段已经被赋值undefined,在执行阶段js是自上而下单线执行,当console.log(a)执行之时,a=2还没有被执行,a变量的值便是预处理阶段被赋予的undefined,


5.函数声明与函数表达式

我们看到,在编译器处理阶段,除了被var声明的变量会有变量提升这一特性之外,函数也会产生这一特性,但是函数声明与函数表达式两种范式创建的函数却表现出不同的结果.

我们先看一个实例,运行以下代码

1
2
3
4
5
6
7
8
9
10
f();
g();
//函数声明
function f() {
console.log('f');
}
//函数表达式
var g = function() {
console.log('g');
};

f成功被打印出来,而g函数出现了类型错误,这是什么原因呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
executionContextObj = {
'scopeChain': { ... },
'variableObject': { f: pointer to function f(), g: undefined},
'this': {...}
}

f();
g();
//函数声明
function f() {
console.log('f');
}
//函数表达式
var g = function() {
console.log('g');
};

我们看到,在预解释阶段函数声明的f是被指向了正确的函数得以执行,而函数表达式g被赋予undefined,undefined无法被当作函数执行因此报错g is not a function.


6.冲突处理

通常情况下我们不会将同一变量变量重复声明,但是出现了类似情况后,编译器会如何处理这些冲突呢?

  1. 变量之间冲突
    执行以下函数:
    1
    2
    3
    var a = 3;
    var a = 4;
    console.log(a);

结果显而易见,后声明变量值覆盖前者的值

  1. 函数之间冲突
    1
    2
    3
    4
    5
    6
    7
    8
    f();
    function f() {
    console.log('f');
    }

    function f () {
    console.log('g');
    };

结果同变量冲突,后者覆盖前者.

  1. 函数与变量之间冲突
1
2
3
4
5
6
console.log(f);

function f() {
console.log('f');
}
var f ='g';

结果如下,函数声明将覆盖变量声明.

[Function: f]

7.ES6中的let

在ES6中出现了两个最新的声明语法let与const,我们以let为例,进行测试看看与var的区别.

1
2
3
4
5
function f() {
console.log(a);
let a = 2;
}
f(); // ReferenceError: a is not defined

这段代码直接报错显示未定义,let与const拥有类似的特性,阻止了变量提升,当代码执行到console.log(a)时,执行换将中a还从未被定义,因此产生了错误.

理解javaScript--类型判断

发表于 2018-02-25 | 分类于 JavaScript | 阅读次数

一、类型判断

  

JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol

JavaScript引用类型:Object


1.原始类型与引用类型

1.1

  原始类型又被称为基本类型,原始类型保存的变量和值直接保存在栈内存(Stack)中,且空间相互独立,通过值来访问,通过一个例子来解释.

1
2
var person = 'Messi';
var person1 = person;

上述代码在栈内存的示意图是这样的,可以看到,虽然person赋值给了person1.但是两个变量并没有指向同一个值,而是person1自己单独建立一个内存空间,虽然两个变量的值相等,但却是相互独立的.

1
2
3
4
5
6
7
var person = 'Messi';
var person1 = person;

var person = 1;

console.log(person); //1
console.log(person1); //'Messi'

上述代码示意图是这样的,person的值虽然改变,但是由于person1的值是独立储存的,因此不受影响.

值得一提的是,虽然原始类型的值是储存在相对独立空间,但是它们之间的比较是按值比较的.

1
2
3
var person = 'Messi';
var person1 = 'Messi';
console.log(person === person1); //true
1.2引用类型

剩下的就是引用类型了,即Object 类型,再往下细分,还可以分为:Object 类型、Array 类型、Date 类型、Function 类型 等。

与原始类型不同的是,引用类型的内容是保存在堆内存中,而栈内存(Heap)中会有一个堆内存地址,通过这个地址变量被指向堆内存中Object真正的值,因此引用类型是按照引用访问的.

1
2
3
4
5
6
7
8
9
10
11
12
var a = {name:"percy"};
var b;
b = a;
a.name = "zyj";
console.log(b.name); // zyj
b.age = 22;
console.log(a.age); // 22
var c = {
name: "zyj",
age: 22
};
console.log(a === c); //false

我们可以逐行分析:

1. `b = a`,如果是原始类型的话,`b`会在栈内自己独自创建一个内存空间保存值,但是引用类型只是`b`的产生一个对内存地址,指向堆内存中的`Object`.
2.`a.name = "zyj"`,这个操作属于改变了变量的值,在原始类型中会重新建立新的内存空间(可以看上一节的示意图),而引用类型只需要自己在堆内存中更新自己的属性即可.
3.最后创建了一个新的对象`c`,看似跟`b` `a`一样,但是在堆内存中确实两个相互独立的`Object`,引用类型是按照**引用比较**,由于`a` `c`引用的是不同的`Object`所以得到的结果是`fasle`.  

2. 类型中的坑

2.1 数组中的坑
数组是JavaScript中最常见的类型之一了,但是在我们实践过程中同样会遇到各种各样的麻烦.

稀疏数组:指的是含有空白或空缺单元的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [];

console.log(a.length); //0

a[4] = a[5];

console.log(a.length); //5

a.forEach(elem => {
console.log(elem); //undefined
});

console.log(a); //[,,,,undefined]

这里需要注意:

  1. 一开始建立的空数组a的长度为0,这可以理解,但是在a[4] = a[5]之后出现了问题,a的长度变成了5,此时a数组是[,,,,undefined]这种形态.
  2. 我们通过遍历,只得到了undefined这一个值,这个undefind是由于a[4] = a[5]赋值,由于a[5]没有定义值为undefined被赋给了a[4],可以等价为a[4] = undefined.

字符串索引

1
2
3
4
5
6
var a = [];
a[0] = 'Bale';
a['age'] = 28;
console.log(a.length); //1
console.log(a['age']); //28
console.log(a); //[ 'Bale', age: 28 ]

数组不仅可以通过数字索引,也可以通过字符串索引,但值得注意的是,字符串索引的键值对并不算在数组的长度里.

2.2 数字中的坑
二进制浮点数

JavaScript 中的数字类型是基于“二进制浮点数”实现的,使用的是“双精度”格式,这就带来了一些反常的问题,我们那一道经典面试提来讲解下.

1
2
3
var a = 0.1 + 0.2;
var b = 0.3;
console.log(a === b); //false

这是个出人意料的结果,实际上a的值约为0.30000000000000004这并不是一个整数值,这就是二进制浮点数带来的副作用.

1
2
3
4
5
6
var a = 0.1 + 0.2;
var b = 0.3;
console.log(a === b); //false
console.log(Number.isInteger(a*10)); //false
console.log(Number.isInteger(b*10)); //true
console.log(a); //0.30000000000000004

我们可以用Number.isInteger()来判断一个数字是否为整数.

NaN

1
2
3
4
var a = 1/new Object();
console.log(typeof a); //Number
console.log(a); //NaN
console.log(isNaN(a)); //true

NaN属于特殊的Number类型,我们可以把它理解为坏数值,因为它属于数值计算中的错误,更加特殊的是它自己都不等价于自己NaN === NaN //false,我们只能用isNaN()来检测一个数字是否为NaN.


3.类型转换原理

类型转换指的是将一种类型转换为另一种类型,例如:

1
2
3
var b = 2;
var a = String(b);
console.log(typeof a); //string

当然,类型转换分为显式和隐式,但是不管是隐式转换还是显式转换,都会遵循一定的原理,由于JavaScript是一门动态类型的语言,可以随时赋予任意值,但是各种运算符或条件判断中是需要特定类型的,因此JavaScript引擎会在运算时为变量设定类型.

这看起来很美好,JavaScript引擎帮我们搞定了类型的问题,但是引擎毕竟不是ASI(超级人工智能),它的很多动作会跟我们预期相去甚远,我们可以从一到面试题开始.

1
{}+[] //0

答案是0

是什么原因造成了上述结果呢?那么我们得从ECMA-262中提到的转换规则和抽象操作说起,有兴趣的童鞋可以仔细阅读下这浩如烟海的语言规范,如果没这个耐心还是往下看.

这是JavaScript种类型转换可以从原始类型转为引用类型,同样可以将引用类型转为原始类型,转为原始类型的抽象操作为ToPrimitive,而后续更加细分的操作为:ToNumber ToString ToBoolean,这三种抽象操作的转换表如下所示

如果想应付面试,我觉得这张表就差不多了,但是为了更深入的探究JavaScript引擎是如何处理代码中类型转换问题的,就需要看 ECMA-262详细的规范,从而探究其内部原理,我们从这段内部原理示意代码开始.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}

var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}

var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}

上面代码的逻辑是这样的:

  1. 如果变量为字符串,直接返回.
  2. 如果!IS_SPEC_OBJECT(x),直接返回.
  3. 如果IS_SYMBOL_WRAPPER(x),则抛出异常.
  4. 否则会根据传入的hint来调用DefaultNumber和DefaultString,比如如果为Date对象,会调用DefaultString.
  5. DefaultNumber:首先x.valueOf,如果为primitive,则返回valueOf后的值,否则继续调用x.toString,如果为primitive,则返回toString后的值,否则抛出异常
  6. DefaultString:和DefaultNumber正好相反,先调用toString,如果不是primitive再调用valueOf.

那讲了实现原理,这个ToPrimitive有什么用呢?实际很多操作会调用ToPrimitive,比如加、相等或比较操。在进行加操作时会将左右操作数转换为primitive,然后进行相加。

下面来个实例,({}) + 1(将{}放在括号中是为了内核将其认为一个代码块)会输出啥?可能日常写代码并不会这样写,不过网上出过类似的面试题。

加操作只有左右运算符同时为String或Number时会执行对应的%_StringAdd或%NumberAdd,下面看下({}) + 1内部会经过哪些步骤:

{}和1首先会调用ToPrimitive
{}会走到DefaultNumber,首先会调用valueOf,返回的是Object {},不是primitive类型,从而继续走到toString,返回[object Object],是String类型
最后加操作,结果为[object Object]1
再比如有人问你[] + 1输出啥时,你可能知道应该怎么去计算了,先对[]调用ToPrimitive,返回空字符串,最后结果为”1”。


本系列主要参考:

  1. JavaScript 类型的那些事

理解promise

发表于 2018-02-20 | 分类于 node | 阅读次数

前言

在学习node的路上,理解异步操作是重点,而处理的方法一直在完善,先有setTimeout,后又promise,貌似目前目前大招是async/await(目前三个都不理解,(捂脸…)),下面就开始吧。
这是在看JavaScript Promise迷你书时记录的笔记,若有错误,敬请指出,感激不尽。
原文Promise迷你书

1、什么是Promise

1.1、常见异步处理

Promise是抽象异步处理对象以及对其进行操作的组件。
Promise最初被提出是在E语言,E语言是基于并列/并行处理设计的一种编程语言。
常见基于javaScript的异步处理大都利用回调函数

1
2
3
4
5
6
7
8
----
//使用回调函数的异步处理
getAsync("fileA.txt", function(error, result){
if(error){
throw error;
}
});
----

1.2、使用Promise进行异步处理

Promise是把类似的异步处理对象和处理规则规范化,并按照统一的接口来编写,而采取规定方法之外的写法都会出错。

1
2
3
4
5
6
7
8
//使用Promise进行异步处理
var promise = getAsyncPromise("fileA.txt");
promise.then(function(result){
//获取文件内容成功时的处理
}).catch(function(error){
//获取文件内容失败时的处理
});
<1>返回promise对象

我们可以向预设了抽象化异步处理的promise对象,注册这个promise对象执行成功和失败时相应的回调函数。
这里与常见异步处理操作不同,需严格遵守规则,无法自由的定义回调函数的参数。
所以,promise的功能是可以将复杂的异步处理轻松地进行模式化

1.3、Promise简介

三种API类型

Constructor

Promise类似XMLHttpRequest,从构造函数promise新建一个promise对象作为接口。

1
2
3
4
5
//创建promise对象
var promise = new Promise(function(res,rej){
//异步处理
//处理完后,调用resolve或reject
});

Instance Method

通过new生成promise对象,promise.then()方法可以设置在resolve(成功)/reject(失败)时调用的回调函数。
promise.then(onFulfilled, onRejected)

resolve(成功)时

onFulfilled 会被调用

reject(失败)时

onRejected 会被调用
只想对异常进行处理时可以采用 promise.then(undefined, onRejected)这种方式,
只指定reject时的回调函数即可。
不过这种情况下 promise.catch(onRejected) 应该是个更好的选择。

Static Method

辅助方法:
promise.all()
promise.resolve()

1.3.1、Promise workflow

1
2
3
4
5
6
7
8
9
10
11
12
function asyncFunction() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Async Hello world');
}, 16);
});
}
asyncFunction().then(function (value) {
console.log(value); // => 'Async Hello world'
}).catch(function (error) {
console.log(error);
});

1.3.2、Promise的状态

用new Promise实例化的promise对象有以下三个状态:

“has-resolution” - Fulfilled

resolve(成功)时。此时会调用 onFulfilled

“has-rejection” - Rejected

reject(失败)时。此时会调用 onRejected

“unresolved” - Pending

既不是resolve也不是reject的状态。也就是promise对象刚被创建后的初始化状态等
状态变化
"状态变化"

promise对象的状态,从Pending转换为Fulfilled或Rejected之后, 这个promise对象的状态就不会再发生任何变化。
也就是说,Promise与Event等不同,在.then后执行的函数可以肯定地说只会被调用一次。
另外,Fulfilled和Rejected这两个只中任一状态都可以表示为Settled(不变的)。

Settled

resolve(成功)或rejected(失败)。

1.4、编写Promise代码

如何写Promise代码。

1.4.1、创建promise对象

流程如下
1.new Promise(fn)返回一个promise对象

2.在fn中指定异步等处理

* 处理结果正常,调用resolve(处理结果值)
* 处理结果错误,调用reject(Error对象)

在此用Promise来通过异步处理方式来获取XMLHttpRequest(XHR)的数据。

创建XHR的promise对象

首先,创建一个用Promise把XHR处理包装起来的名为getURL的函数。

xhr-promise.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
}
});
}

//实例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(function onRejected(error){
console.error(error);
});

//输出结果为请求数据

getURL只有在通过XHR取得结果状态码为200(数据取得成功)时,调用resovle,其他情况(取得失败)时调用reject方法。

成功时

resolve(req.responseText)在response的内容中加入了这个参数,resolve方法把参数传给回调函数(then方法会接收这个参数)。

常见回调函数callback(err, response)的值为err(出错)和response(接收的数据),来处理正常和异常的情况,而在Promise中resolve/reject则担当了这个职责

失败时

当发生错误时,onerror事件被触发,调用reject。

此时,如reject(new Error(req.statusText)),创建一个Error对象并将具体的值传入,传给reject的参数只要是Error对象(或者继承自Error对象)就行。

我的疑问

node要点总结

发表于 2018-02-20 | 分类于 node | 阅读次数

node开发要点

1、常用前端技术:
    html5
    css3
    jQuery
2、js高级话题:
    面向对象
    闭包
    作用域
    设计模式
3、其他后端技术:
    mongodb
    redis
    apache
    nignix
    mysql
4、node内置核心库:
    全局对象
    时事件
    流
    网络
    子程序
5、常用知名第三方库:
    async
    express
    koa
6、node高级话题:
    异步
    部署
    性能调优
    异常调试

node事件循环机制

* javaScript高级话题(面向对象、作用域、闭包、设计模式)

面向对象

1、常用js类定义的方法?

构造函数原型法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
		function Person () {
this.name = 'pinocchio';
}
Person.prototype.sayName = function () {
alert(this.name);
; }
var person = new Person ();
person.sayName();
//对象创建:
var Person = {
name: 'pinocchio',
sayName: function () {
alert(this.name);
}
};
var person = Object.create(Person);
person.sayName();

2、js类继承的方法?

原型链法(Person是Animal的子集,即子类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	function Animal () {
this.name = 'animal';
}
Animal.prototype.sayName = function () {
alter(this.name);
}
function Person () {}
Person.prototype = Animal.prototype; //人继承动物
Person.prototype = constructor = 'Person'; //更新构造函数为人
属性复制法(单纯复制,两者无继承关系):
function Animal () {
this.name = 'animal';
}
Animal.prototype.sayName = function () {
alter(this.name);
};
function Person () {}
//复制动物所有属性到人
for (prop in Animal.prototype) {
Person.prototype[prop] = Animal.prototype[prototype];
}
//更新构造函数为人
Person.prototype.constructor = 'Person';
构造器应用法:
function Animal () {
this.name = 'animal';
}
Animal.prototype.sayName = function () {
alter(this.name);
};
function Person () {
//apply,call,bind方法都行,有细微区别
Animal.call(this);
}

3、js类多重继承的方法怎么实现?

通过类继承里面的属性复制法来实现,可以继承所有父类的prototype属性

作用域

4、js里作用域是什么样?

函数作用域,

1
2
3
4
5
6
7
8
9
var globalVar = 'global var';

function test() {
alert(globalVar); // undefined, 因为globalVar在本函数内被重定义了,导致全局失效,这里使用函数内的变量值,可是此时还没定义
var globalVar = 'overrided var'; // globalVar在本函数内被重定义

alert(globalVar); // overrided var
}
alert(globalVar); // global var,使用全局变量

5、js中this指的是什么?

this指的是对象本身

6、apply,call,bind有什么区别?

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
三者第一个参数都是this要指向的对象,也就是想指定的上下文;
三者都可以利用后续参数传参;
apply,call是直接执行函数调用,bind是绑定,执行需要再次调用.apply和call的区别是apply接受数组作为参数,而call是接受逗号分隔的无限多个参数列表
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person () {}
Person.prototype.sayName() {
alert(this.name);
}
//普通对象,不是Person实例
var obj = {
name: 'pinocchio'
};
//apply
Person.prototype.sayName.apply(obj, [param1, param2, param3]);
//call
Person.prototype.sayName.call(obj, param1, param2, param3);
//bind,先绑定,再执行
var sn = Person.prototype.sayName.bind(obj);
sn([param1, param2, param3]);
sn(param1, param2, param3);

7、caller,callee和arguments分别是什么?

1
2
3
4
5
6
7
8
9
10
11
12
caller,callee之间的关系就像是employer和employee之间的关系,就是调用与被调用的关系,二者返回的都是函数对象引用.arguments是函数的所有参数列表,它是一个类数组的变量.
function parent(param1, param2, param3) {
child(param1, param2, param3);
}

function child() {
console.log(arguments); // { '0': 'mqin1', '1': 'mqin2', '2': 'mqin3' }
console.log(arguments.callee); // [Function: child]
console.log(child.caller); // [Function: parent]
}

parent('mqin1', 'mqin2', 'mqin3');

闭包

8、什么是闭包?闭包有什么用处?

通俗的说,闭包就是作用域范围,因为js是函数作用域,所以函数就是闭包.全局函数的作用域范围就是全局,所以无须讨论.更多的应用其实是在内嵌函数,这就会涉及到内嵌作用域,或者叫作用域链。
这就会带来另外一个问题,什么时候引用结束?如果不结束,就会一直占用内存,引起内存泄漏.不用的时候就引用设为空。    

9、defineProperty, hasOwnProperty, propertyIsEnumerable都是做什么用的?

Object.defineProperty(obj, prop, descriptor)用来给对象定义属性,有value,writable,configurable,enumerable,set/get等.hasOwnProerty用于检查某一属性是不是存在于对象本身,继承来的

父亲的属性不算.propertyIsEnumerable用来检测某一属性是否可遍历,也就是能不能用for..in循环来取到.

设计模式

10、js常用设计模式的实现思路,单例,工厂,代理,装饰,观察者模式等

1) 单例: 任意对象都是单例,无须特别处理

var obj = {name: 'michaelqin', age: 30};

2) 工厂: 就是同样形式参数返回不同的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() { this.name = 'Person1'; }
function Animal() { this.name = 'Animal1'; }

function Factory() {}
Factory.prototype.getInstance = function(className) {
return eval('new ' + className + '()');
}

var factory = new Factory();
var obj1 = factory.getInstance('Person');
var obj2 = factory.getInstance('Animal');
console.log(obj1.name); // Person1
console.log(obj2.name); // Animal1

3) 代理: 就是新建个类调用老类的接口,包一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() { }
Person.prototype.sayName = function() { console.log('michaelqin'); }
Person.prototype.sayAge = function() { console.log(30); }

function PersonProxy() {
this.person = new Person();
var that = this;
this.callMethod = function(functionName) {
console.log('before proxy:', functionName);
that.person[functionName](); // 代理
console.log('after proxy:', functionName);
}
}

var pp = new PersonProxy();
pp.callMethod('sayName'); // 代理调用Person的方法sayName()
pp.callMethod('sayAge'); // 代理调用Person的方法sayAge()

4) 观察者: 就是事件模式,比如按钮的onclick这样的应用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Publisher() {
this.listeners = [];
}
Publisher.prototype = {
'addListener': function(listener) {
this.listeners.push(listener);
},

'removeListener': function(listener) {
delete this.listeners[listener];
},

'notify': function(obj) {
for(var i = 0; i < this.listeners.length; i++) {
var listener = this.listeners[i];
if (typeof listener !== 'undefined') {
listener.process(obj);
}
}
}
}; // 发布者

function Subscriber() {

}
Subscriber.prototype = {
'process': function(obj) {
console.log(obj);
}
}; // 订阅者


var publisher = new Publisher();
publisher.addListener(new Subscriber());
publisher.addListener(new Subscriber());
publisher.notify({name: 'michaelqin', ageo: 30}); // 发布一个对象到所有订阅者
publisher.notify('2 subscribers will both perform process'); // 发布一个字符串到所有订阅者

11、列举数组相关的常用方法

push/pop, shift/unshift, split/join, slice/splice/concat, sort/reverse, map/reduce, forEach, filter

12、列举字符串相关的常用方法

indexOf/lastIndexOf/charAt, split/match/test, slice/substring/substr, toLowerCase/toUpperCase

node核心内置库类(事件, 流, 文件, 网络)

node概览:

1、node架构

主要分为三层,应用app >> V8及node内置架构 >> 操作系统. V8是node运行的环境,可以理解为node虚拟机.

node内置架构又可分为三层: 核心模块(javascript实现) >> c++绑定 >> libuv + CAes + http.

2、node的核心模块

EventEmitter, Stream, FS, Net, 全局对象

node全局对象:

1、node有哪些全局对象?

process、 console、 Buffer

2、process有哪些方法?

process.stdin, process.stdout, process.stderr, process.on, process.env, process.argv, process.arch, process.platform, process.exit

3、console有哪些常用方法?

console.log/console.info, console.error/console.warning, console.time/console.timeEnd, console.trace, console.table

4、node有哪些定时功能?

setTimeout/clearTimeout, setInterval/clearInterval, setImmediate/clearImmediate, process.nextTick

5、node中的事件循环是什么样子的?

process.nextTick >> setImmidate >> setTimeout/SetInterval

6、node中的Buffer如何应用?

Buffer是用来处理二进制数据的,比如图片,mp3,数据库文件等.Buffer支持各种编码解码,二进制字符串互转

EventEmitter:

1、什么是EventEmitter?

EventEmitter是node中一个实现观察者模式的类,主要功能是监听和发射消息,用于处理多模块交互问题.

2、如何实现一个EventEmitter?

主要分三步:定义一个子类, 调用构造函数, 继承EventEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var util = require('util'),	
EventEmitter = require('event').EventEmitter;
//构造函数
function MyEmitter () {
EventEmitter.call(this);
}
//继承
util.inherits(MyEmitter, EventEmitter);

var em = new MyEmitter();
//接受事件
em.on('hello', function(data) {
console.log('收到事件hello的数据:', data);
});
em.emit('hello', 'EventEmitter传递消息真方便');

3、EventEmitter有哪些典型应用?

模块之间传递消息
回调函数内外传递消息
处理流数据
观察者模式发射触发机制

4、怎么捕获EventEmitter的错误事件?

监听error事件即可,若有多个EventEmitter,可用domain来统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
var domain = require('domain');
var myDomain = domain.create();
//接受事件并打印
myDomain.on('error', function(err){
console.log('domain接受到的事件', err);
});
myDomain.run(function() {
var emitter1 = new MyEmitter();
emitter1.emit('error', '错误事件来自emitter1');
var emitter2 = new MyEmitter();
emitter2.emit('error', '错误事件来自emitter2');
});

5、EventEmitter中的newListenser事件有什么用处?

newListener可以用来做事件机制的反射,特殊应用,事件管理等.当任何on事件添加到EventEmitter时,就会触发newListener事件,基于这种模式,我们可以做很多自定义处理.

1
2
3
4
5
6
7
8
9
var emitter3 = new MyEmitter();
emitter3.on('newListener', function(name, listener) {
console.log("新事件的名字:", name);
console.log("新事件的代码:", listener);
setTimeout(function(){ console.log("我是自定义延时处理机制"); }, 1000);
});
emitter3.on('hello', function(){
console.log('hello node');
});

Stream

stream是基于事件EventEmitter的数据管理模式.由各种不同的抽象接口组成,主要包括可写,可读,可读写,可转换等几种类型.

1、Stream有什么好处?

非阻塞式数据处理提升效率,片段处理节省内存,管道处理方便扩展

2、Stream有哪些典型应用?

文件处理、网络交换、数据转换、音视频处理

3、怎么捕获Stream的错误事件?

监听error事件、方法同EventEmitter

4、哪些常用Stream,分别什么时候用?

Readable为可被读流,在座位数据源时使用;
Writeable为可被读流,在作为输出元时使用;
Duplex为读写流,作为输入源接受被写入,同时又作为输入源被后面的流读出;
Transform机制和Duplex一样,都是双向流,区别时Transfrom只需要实现一个函数_transfrom(chunk, encoding, callback);
而Duplex需要分别实现_read(size)函数和_write(chunk, encoding, callback)函数.    

5、实现一个Writable Stream?

三步走:1)构造函数call Writable 2) 继承Writable 3) 实现_write(chunk, encoding, callback)函数

```
var Writeable = require('stream').Writable;
var util = require('util');
//构造函数
function MyWriteable(options) {
    Writeable.call(this, options);
}
//继承Writeable
util.inherits(MyWriteable, Writable);
MyWritable.prototype._write = function(chunk, encoding, callback) {
    // 此处可对写入的数据进行处理
    console.log("被写入的数据是:", chunk.toString()); 
    callback();
};
// stdin作为输入源,MyWritable作为输出源   
process.stdin.pipe(new MyWritable()); 
```            

node高级话题(异步, 部署, 性能调优, 异步调试)

1、node中的异步与同步怎么理解?

node是单线程的,异步是通过一次次的事件循环来实现的。
同步则是阻塞式的io,在高并发环境下会有很大的性能问题,所以同步一般只在基础框架的启动时使用,如:加载配置文件、初始化程序

2、哪些方法可以进行异步流程控制?

1)多层嵌套回调
2)单独为回调写函数,函数里再回调
3)第三方框架:async、q、promise

3、怎么绑定node程序到80端口?

1)sudo
2)Apache/nginx代理
3)操作系统的firewall iptables端口绑定
4)第三方库up

4、有哪些方法可以让node程序遇到错误后自动重启?

1) runit 
2) forever 
3) nohup npm start &
4)up -watch -port 80 server.js

5、怎么充分利用多个cpu?

1个cpu运行一个node程序

6、怎样调节node执行单元的内存大小?

用--max-old-space-size 和 --max-new-space-size 来设置 v8 使用内存的上限

7、有哪些常用方法可以防止程序崩溃?

1) try-catch-finally 
2) EventEmitter/Stream error事件处理 
3) domain统一控制 
4) jshint静态检查 
5) jasmine/mocha进行单元测试

8、怎样调试node程序?

1) 监听错误事件req.on('error', function(){}), 适用EventEmitter存在的情况; 
2) Promise.then.catch(error),适用Promise存在的情况 
3) try-catch,适用async-await和js运行时异常,比如undefined object

Immutable详解及React中实践

发表于 2017-12-17 | 分类于 React | 阅读次数

1、js中数据可变状态

概念:

js中的对象一般是可变的,由于使用了引用赋值,新的对象简单的引用了原始的对象,改变的新对象将影响到原始对
象。
例如:
    
1
2
3
4
foo = {a:1};
bar = foo;
bar.a = 2;
console.log(foor.a); //输出2

优缺点:

优点:节约内存
缺点:当应用复杂后,会造成非常大的隐患

解决办法:

shallowCopy(浅拷贝),deepCopy(深拷贝)
两种方法可将对象置为不可变对象,但同时也造成cpu和内存的浪费

2、Immutable.Data

React笔记

发表于 2017-12-17 | 分类于 React | 阅读次数

数据类型 state/props

state—组件本身的数据
props—从其他组件传入的组件

生命周期

getDefaultProps //获得默认数据
getInitialState //设置State数据
componentWillMount //在component即将挂在到页面上时
render
componentDidMount //ui真正挂载到文档流
componentWillUnmount //文件销毁

组件间通信

widget之间通过props通信
子组件与父组件的通信
事件订阅

React-router

url模式:
browserHistory //yoursite.com/a/b
hashHistory //yoursite.com/#a/b

两种组件:(装载页面的组件)
IndexRouter //默认(没有存url)
Router

写法:
< Link to = “url”>
注释:
页面切换时,组件会被销毁,再次进入时重新加载。若要实现某功能一直运行,不受页面切换的影响,则将该功
能置于根节点上。

事件订阅

//TODO:完善整个概念

webpack笔记

发表于 2017-12-13 | 分类于 webpack | 阅读次数

webpack使用中的错误:

1.npm run dev 出错

打包错误

in ./src/index.js
1
2
3
4
5
Module build failed: TypeError: fileSystem.statSync is not a function
at module.exports (/home/zaizizaizai/computerscience/project/react/ToDoList/node_modules/babel-loader/lib/utils/exists.js:7:25)
at find (/home/zaizizaizai/computerscience/project/react/ToDoList/node_modules/babel-loader/lib/resolve-rc.js:13:9)
at Object.module.exports (/home/zaizizaizai/computerscience/project/react/ToDoList/node_modules/babel-loader/lib/index.js:113:132)
@ multi main

原因及解决方法:

babel-loader7.x版本与webpack1.x不兼容,将babel-loader版本降到6.x

node笔记

发表于 2017-12-13 | 分类于 node | 阅读次数

安装以及升级node,npm:

1.安装nodejs

sudo apt install nodejs-legacy

2.安装npm

sudo apt install npm

3.清楚cache

sudo npm cache clean -f

4.安装n包

sudo npm install -g n

5.获取nodejs稳定版

sudo n stable

6.升级npm

sudo npm install -g npm

1…456
pinocchio

pinocchio

喜欢探索,热爱编程

59 日志
18 分类
42 标签
RSS
Github E-mail
© 2017 - 2020 pinocchio
本站访客数:
博客全站共38.9k字