-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
192 lines (192 loc) · 151 KB
/
search.xml
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[手摸手带你学JS第五篇]]></title>
<url>%2F2019%2F04%2F11%2F%E6%89%8B%E6%91%B8%E6%89%8B%E5%B8%A6%E4%BD%A0%E5%AD%A6JS%E7%AC%AC%E4%BA%94%E7%AF%87%2F</url>
<content type="text"><![CDATA[这是手摸手系列的第五篇文章,这篇文章的大致内容,原型,原型链,继承的知识。我会从js基础、js高级、js-web、js-dom、其中会有许多基础知识理论,以及小练习。还会开个js-game板块,会有一些好玩的游戏。当然此系列也涉及到前端框架的知识。以及实战项目。学习完这个系列保证你会收获很多。更多请关注Github 原型原型的定义:原型是function对象的一个属性,它定义了构造函数制造出来的对象的公共祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。 1function Person(){} 我们先定义一个构造函数,Person.prototype这个属性就是这个构造函数的原型,这个属性是天生就有的,并且这个属性的值也是一个对象。 每一个函数身上都有一个prototype属性,叫做原型。我们可以在原型prototype上添加属性和方法,每一个构造出来的对象都可以继承这些属性和方法。 12345678Person.prototype.name = 'song';Person.prototype.age = 21;Person.prototype.money = 200;let person = new Person();console.dir(person.name);//songconsole.log(person.age);//21 虽然每一个对象都是独立的,但是他们都有共同的祖先,当我们访问这个对象的属性的时候,如果它没有这个属性,就会向上找到它的原型,然后在原型上访问这个属性。 12345678910function Person(){ this.money = 100;}Person.prototype = { money:200;}let p1 = new Person()console.log(p1.money)//100delete p1.money;console.log(p1.money)//200 这里我们p1对象因为自身有一个money属性,所以就不会到原型上去寻找money属性,而是查询自身的money属性,因此打印的是100,但是当我们删除了自身的money属性之后,它就会到原型上去寻找money这个属性,因此就打印200。 12p1.money = 1000;console.log(p1.money)//100 当我们再次给对象添加属性之后,打印money属性就是自身的属性。 利用原型特点和概念,可以提取公有属性。 我们可以把每一个对象都有的公有属性不写在构造函数里面,而是提取到原型上,这样当我们用构造函数构造大量的对象的时候就不需要走多次构造函数里面的赋值语句了,而只需要走一遍,每一个对象调用属性的时候直接上原型上查找就可以了。 对象如何查看原型 我们前面提到过用构造函数构造对象的时候,会隐式创建一个this对象,这个this对象里面有一个默认的属性叫做proto属性,这个属性的值就是指向的这个对象的原型。 1234var this = { //... __proto__: Person.prototype;} 当查找的属性是自身没有的属性的时候,就会先查找__proto__这个属性,然后这个属性指向了原型,所以就到原型上面继续查找属性了 12Person.prototype.money = 100console.log(p1.__proto__.money)//100 注意:prototype是函数的属性,__proto__是对象的属性。 对象如何查看构造自身的构造函数 在prototype里面,有一个隐式的属性叫做constructor,这个属性记录的就是对象的构造器,里面存的就是构造函数。 1console.log(p1.constructor)//function Person(){} 原型链有了原型,原型还是一个对象,那么这个名为原型的对象自然还有自己的原型,这样的原型上还有原型的结构就构成了原型链。 123456789101112131415161718192021Gra.prototype.firstName = 'peng';function Gra() { this.name = 'grandfather'; this.sex = 'male';}var grandfoo = new Gra();grandfoo.word = 'hello';Foo.prototype = grandfoo;function Foo() { this.age = 18; this.money = 1000;}var father = new Foo();function Son() { this.name = 'song';}Son.prototype = father;var son = new Son(); Foo创造出来的每一个对象都继承自grandfoo这个对象,son的每一个对象都继承自father这个由Foo创造出来的对象,这样son就可以继承上面Foo和Gra的所有属性。 当我们查找son上的属性的时候,如果son自身有属性,那么就打印出来,如果没有,就向上查找原型father,如果father上面还有这个属性,那么继续向上查找grandfoo,如果有就输出,如果没有就返回undefined。 123456console.log(son.name);//songconsole.log(son.money);//1000console.log(son.age);//18console.log(son.sex);//maleconsole.log(son.firstName);//pengconsole.log(son.word);//hello 这种链式的查询结构就叫做原型链 那么原型链有没有终点?我们写的Gra是不是这个原型链的终点? 123console.log(Gar.prototype)//{name:"peng"}console.log(Gar.prototype.__proto__)//Object对象console.log(Gar.prototype.__proto__.__proto__)//null 测试可以看出,其实我们的Gra.prototype上面还有原型,这个原型是一个空对象,这个空对象上面就没有了。 其实,绝大部分的对象最终都会继承自Object.prototype这个对象。 1234var obj = {}var obj1 = new Object()obj.__proto__===Object.prototype //trueobj1.__proto__===Object.prototype //true 其实,绝大部分的对象最终都会继承自Object.prototype这个对象。 由此可见,原型链的终点一般是Object.prototype 但是并不是所有的对象都有原型。 创建对象不只有,字面量式和构造函数式,还有第三种方法,Object.create Object.create()方法需要写一个参数,这个参数就是我们这个对象的原型。如果我们想要构造和var obj = {};一样的空对象,那么就需要写: 1var obj = Object.create(Object.prototype) 当然,我们也可以写一个自定义的属性,让它成为原型 12var obj = Object.create({name:'pengsong'})console.log(obj.name);//pengsong 但是,当我们写参数为null的时候,我们就构造出来了一个没有原型的对象。 12var obj = Object.create(null)console.log(obj.__proto__) //undefined undefined null也都没有原型。它们之所以能打印出来,是因为不调用任何方法的,直接打印出来。 原型链上属性的增删改查 其实我们前面一直在使用着这些方法,这里说一下原型上的引用值。当我们通过一个对象改变了原型上的引用值类型的属性的话,那么所有对象的这个属性的值都会随之更改。 12345Person.prototype.arr = [1,2,3]let p1 = new Person()let p2 = new Person()p1.arr.push(4)console.log(p2.arr) //1 2 3 4 删除 1234567Person.prototype.name = "fatcher"function Person(){ this.name = "son"}let p1 = new Person()delete p1.nameconsole.log(p1.name) //fatcher 这个时候p1对象上面没有了name属性,那么依据我们前面说的当自身没有这个属性的时候就会向原型查询这个属性的说法,我们再次删除这个属性是不是就可以删除原型上的属性了? 12delete p1.nameconsole.log(p1.name) //fatcher 然而事实并没有,由此可见,对象并不能删除原型上的属性。 修改 1234567Person.prototype.name = "fatcher"function Person(){}let p1 = new Person()console.log(p1.name)//fatcherp1.name = 'mother'console.log(p1.name)//mother 看起来输出了我们修改后的,但是事实真的如此吗? 12delete p1.nameconsole.log(p1.name)//fatcher 所以上面的代码并没有修改到原型链上的属性,只是因为自身没有这个属性,而添加了一个到自己身上。 原型链总结prototype:每一个函数天生自带一个属性(prototype),prototype属性的值为一个对象,里面默认结构如下: 1234prototype:{ constructor: 该函数本身, __proto__: 指向的是构造这个对象的函数的prototype} __proto__: 除了特殊情况的,每一个对象天生自带一个属性(__proto__),指向的是构造这个对象的构造函数的prototype 因为所有的函数都是内置类Function的实例对象, 换句话说函数也是对象, 所以函数对象里面也有__proto__属性 所谓的原型链就是顺着隐式原型(__proto__)一步一步往上找的. 一图胜过万千语: ps:上图左侧的person构造函数还可以延伸出他的原型链,下面我们会说到。 下面我们写一个例子,帮助理解看如下实例: 12345678910111213141516// 1. 给内置类构造函数的原型上绑定一个自定义属性 : Function.prototype.wife = "小仙女";// 2. 创建一个构造函数用来生产实例function People(name, age) { this.name = name; this.age = age;}console.log(People.prototype.__proto__ === Object.prototype); // 返回值 : trueconsole.log(People.__proto__ === Function.prototype); // 返回值 : trueconsole.log(People.wife); // 返回值 : 小仙女// 3. 实例化一个对象let p1 = new People("Picsong", 22);console.log(p1); // 返回值 : {name: "Picsong", age: 22}console.log(p1.wife); // 返回值 : undefined 1234567891011121314151617181920212223分析过程如下:1. 此时p1对象结构如下:p1 = { name: "Picsong", age: 22, __proto__(隐式原型): People.prototype (所属类的prototype, 而p1就是有People实例化生成的)}2. 此时People函数对象的结构如下:People = { prototype: { constructor: function People() { }, __proto__: Object.prototype(proto属性此时所在的prototype是一个对象, 而且该对象是Object的一个实例对象) }, proto: Function.prototype}3. 在访问p1.wife时, 现在p1自身里面查找(没有), 就会顺着p1.proto属性往上找, 而People.prototype里面也没有则继续沿着People.prototype.proto往上找, Object.prototype里面也没有, 而对象里面访问一个未定义的属性时, 返回的值为undefined. 友情链接:javascript中的原型链 一道题彻底弄懂js继承 javascript中的继承 继承了解了上面的知识之后,其实原型,构造函数最重要的运用就是来实现继承。 面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。 大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。这里我们就来看看常见继承的6种方式。 原型链继承将父类型的实例作为子类型的原型对象,以此构成的链式关系叫做原型链。 12345678910111213141516//父类型function Person(name,age){ this.name = name this.age = age this.arr = [1,2,3] this.setName = function (){}}Person.prototype.getName = function(){}//子类型function Student(money){ this.money = money}Student.prototype = new Person()//就是在这里将子类型的原型变为了父类型的一个实例,作为原型let s1 = new Student(100)let s2 = new Student(200)console.log(s1,s2)//输出如下 原型链式继承的本质是重写子类型的原型对象,代之以一个父类型的实例。 所以子类的实例就可以通过proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性 子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法, 我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。 1234s1.arr.push(4)console.log(s1.arr,s2.arr)console.log(s1.__proto__ === s2.__proto__)//trueconsole.log(s1.__proto__.__proto__ === s2.__proto__.__proto__)//true s1中arr属性发生变化,与此同时,s2中arr属性也会跟着变化。 还有一点:我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后 1234567891011121314151617function Person (name, age) { this.name = name, this.age = age}Person.prototype.setAge = function () { console.log("111")}function Student (price) { this.price = price this.setScore = function () { }}// Student.prototype.sayHello = function () { }//在这里写子类的原型方法和属性是无效的,//因为会改变原型的指向,所以应该放到重新指定之后Student.prototype = new Person()Student.prototype.sayHello = function () { }var s1 = new Student(15000)console.log(s1) 特点 父类新增原型方法/原型属性,子类都能访问到 简单,易于实现 缺点 无法实现多继承 来自原型对象的所有属性被所有实例共享 创建子类实例时,无法向父类构造函数传参 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中 经典继承 经典继承也叫借用构造函数或伪造对象。 由于函数只是在特定环境中执行代码的对象,所以可以通过使用apply()和call()方法改变父类型的构造函数的执行环境,从而达到继承的目的。 这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数 12345678910111213function Person(name, age) { this.name = name, this.age = age, this.setName = function () {}}Person.prototype.setAge = function () {}function Student(name, age, price) { Person.call(this, name, age) // 相当于: this.Person(name, age) /*this.name = name this.age = age*/ this.price = price}var s1 = new Student('Tom', 20, 15000) 这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。 1console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function 特点 解决了原型链继承中子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象) 缺点 实例并不是父类的实例,只是子类的实例 只能继承父类的实例属性和方法,不能继承原型属性和方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能 组合继承 组合继承指的是将原型链和经典继承的技术组合到一起,从而发挥二者之长的一种继承模式。 这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。 12345678910111213141516171819function Person (name, age) { this.name = name, this.age = age, this.setAge = function () { }}Person.prototype.setAge = function () { console.log("111")}function Student (name, age, price) { Person.call(this, name, age) this.price = price this.setScore = function () { }}Student.prototype = new Person()Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的Student.prototype.sayHello = function () { }var s1 = new Student('Tom', 20, 15000)var s2 = new Student('Jack', 22, 14000)console.log(s1) 这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。 优点: 可以继承实例属性/方法,也可以继承原型属性/方法 不存在引用属性共享问题 可传参 函数可复用 缺点:调用了两次父类构造函数,生成了两份实例 组合继承优化1这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。 1234567891011121314151617function Person (name, age) { this.name = name, this.age = age, this.setAge = function () { }}Person.prototype.setAge = function () { console.log("111")}function Student (name, age, price) { Person.call(this, name, age) this.price = price this.setScore = function () { }}Student.prototype = Person.prototype //共享原型模式Student.prototype.sayHello = function () { }var s1 = new Student('Tom', 20, 15000)console.log(s1) 但这种方式没办法辨别是对象是子类还是父类实例化 12console.log(s1 instanceof Student, s1 instanceof Person)//true trueconsole.log(s1.constructor)//Person PS: instanceof 操作符,官方解释是,前一个对象是不是后一个构造函数构造出来的,其实正确的理解应该这样理解:前一个的原型链上有没有后一个的原型。 123456s1 instanceof Person //trues1 instanceof Object //truelet arr = [];arr instanceof Array //truearr instanceof Object //trues1 instanceof Array //false 优点:不会初始化两次实例方法/属性,避免的组合继承的缺点 缺点:没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。而且当子类或者父类任何一个修改了原型都会,引起另一个的变化。 12345Student.prototype.sex = "male"console.log(s1.sex) //malelet p1 = new Person()console.log(p1.sex) //male//可以看到,字类修改了原型后,父类的也跟着变化了。 组合继承优化2借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。 123456789101112131415161718function Person (name, age) { this.name = name, this.age = age}Person.prototype.setAge = function () { console.log("111")}function Student (name, age, price) { Person.call(this, name, age) this.price = price this.setScore = function () { }}Student.prototype = Object.create(Person.prototype)//核心代码Student.prototype.constructor = Student//核心代码var s1 = new Student('Tom', 20, 15000)console.log(s1 instanceof Student, s1 instanceof Person) // true trueconsole.log(s1.constructor) //Studentconsole.log(s1) 这种模式就解决了我们上一个中的问题,现在两个原型之间多了一个对象来做中间层交接,从而做到了,修改原型,不会引起另一个的更改。下面一种继承,原理其实也是和这一种一样的,只是写法的差异。 圣杯模式我们之间看代码: 1234567function inherit(C,P){ //C就是Child,P就是Parent function F(){}; F.prototype = P.prototype; C.prototype = new F(); //这里和上面的原理是一样的,用了一个中间对象做交接, //C.prototype = Object.create(P.prototype) //是一样的。} 我们再优化优化,封装一个完整的函数. 123456789var inherit = (function(){ var F = function(){}; return function(C,P){ F.prototype = P.prototype; C.prototype = new F(); C.prototype.constructor = C; C.prototype.uber = P.prototype;//这里是保存一下,它超类的信息, }}()) 这样通过立即执行函数和闭包,我们既可以使用F函数,又可以不让它出现在我们的真正的继承函数中(即返回的那个函数)。 ES6中class 的继承ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。 需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。 12345678910111213141516171819202122232425262728class Person { //调用类的构造方法 constructor(name, age) { this.name = name this.age = age } //定义一般的方法 showName () { console.log("调用父类的方法") console.log(this.name, this.age); }}let p1 = new Person('kobe', 39)console.log(p1)//定义一个子类class Student extends Person { constructor(name, age, salary) { super(name, age)//通过super调用父类的构造方法 this.salary = salary } showName () {//在子类自身定义方法 console.log("调用子类的方法") console.log(this.name, this.age, this.salary); }}let s1 = new Student('wade', 38, 1000000000)console.log(s1)s1.showName() 优点:语法简单易懂,操作更方便 缺点:并不是所有的浏览器都支持class关键字 参考文章JS继承的实现方式 js继承的常用6种 pandajs的继承的4个阶段 ECMAScript 继承机制实现]]></content>
<categories>
<category>手摸手</category>
</categories>
<tags>
<tag>手摸手</tag>
<tag>JS基础</tag>
<tag>原型</tag>
<tag>继承</tag>
</tags>
</entry>
<entry>
<title><![CDATA[手摸手带你学JS第四篇]]></title>
<url>%2F2019%2F04%2F09%2F%E6%89%8B%E6%91%B8%E6%89%8B%E5%B8%A6%E4%BD%A0%E5%AD%A6JS%E7%AC%AC%E5%9B%9B%E7%AF%87%2F</url>
<content type="text"><![CDATA[这是手摸手系列的第四篇文章,这篇文章的大致内容,在上一篇文章没有说完的执行上下文,以及闭包。我会从js基础、js高级、js-web、js-dom、其中会有许多基础知识理论,以及小练习。还会开个js-game板块,会有一些好玩的游戏。当然此系列也涉及到前端框架的知识。以及实战项目。学习完这个系列保证你会收获很多。更多请关注Github 前言这一篇文章是对上篇文章作用域,作用域链,执行上下文的补充,以及引入一个东西叫做闭包,这里的这些看似纠缠不清,只要理解了他们之间的关系,其实就透彻了。这里先抛出这几个东西到底是啥。 这里引用一下《你不知道的JavaScript》(上卷)第一部分第一章中所说的内容,来解释什么是作用域。 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。更多请在本书中查看,如果需要可以联系我要电子版的书籍。 我们可以把函数的范围叫做作用域,但是不精准,作用域确实是因为函数的产生而产生的,作用域属于一个函数,一个函数的定义产生了一个作用域,这两个是相互绑定的。那么它在哪里呢?我们怎么访问它? 每一个对象都可以有属性和方法,而在javascript中一切又都是对象。所以我们的函数也是一个对象,根据古希腊哲学,三段论那么它就有属性和方法。在函数身上就有一个属性[[scope]]叫做域,这个属性里面存储的就是我们所说的作用域。但是这个属性我们是不能操作的。它仅供javascript引擎存取。 [[scope]]指的就是我们说所的作用域,其中存储了执行上下文的集合。 可能你又有疑问了,执行上下文的集合又是个啥?我们先了解一下执行上下文是啥。 在函数执行时,会创建一个称为执行上下文的对象,一个执行上下文定义了一个函数执行时的环境。 作用域链:上面说到[[scope]]中所存储的执行上下文的集合。这个集合呈链式链接,我们把这种链式链接叫做作用域链 我们先要理清楚这些原理,只需要掌握这篇知识,在开始之前我们要先从javascript编译原理开始详细的在上文所说的书籍中有详细的解释,我这里只是简单的介绍。 预编译预编译,在js运行的时候有一个阶段叫做预编译,也就是上面的创建执行上下文的过程。 js运行三部曲 1.语法分析 2.预编译 3.解释执行 语法分析:js引擎在解析js代码之前,会先通篇扫描一下,找出低级语法错误,比如括号写错。 编译执行:js是一种解释性语言,编译一行执行一行,当语法分析没有问题,并且已经完成预编译阶段之后,就开始解释执行代码。 首先我们要知道一个全局上下文的概念它被保存在全局作用域中。这个是一直存在而且只有一个,当你关闭浏览器是它才随之消失。 全局的预编译Global Object简称GO,它的步骤如下 第一步 : 创建一个GO对象(也就是我们所说的全局上下文). 注 : GO === window 第二步 : 找变量声明, 如果有,就将该变量作为GO对象的属性, 并赋值为undefined. 第三步 : 找函数声明(注 : 一定要区别函数声明和函数表达式), 如果有, 就将该函数名作为GO对象的属性, 值为指向这个函数的一个地址。 函数的预编译发生时间 : 函数准备开始执行的前一刻, 即在函数开始执行时, 函数预编译就完成了。 第一步 : 创建一个AO(Activeaction Object)对象(函数自身的执行期上下文,)。 这个对象里面有一些我们看不到的却存在的隐式属性,比如this:window属性,arguments:[]属性,这个对象用来存放一些属性和方法,这些属性和方法就按照前面的四步来产生。注意var a = function(){}是函数表达式,其中var a是一个变量声明。 第二步 : 找形参和变量声明, 如果有, 就将形参和变量名作为AO对象的属性, 并赋值为undefined。 第三步 : 将实参和形参的值相统一。 第四步 : 在函数体里面找函数声明, 如果有, 就将函数名作为AO对象的属性, 值为指向这个函数的一个地址。 方便理解我们这里用一个例子来代码体现: 12345678910111213141516171819202122232425function test(a, b) { console.log(a);//function a(){} function a() { } a = 222; console.log(a);//222 function b() { } console.log(b);//function b(){} var b = 111; var a;}test(1);/*GO{ test:function test(){}}AO{ a: undefined ==>1==>f a b: undefined ==> f b}*/ 根据上面预编译的过程,我们来分析,一开始全局只有一个函数test在预编译过程中也就是在他执行之前,他只是定义状态。这时他被放在了全局GO里面。然后代码开始执行,test函数执行,在test函数执行前一刻他会生成自己的执行上下文AO,按照步骤他会寻找test函数的形参和变量声明,如果有就将它们作为属性名初始值为undefined然后再进行下一步,将形参和实参统一,所以这里a变量的值从undefined变为了1,然后第四部,找函数声明,这里找到了两个函数,a、b,所以我们现在AO里面的a、b属性的值分别变成了a、b函数。这一步完成后我们的AO对象就创建完成了,当然里面其实还有this,arguments这些,我们这里不讨论,这是代码开始真正执行,第一句语句就是一个输出语句,要输出a,首先它会在自己的AO里面寻找有没有,这里我们显然是有的,它是一个函数a,所以输出是a函数,代码接着运行,因为函数声明其实已经被提到了最上面定义了,所以这里紧接着就是一个赋值语句a=222,然后输出222,输出b,接着又是赋值语句b=111。整个函数的执行过程就是这样的,假如将代码稍稍修改一下: 12345678910111213141516var c = 5;function test(a, b) { console.log(c)}test(1);/*预编译GO{ c:undefined test:function test(){}}AO{ a:undefined==>1 b:undefined}*/ 我们现在不输出a、b了,我们要输出一个c,显而易见我们知道这里肯定可以输出5,因为在以前我们的理解是这样的,要输出一个变量时,他会先在自己的作用域里面寻找,如果没有就会到它所在作用域的上一级去找有没有,就这样一层一层的寻找如果到了全局作用域都没有,就会抛出错误,ReferenceError,那么他是根据什么到上级去找的呢,当我们了解了执行上下文之后,我们知道,其实他是先在自己的AO对象里面寻找,如果没有就去GO里面找了,其实就是因为作用域链的原因才能沿着作用域链去到上一级中。所以我们就在说说这个过程是怎么样的。 作用域、作用域链精解在上面我们了解了执行上下文从创建过程,下面我们就将这3个的联系,弄清楚。 函数的作用域[[scope]]里面存储的是执行上下文集合,作用域链就是因为这个集合呈链式链接的所以称为作用域链,也就是作用域里面放的就是我们所说的作用域链。下面用一个例子来说明: 123456//外层是全局环境function a(){ //...}var glob = 100;a(); a函数定义时它就有了它的作用域[[scope]]里面存储的就是作用域链,不过由于现在还只是定义状态并没有执行里面只放了一个全局的执行上下文GO。这个作用域链里面就是放执行上下文的,所以说是个集合,虽然目前只有一个在里面放着。如下图所示: 然后我们的代码紧接着开始执行。当a函数执行时会产生它自己的执行上下文AO,然后他会把自己的执行上下文放在第0位,全局的就变为了第1位去了,就像一个数组一样,每次产生的都会被放在自己作用域链的头部。 正是因为形成了这样的结构,我们才能够沿着作用域链向上寻找,自己需要的东西。 注意:因为执行上下文,在执行完后就会销毁,a函数又会回到一开始被定义的状态,等待下一次调用。每次调用都会产生一个独一无二的执行上下文。 我们再将例子复杂一点 123456789function a(){ function b(){ var b = 234 } var a = 123 b()}var glob = 100a() 在这个例子中我们在a函数中又定义了一个b函数,前面的都是一样的过程,我们就来看看由于a的执行导致了b定义。b定义是啥样的? 因为b是在a函数的作用域内出生的,所以他一出来就拿到了a的劳动成果,也就是a作用域链。就像是一出生就站在了巨人的肩膀上一样。这也是为什么我们说在内层作用域能够访问到外层作用域的变量的原因,因为内层作用域拿到了外层作用域存储的作用域链,当要查找一个变量的时候,就会先找自己的AO当自己没有,就继续沿着作用域链向上寻找。我们再来看看b执行时的状态。 PS:这里的GO只有一个,a函数中的GO,与b函数中的GO都是一个。同样b函数定义时拿的AO和a函数的AO也是同一个,要验证也很简单,你可以试试在b函数中修改外部a变量。然后在a函数中输出看,有没有变化。 在上面我们说过函数一执行完就会销毁自己的执行上下文。对于上面的代码b函数执行完,其实也标志着a函数的执行结束。意思就是他们会根据顺序一一销毁掉自己的上下文。其实这个过程就是执行上下文的出入栈操作,关于更多执行上下文栈可以看看这篇文章-执行上下文栈-,其实我们的作用域链就是一个栈结构,栈的特点就是先进后出,我们这里b执行完销毁,就是把上图的第0位的链接剪断了。b然后回到了最初的定义状态。等待下一次执行。然后a函数也执行完了,开始销毁自己的执行上下文,但是我们看到在a的AO里面存储了一个b的函数,但是我们a还是要销毁这个执行上下文,这个b也就永远没有了,它不用等待下一次执行了,a销毁了之后,这个b函数会被垃圾回收机制给清除了,然后a又回到了被定义的状态,等待下一次执行,当它执行时,它又会产生一个新的执行上下文,同时因为a的执行又会产生一个b的定义。这又是一个全新的b,这个过程周而复始。 了解了这个原理之后对我们编程有很大的帮助,而接下来一个概念理解起来也会很轻松。就是闭包我们就来说说闭包这个东西。 闭包闭包是怎么产生的呢?其实在上面所说的一个过程中就可以产生闭包,上面我们说的函数执行完毕会销毁自己的执行上下文。那么我们要是不让他销毁呢,这样就可以产生闭包。一句话理解闭包就是:闭包就是能够读取其他函数内部变量的函数。 我们前面提到过,不同作用域之间不能够互相访问,但是我们如果在一个函数内部再定义一个函数,并且这个内部函数与外部函数的变量有关联,那么我们就可以通过返回这个内部的函数,然后来访问外部函数里面的变量。 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 1234567891011121314var add;function a(){ var demo1 = 123; add = function(){ demo1++; } return function(){ console.log(demo1) }}var demo = a();demo();add();demo(); 当函数执行完之后,函数的执行上下文就会被销毁,自然我们就无法访问里面的变量了,但是我们这个函数返回了一个依赖于这个函数的新函数,也就是说这个没有被销毁的新函数的作用域链中还存在着对原本函数的作用域的引用,就导致我们原本的函数的上下文不会被销毁,我们称返回的这个新函数是原本函数的闭包函数。 在上面的例子中,a函数内部有一个全局的函数add和一个局部变量demo1,我们这个把返回函数给了一个全局变量demo进入到了内存中,但是由于这个返回的新函数依赖于本来的a函数,这就导致本来的a函数的上下文不会被销毁。 这里我们的打印函数一共运行了两次,都能打印出来值,说明a函数的demo1变量在函数执行完之后并没有被销毁而是存到了内存中。 其次,add的值是一个匿名函数,而这个匿名函数本身也是一个闭包,所以add相当于是一个setter叠加器,可以在函数外部对函数内部的局部变量进行操作。 我们还是结合图&代码来说明: 1234567891011function a(){ function b(){ var b = 234 console.log(a) } var a = 123 return b}var glob = 100var demo = a()demo() 这个例子就是一个的闭包b函数被保存到了外部,那么他是怎么做到没有让a的执行上下文没有被销毁的呢? 首先b函数是因为a函数执行才被定义出来的,所以b函数定义和a函数执行所产生的作用域链是一样的,所以在图上我们就放在了一起。然后a函数执行完后开始销毁自己的执行上下文,相当于就是把上图a函数指向它的AO的线给切断了,但是我们的b函数里面却保留了对a函数的AO的引用,而且被保存到了外面,a函数的执行上下文,并没有得到释放,b函数依然能够访问它,这里就是形成了闭包。 使用闭包的注意点: 1.由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。 解决方法是,在退出函数之前,将不使用的局部变量全部删除。 2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。 总结: 当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被释放,因为闭包需要它们。 最后做一道闭包的题。 123456789101112131415161718192021function fun(n, o) { console.log(o); return { fun: function (m) { return fun(m, n) } }}let a = fun(0);//输出undefined ,a现在{fun(){// return fun(m,n)// }}a.fun(1);//1,0 //输出0a.fun(2); //0a.fun(3);//0//n=0,fun(0)执行后返回{fun(m){return fun(m,n)}},然后m=1,n=0,执行fun(1,0);n=1,o=0,fun(2,1)执行;n=2,o=1;let b = fun(0).fun(1).fun(2).fun(3);//undefined,0,1,2//n=1let c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,0,1,1]]></content>
<categories>
<category>手摸手</category>
</categories>
<tags>
<tag>手摸手</tag>
<tag>JS基础</tag>
<tag>执行上下文</tag>
<tag>闭包</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vuex基础入门]]></title>
<url>%2F2019%2F04%2F08%2Fvuex%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[通过这篇文章你能快速学习到关于vuex的知识,我们还是围绕官网的vuex教程来学习它的核心概念,如果你了解redux学习起来会感觉异常轻松。 Vuex是什么?Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能,下面我们将围绕下图进行分析。 vuex的核心概念每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同: Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。 我们对上图进行简单的分析,有助于我们接下来的学习。我们从数据出发也就是state,数据再到我们的Vue components,里面又可以通过dispatch触发不同的actions,在我们的actions里面我们可以做很多事,比如调用后台接口请求数据做一些异步的操作,也可以做一些事务性操作,因为在actions中可以拿到全局的任意属性方法。比如我们这里发起请求拿到了数据,要想加到state中,就要遵循上面的第二条,通过提交一个commit,执行对应的mutation方法,把我们的数据加到state中。下面我们就通过一个待办事项例子来学习。 StateVuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 在开始代码示例之前先确保你的项目结构和我一样,使用vue-cli3.0以上版本。并自定义配置安装了vuex,和babel.如图:有一些我们暂时不会用到。 简单解释一下,我们的文件。assets里面存放一些,图片资源,components是放我们一些组件的功能性组件。serve是异步请求的一些方法,store是我们自己创建的文件夹用于管理各种组件的状态。view是一些页面级组件。 store.js是它自带的,具体里面怎么写看下面代码: 1234567891011121314151617181920212223242526272829303132//store.jsimport Vue from 'vue'import Vuex from 'vuex'import todoList from './store/todoList'Vue.use(Vuex)export default new Vuex.Store({ state: { index: 3, filter: "ALL", todos: [ { id: 0, text: "HTML", completed: false, flag: true }, { id: 1, text: "CSS", completed: true, flag: true }, { id: 2, text: "JAVASCRIPT", completed: false, flag: true } ] },}) 仓库定义好了,我们还应该在main.js的实例对象中添加上去。 123456789import Vue from 'vue'import App from './App.vue'import store from './store'Vue.config.productionTip = falsenew Vue({ store, render: h => h(App)}).$mount('#app') 这些基础状态有了我们怎么在组件中,不通过props传值就拿到他们呢?我们先把我们的组件初始化出来。 1234567891011121314151617181920212223242526272829303132<template> <div class="todolist"> <AddTodo/> <Todos /> <Filters/> </div></template><script>import AddTodo from "./AddTodo.vue";import Todos from "./Todos.vue";import Filters from "./Filters.vue";export default { name: "VTodoList", components: { AddTodo, Todos, Filters },};</script><!-- Add "scoped" attribute to limit CSS to this component only --><style scoped>.todolist { width: 350px; margin: 40px auto; background-color: #f5f5f5; padding: 20px;}</style>//父组件,会引入到App.vue中去 接下来是AddTodo组件,为了方便解释,就不把代码拆分了,里面是完整的AddTodo代码,很不巧这个组件没有到我们的仓库去拿状态,但是有拿我们mutation中的方法,我们通过vuex提供的辅助函数,以及辅助辅助函数的方法来实现。这里看不明白可以看看官网的详细解释 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556<template> <div class="addtodo"> <input ref="input" type="text" @keydown="affirm" placeholder="这是vuex版的todolist"> <button @click="handleClick">addTodo</button> </div></template><script>import { createNamespacedHelpers } from "vuex";const { mapState, mapMutations, mapGetters } = createNamespacedHelpers( "todoList");export default { name: "AddTodo", methods: { ...mapMutations(["addTodo"]), handleClick() { this.addTodo(this.$refs.input.value); }, affirm(e) { if (e.code === "Enter") { this.handleClick(); } } }};</script><!-- Add "scoped" attribute to limit CSS to this component only --><style scoped>.addtodo { height: 30px; margin-bottom: 10px;}.addtodo > input { height: 100%; width: 65%; border-radius: 1ch; border: 0; padding: 0 15px;}.addtodo > button { height: 30px; margin-left: 20px; width: 67px; border-radius: 6px; background: #6771f0; color: aliceblue; border: none; cursor: pointer;}.addtodo > button:hover { background: rgba(0, 0, 0, 0.2); color: #6771f0;}</style> Mutation和Getter更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475export default {//这里是我们todoList.js文件,也就是我们从store.js中分离出来的。详细在moduls namespaced: true, state: {//我们的state状态可以通过comptued计算属性中通过辅助函数mapState拿到 index: 3, filter: "ALL", todos: [ { id: 0, text: "HTML", completed: false, flag: true }, { id: 1, text: "CSS", completed: true, flag: true }, { id: 2, text: "JAVASCRIPT", completed: false, flag: true } ] }, //Vuex允许我们在商店中定义“getters”。您可以将它们视为商店的计算属性。 //与计算属性一样,getter的结果基于其依赖性进行缓存, //并且只会在其某些依赖项发生更改时重新进行评估。 getters: { filterData(state) { switch (state.filter) { case "COMPLETED": return state.todos.filter(e => e.completed && e.flag); case "ACTIVE": return state.todos.filter(e => !e.completed && e.flag); default: return state.todos.filter(e => e.flag); } }, total(state) { return state.todos.filter(e => e.flag).length; }, completedTotal(state) { return state.todos.filter(e => e.completed && e.flag).length; } }, //在这里,我们在mutation中定义的方法可以在组件的methods中通过辅助函数mapMutation拿到。 mutations: { completedTodo(state, item) { item.completed = !item.completed; }, addTodo(state, text) { if (text) { state.todos.push({ id: state.index++, text, completed: false, flag: true }); } }, toggle(state, filter) { state.filter = filter; }, removeItem(state, item) { item.flag = !item.flag; }, }, actions: { }} Module由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割,我们在上面已经在store文件夹下创建了todoList.js文件,这就可以看作是一个模块了。对应的我们也应该在store.js中修改: 12345678910import Vue from 'vue'import Vuex from 'vuex'import todoList from './store/todoList'Vue.use(Vuex)export default new Vuex.Store({ modules: { todoList, }}) 这也是我们前面为什么用到了帮助我们使用正确的辅助函数的原因,现在通过this.$store.state已经拿不到对应的属性了。 import { createNamespacedHelpers } from “vuex”; const { mapState, mapMutations, mapGetters } = createNamespacedHelpers( “todoList”//这就是我们分割出的子模块 ); ActionsAction 类似于 mutation,不同在于: Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。 Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 store里面存在的任何东西。这里我们暂时没有用到。这里我把我们的demo的代码都发出来,:Todos.vue 文件 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889<template> <ul class="ulBox"> <li :key="item.id" v-for="item in todos" :class="{completed:item.completed}" @click="completedTodo(item)" > {{item.text}} <i @click="removeItem(item)">删</i> </li> </ul></template><script>import { createNamespacedHelpers } from "vuex";const { mapState, mapMutations, mapGetters } = createNamespacedHelpers( "todoList");export default { name: "Todos", methods: { // completedTodo(item) { // // console.log(this); // this.$store.commit('completedTodo',item) // }, // removeItem(item) { // this.$store.commit("removeItem", item); // } //两种写法,使用辅助函数。 ...mapMutations(["completedTodo", "removeItem"]) }, computed: { // todos(){ // return this.$store.getters.filterData // } ...mapGetters({ todos: "filterData" }) }};</script><!-- Add "scoped" attribute to limit CSS to this component only --><style scoped>.completed { color: red; text-decoration: line-through;}li { position: relative; color: #333; cursor: pointer;}li:hover { background: #fef3f3;}.ulBox i { font-size: 10px; position: absolute; right: 10px; top: 3px; border: 1px solid red; font-style: normal; color: #f40; padding: 0 3px; transition: all 1s; opacity: 0;}.ulBox li:hover i { opacity: 1;}.ulBox { list-style: none; margin: 10px 0; background: #fff; border: 1px solid #f5f5f5; padding: 5px; box-sizing: border-box; max-height: 200px; overflow: auto;}.ulBox > li { height: 20px; margin: 5px; border-bottom: #d57979 1px dashed;}</style> Filters底部按钮组件, 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100<!--<template> <div class="filters"> <template v-for="item in filters"> <span :style="{color:'red'}" v-if="item===filter" :key="item">{{item}}</span> <a href="#" v-else @click.prevent="$emit('toggle',item)" :key="item">{{item}}</a> </template> {{completedTotal}}/{{total}} </div> </template>--><script>import { createNamespacedHelpers } from "vuex";const { mapState, mapMutations, mapGetters } = createNamespacedHelpers( "todoList");export default { name: "Filters", data() { return { filters: ["ALL", "COMPLETED", "ACTIVE"] }; }, computed: { ...mapState(["filter"]), ...mapGetters(["completedTotal", "total"]) }, methods: { ...mapMutations(["toggle", "addTodo"]) }, render(h) { let _this = this; //render类似react中的render,也可以用jsx语法,参数h其实是createElement和React.createElement return ( <div class="filters"> {this.filters.map(item => { if (this.filter === item) { return ( <span style={{ color: "red" }} key={item}> {item} </span> ); } return ( <a href="#" key={item} onClick={e => { e.preventDefault(); this.toggle(item) }} > {item} </a> ); })} {this.completedTotal}/{this.total} </div> ); }};</script><!-- Add "scoped" attribute to limit CSS to this component only --><style scoped>.filters > * { margin-right: 10px; margin-top: 10px;}.filters { color: chocolate;}.filters > span { display: inline-block; height: 25px; line-height: 25px; padding: 4px 10px; text-align: center; border: 1px solid #f40; border-radius: 5px; opacity: 0.7;}.filters > a { display: inline-block; height: 25px; padding: 5px 10px; line-height: 25px; text-align: center; border-radius: 5px; background: #6771f0; text-decoration: none; color: aliceblue; cursor: pointer;}.filters > a:hover { background: rgba(0, 0, 0, 0.2); color: #6771f0;}</style> 以上就是vuex的一些基础知识了,希望对你有一些帮助。如果你不想复制代码的话也可以到我的github仓库去拿我把它放在了Vue这个仓库在管理。]]></content>
<categories>
<category>Vue</category>
</categories>
<tags>
<tag>Vue</tag>
<tag>vuex</tag>
</tags>
</entry>
<entry>
<title><![CDATA[手摸手带你学JS第三篇]]></title>
<url>%2F2019%2F04%2F08%2F%E6%89%8B%E6%91%B8%E6%89%8B%E5%B8%A6%E4%BD%A0%E5%AD%A6JS%E7%AC%AC%E4%B8%89%E7%AF%87%2F</url>
<content type="text"><![CDATA[这是手摸手系列的第三篇文章,这篇文章的大致内容,作用域,作用域链,执行期上下文。我会从js基础、js高级、js-web、js-dom、其中会有许多基础知识理论,以及小练习。还会开个js-game板块,会有一些好玩的游戏。当然此系列也涉及到前端框架的知识。以及实战项目。学习完这个系列保证你会收获很多。更多请关注Github 前言JavaScript中有一个被称为作用域(Scope)的特性。还有一个上下文的东西,作用域和上下文也是Javascript程序员在开发中经常迷惑的地方。我会尽我所能用最简单的方式来解释作用域,作用域链,上下文之间的差别和联系。理解作用域将使你的代码脱颖而出,减少错误,并帮助您使用它强大的设计模式。 什么是作用域?你是否担心面试被人问到什么是作用域而答不上来。请看下面的解释。 作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。可能你觉得还是不好理解,作用域就是变量和函数的可访问范围,或者说变量或函数起作用的区域。 1234567var b = 1;function test(){ var a = "内层变量"; console.log(b)//1}test()console.log(a)//Uncaught ReferenceError: a is not defined 从上面的例子可以体会到作用域的概念,变量a没有在全局作用域中声明,所以在全局作用域下引用会报错。但是b是在全局声明了的,所以我们可以在test函数的内部拿到它外部的变量b。我们可以这样理解:作用域就是一个独立的地盘,让内部的变量在外部不能直接访问,但是在作用域内部能访问它外层的作用域内的变量它带来的好处就是 隔离变量,不同作用域下同名作用域不会有冲突。在es6之前javascript只有全局作用域和函数作用域,没有块级作用域。es6之后有了新的块级作用域,可通过let const命令来创建出块级作用域,更多请移步我上一篇文章,有详细说块级作用域 ,这里我们就只说全局作用域和函数作用域了。 全局作用域当您开始在文档中编写JavaScript时,您已经在全局作用域中了。全局作用域贯穿整个javascript文档。如果变量在函数之外定义,则变量处于全局作用域内。在代码中任何地方都能够访问到的对象拥有全局作用域,一般来说以下几种情况会拥有全局作用域。 最外层函数和最外层函数外面定义的变量拥有全局作用域 12345678910111213var outVariable = "我是最外层变量"function outFoo() { var inVariable = '我是内层变量' function innerFoo() { //内层函数 console.log(inVariable); } innerFoo()}console.log(outVariable);//我是最外层变量outFoo()//内层变量console.log(inVariable);//inVariable is not definedinnerFoo()//innerFoo is not defined 如果任何变量未经声明就赋值使用(暗示全局变量),此变量就会成为全局对象window所有,并且成为window对象的一个属性。 1234567function outFoo2(){ let a = 1; b = 2;//变量b未经声明就直接赋值了。}outFoo2()console.log(b)//2console.log(a)//a is not defined 所有window对象的属性拥有全局的作用域。 12345window.a = 123;console.log(window.a === a);//trueb = 123;console.log(window.b);//123console.log(window.b===b)//true 以下是扩展知识,暗示全局变量等等。 一切var声明的全局变量,都是window的属性,意思就是let const声明的不会 1234var a = 123;console.log(window.a);//123let b = 456;console.log(window.b);//undefined 这样看不论是全局变量有没有声明,似乎都会成为全局对象上的属性那么两者之间的区别是什么呢? 区别在于:经过声明的全局变量不能通过delete操作来删除,但是未经声明的全局变量可以被删除。 1234567891011a = 123;console.log(a);//123console.log(window.a);//123console.log(window.a === a);//truedelete a;console.log(window.a);//undefinedconsole.log(a);//ReferenceError a is not definedvar a = 123;delete window.a;console.log(windeow.a);//123 这就导致我们总是在无形中就声明一些全局变量,a是经过声明的,b是暗示全局变量 123456789101112function f() { var a = b = 0;}f();// console.log(a);//报错 a is not definedconsole.log(b);//0+function test() { let a = b = 1; console.log(a);//1 console.log(b);//1}();console.log(b);//1 全局作用域的一个弊端就是:如果我们写了很多js代码,变量定义在全局作用域中,这样就很容易引起变量污染。命名冲突发生 函数作用域函数内定义的变量在局部(本地)作用域中。而且每个函数都具有不同的作用域。这意味着具有相同名称的变量可以在不同的函数中使用。这是因为这些变量被绑定到它们各自具有不同作用域的相应函数,并且在其他函数中不可访问。其实上面的例子都提到过了。 注意:只有函数的大括号{}内才会创建一个作用域,普通的块语句{}不会创建。 1234if(2333){ var name = "zhangsan"//这里依然在全局作用域中}console.log(name)//zhangsan 那么什么又是作用域链?什么是自由变量首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。 1234567var a = 100function fn() { var b = 200 console.log(a) // 这里的a在这里就是一个自由变量 console.log(b)}fn() 什么是作用域链作用域链(Scope Chain)是javascript内部中一种变量、函数查找机制,它决定了变量和函数的作用范围,即作用域。 每一个作用域都有一条对应的作用域链,链头是全局作用域,链尾是当前函数的作用域。 当JavaScript需要查找变量X的时候(这个过程称为变量解析),它首先会从自己作用域链的尾部也就是当前自己的作用域进行查找是否有X属性,如果 没有就沿着作用域链继续查找,直到找到链的开头全局作用域,任未找到的话就认为这段代码的作用域链上不存在X变量,并抛出一个引用错误(ReferenveError)的异常 123456789101112var a = 100function F1() { var b = 200 function F2() { var c = 300 console.log(a) // 自由变量,顺作用域链向父作用域找 console.log(b) // 自由变量,顺作用域链向父作用域找 console.log(c) // 本作用域的变量 } F2()}F1() 关于自由变量的取值关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。 1234567891011var x = 10function fn() { console.log(x)}function show(f) { var x = 20 (function() { f() //10,而不是20 })()}show(fn) 在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取,无论fn函数将在哪里调用。 所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切:要到创建这个函数的那个域”。 作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记——其实这就是所谓的”静态作用域” 1234567891011var a = 10function fn() { var b = 20 function bar() { console.log(a + b) //30 } return bar}var x = fn(), b = 200x() //bar() fn()返回的是bar函数,赋值给x。执行x(),即执行bar函数代码。取b的值时,直接在fn作用域取出。取a的值时,试图在fn作用域取,但是取不到,只能转向创建fn的那个作用域中去查找,结果找到了,所以最后的结果是30 作用域与执行上下文许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。我以前也是将这两个以为是同一个东西,叫法不一样而已。所以我们这里就来说说他们的区别,以便于更好的理解这两个很重要的概念。 作用域的概念在上面已经说了,就是变量的可访问的范围。这里我们来说下执行上下文(Execution Contexts)。 是我们js代码运行的一个执行环境,当解析器进入ECMAScript的可执行代码,解析器就进入一个执行环境,活动的执行环境组成一个逻辑上的栈,在这个逻辑栈顶部的执行环境是当前运行的执行环境。 执行上下文可以被看成一个对象,这个对象就是用来管理其对应作用域中的各个数据,这些数据就是对象中的属性 如果觉得太长了,你可以直接到最后看总结。 我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样 解释阶段 词法分析 语法分析 作用域规则确定 执行阶段 创建执行上下文 执行代码 垃圾回收 JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。 作用域和执行上下文之间最大的区别是:执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。 一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。关于更多执行上下文的知识,我会在下一篇文章中来述说,还有闭包。链接…没有请自行在导航条搜索0.0 总结总结的目的是,帮助我们更好的理解掌握知识,上面说到了作用域,作用域链,执行上下文。这里篇幅有限没法把所有都讲到位,回顾一下。 作用域:就是变量、函数的可访问的范围,作用域分为全局作用域和局部作用域,作用域可以嵌套,处于内层作用域能够访问它外层的变量或函数 作用域链:就是因为作用域的嵌套关系,一层一层的互相链接从而形成的链式结构,每一个作用域都有一个作用域链,链的开头都是全局作用域 执行上下文:是代码的运行环境,在函数执行的前一刻创建的(关于内部发生了什么下一篇重点讲解)是一个对象,内部保存了当前函数的作用域链,及this(上下文),arguments这些 ps:我看到网上有把执行上下文等同于 this 的文章,其实 this 的值是通过当前执行上下文中保存的作用域(对象)来获取到的,规范如下。 123456ResolveThisBinding ( )The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context. ResolveThisBinding performs the following steps: 1.Let envRec be GetThisEnvironment( ). 2.Return envRec.GetThisBinding(). 作用域与执行上下文的区别: 这也是这篇文章主要说明的一个问题,区别如下: 作用域是在函数定义时就确定了,并且不会改变,是静态的,一个作用域下可能包含若干个执行上下文环境。有可能从来没有过执行上下文环境(函数从来就没有被调用过) 执行上下文是在函数执行时创建的,随时可能改变。每个执行上下文中都保存了该函数执行时所在的作用域的作用域链。执行上下文再函数执行完毕后就会销毁(闭包除外)作用域不会因为函数执行完毕而释放。 以上是我个人的理解和看法,有错误欢迎指出,学习就是在不断的交流中进步的过程。 下面这张图有助于我们理解: 参考链接什么是作用域和执行上下文 执行上下文对象的原理及使用 深入理解javascript中的作用域和上下文 js作用域和作用域链]]></content>
<categories>
<category>手摸手</category>
</categories>
<tags>
<tag>手摸手</tag>
<tag>JS基础</tag>
<tag>执行上下文</tag>
<tag>作用域</tag>
</tags>
</entry>
<entry>
<title><![CDATA[手摸手带你学JS第二篇]]></title>
<url>%2F2019%2F04%2F07%2F%E6%89%8B%E6%91%B8%E6%89%8B%E5%B8%A6%E4%BD%A0%E5%AD%A6JS%E7%AC%AC%E4%BA%8C%E7%AF%87%2F</url>
<content type="text"><![CDATA[这是手摸手系列的第二篇文章,每篇文章的大致内容,是变量的声明的知识(不要小看这个)。我会从js基础、js高级、js-web、js-dom、其中会有许多基础知识理论,以及小练习。还会开个js-game板块,会有一些好玩的游戏。当然此系列也涉及到前端框架的知识。以及实战项目。学习完这个系列保证你会收获很多。更多请关注Github var、let 及 const 区别 涉及知识:什么是提升?什么是暂时性死区?var、let 及 const 区别?块级作用域又是什么? var对于这个问题,我们应该先来了解提升(hoisting)这个概念。 12console.log(a) // undefinedvar a = 1 从上述代码中我们可以发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量而没有报错。这种情况就叫做提升,并且提升的是声明。 对于这种情况,我们可以把代码这样来看 123var aconsole.log(a) // undefineda = 1 接下来我们再来看一个例子 123var a = 10var aconsole.log(a) 对于这个例子,如果你认为打印的值为 undefined 那么就错了,答案应该是 10,对于这种情况,我们这样来看代码 1234var avar aa = 10console.log(a) 到这里为止,我们已经了解了 var 声明的变量会发生提升的情况,其实不仅变量会提升函数也会被提升。 123console.log(a) // ƒ a() {}function a() {}var a = 1 对于上述代码,打印结果会是 ƒ a() {},即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升。 说完了这些,想必大家也知道 var 存在的问题了,使用 var 声明的变量会被提升到作用域的顶部,接下来我们再来看 let 和 const 。 letEs6新增了let命令,用来声明变量。它的用法类似var,但是所声明的变量,仅在let命令所在的代码块内有效。不存在在变量提升,以及可重复声明。我们先看一个例子来大致了解一下let命令结束。 12345678{ let a = 10; var b = 12;}var c = c;//不报错,因为预编译var变量会提升,值为undefined,所以执行的时候将undefined赋值给了c变量。let c = c;//这句话在执行时,会报错,c is not defined.console.log(a);//ReferenceError: a is not definedconsole.log(b);//12 这里解释下,一个花括号包起来的起来的地方并且内部有使用let声明变量,就可以看成是一个块级作用域。所以let声明的a只在括号内有效。所以在括号外使用是会报错的。 不存在变量提升1234console.log(a);//undefinedconsole.log(b);//b is not definedvar a = 1;let b = 2; 不允许重复声明1234567891011121314let不允许在同一作用域内,重复声明同一个变量。let a = 1;let a = 12;//SyntaxError a has ...been ....//因此不能再一个函数内部重新声明参数function foo(arg) { let arg;//SyntaxError a has ...been ....}foo();function fun(arg){ { let arg;//不报错 }}fun() 暂定性死区只要块级作用域内存在let命令,它所声明的变量就‘绑定’这个区域,不再受外部的影响 12345var tmp = 123;if (true) { tmp = 'abc';//ReferenceError tmp is not defined let tmp;} 原因上面的代码中,存在全局变量tmp,但是块级作用域内又let了一局部变量tmp,导致后者绑定这个块级作用域,所以在let声明前,对tmp赋值会报错。 ES6明确规定如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成封闭作用域。凡是再声明之前就使用这些变量,就会报错。 总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。在语法上叫做暂时性死区(TZD) 123456789101112if (1) { //tzd开始 //tmp = '1230'; //ReferenveError //console.log(tmp);//ReferenceError let tmp;//tzd结束 console.log(tmp);//undefined tmp = 123; console.log(tmp);//123} therefore因为这个暂时性死区,typeof 也不再是一个百分百安全的操作 12console.log(a)//ReferenceErrorconsole.log(typeof a);//未经声明就操作一个变量,这里因为是typeof所以不报错,//undefined 但是 12typeof x;//ReferenceError x is not definedlet x;//因为使用了let在它之前使用它都是死区。 还有一些隐藏的死区,不易被发现 1234567891011function bar(x = y, y = 1) { return [x, y]}bar();//这里因为参数x的默认值等于另一个参数y, 而此时y还没有声明,属于死区。function bbr(y = 1, x = y) { console.log(y); console.log(x);}bbr();//这样就不会报错 块级作用域es5只有函数作用域和全局作用域,没有块级作用域,这带来很多不合理的场景。 第一种场景,内层变量可能会覆盖外层变量 1234567891011121314151617var tmp = new Date();function f() { console.log(tmp); if (null) { var tmp = 'hello world'//改成 let tmp = 'hello world'就生成一个块级作用域,便不会提升它。 }}f();//undefined//原因是变量提升, 预编译过程中f的AO中{ tmp: undefined }, 导致了内层的变量tmp覆盖了外层的tmp变量。 第二种场景,用于计数的for循环遍历泄露为全局变量。 1234for (var i = 0; i < 10; i++) {}console.log(i);//10 ES6的块级作用域 let实际上为javascript新增了块级作用域。 123456789101112//let实际上为javascript新增了块级作用域。function f1() { let n = 1; if (1) { let n = 10; } console.log(n);//1}f1();//而且允许块级作用域的任意嵌套。而且内层作用域可以定义与外层作用域同名的变量。{ { { { { let q = 123 } let q = 234 } } } }; 块级作用域与函数声明 函数能不能再块级作用域之中声明,是一个相当令人混淆的问题。 ES5规定,函数只能再顶层作用域和函数作用域之中声明,不能再块级作用域声明。 1234567891011if (1) { function f() { }}try { function f1() { }} catch (e) {}//这两种代码的函数声明,根据ES5的规定都是非法的。//但是浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域中声明函数,因此上面两种不会报错,不过严格模式会。 ES6引入了块级作用域,明确允许在块级作用域之中声明函数 12345'use strict';if(12){ function a(){}} //不会报错 es6规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。 1234567891011function f() { console.log('I am outside!');}(function () { if (false) { function f() { console.log('i am inside!'); } } f();//但是这段代码在chrome会报错。Uncaught TypeError: f is not a function})(); 在es5中会因为函数提升得到i am inside,es6中则为i am outside 12345678//es5实际运行的是, 由于函数提升(function () { function f() { console.log('i am inside'); } if (false) { } f();}) es6由于规定,在块级作用域中声明的函数类似与let.对作用域之外没有影响,不会提升 123(function () { f();//if由于不满足条件,直接没写了,只剩下函数执行,得到i am outside})() 在chrome会报错,是因为运行的是下面的代码。 1234567(function () { var f = undefined; if (false) { function f() { } } f();//f is not a function})() 所以考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要也应该写成函数表达式。 123456{ let a = 123; let f = function () { console.log(a); }} 另外还有一个地方需要注意的是,es6的块级作用域允许声明函数的规则,只在使用大花括号的情况下才成立,如果没有就会报错。 12345678//不报错if (1) { function f() { }}//报错if (12) function f() { } 补充do表达式本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。 12345678910{ let t = f(); t = t * y + 1}//在这个代码中块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量//现在有一个提案使得块级作用域可以变为表达式,也就是说可以返回值,就是加上do, 使之变为do表达式let x = do { let t = f(); t * t + 1} constconst跟let一样是es6中新的声明方法,很多的特性跟let是一样的。 这里就说一个他的差别,就是const声明的变量的不可变性。 const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。 但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。 12345678const foo = {};// 为 foo 添加一个属性,可以成功foo.prop = 123;foo.prop // 123// 将 foo 指向另一个对象,就会报错foo = {}; // TypeError: "foo" is read-only 在对象中添加属性,是在堆中该对象的数据里添加数据,而没有改变obj中存放的指向该对象的地址,所以是可以执行成功的,而对obj重新赋值的操作则改变了obj的指针指向,故而操作失败,抛出错误。 对于基本类型也是同样,因为基本类型的数据直接就存放在栈中,常量名直接指向这个地址上的数据,一旦改变值,就会导致指针地址发生改变,所以造成了无法改变值的假象。]]></content>
<categories>
<category>手摸手</category>
</categories>
<tags>
<tag>手摸手</tag>
<tag>JS基础</tag>
<tag>变量声明</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入vue响应式原理]]></title>
<url>%2F2019%2F04%2F07%2F%E6%B7%B1%E5%85%A5vue%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86%2F</url>
<content type="text"><![CDATA[深入vue响应式原理 什么是vue响应式呢?Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,Vue 的响应式原理是使Object.defineProperty 追踪依赖,当属性被访问或改变时通知变化。 是怎样追踪变化的呢?当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。或者使用Proxy 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。 检查变化的注意事项受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。例如: 12345678910var vm = new Vue({ data:{ a:1 }})// `vm.a` 是响应的vm.b = 2// `vm.b` 是非响应的 声明响应式属性由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明根级响应式属性,哪怕只是一个空值: 123456789var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>'})// 之后设置 `message`vm.message = 'Hello!' 不足之处 不能检测到增加或删除的属性 数组方面的变动,如根据索引改变元素,以及直接改变数组长度时的变化,不能被检测到。 原因差不多,无非就是没有被 getter/setter 。 第一个是因为只有在初始化时才会对对象进行代理,转换为getter/setter 第二个如果你知道数组的长度,理论上是可以预先给所有的索引设置 getter/setter 的。但是一来很多场景下你不知道数组的长度,二来,如果是很大的数组,预先加 getter/setter 性能负担较大。 现在有一个替代的方案 Proxy,也是我们下面的小栗子用到的 我们模拟实现vue的响应式,以及他的异步更新队列首先我们定义一下页面的基础结构。其中data-on是我们自定义的一个属性里面的值就是我们data中的属性。i-model是我们模拟的v-model实现数据的双向绑定。 12345678910111213141516<div id="app"> <div data-on="msg"></div> <div data-on="msg"></div> <div data-on="msg"></div> <div data-on="msg"></div> <a href="#" data-on="a"></a> <a href="#" data-on="a"></a> <a href="#" data-on="a"></a> <h1 data-on="count"></h1> <h1 data-on="count"></h1> <h1 data-on="count"></h1> <input type="text" i-model="a"> <input type="text" i-model="msg"> <input type="text" i-model="count"> <button id="btn">点一下</button></div> 然后我们写一个类,来构造实例。 12345678910111213class Reactive { constructor({ el, data } = {}) { //el是挂载点,data是用户的数据 this._el = document.querySelector(el); let _data = data(); //_ob是我们返回的观察者对象。具体我们再下面实现 this._ob = this.createObserve() //这个就是生成代理对象的方法。也就是把对象上的属性转换成getter/setter以达到对属性进行监听, this.restoreProxy(_data); //最后我们返回这个代理对象。我们一切的操作都是对代理对象进行的 return this._proxy; }} 构造函数写好了,自然就要new出实例了。根据要接受的参数,我们这样写。有三个属性,再new的时候其实代理对象也创建好了,并返回给我们R1 12345678910let R1 = new Reactive({ el: "#app", data() { return { count: 0, msg: "hello", a: 11, } }, }) 接下来我们继续再我们的Reactive类中实现我们的observe对象,在我们这个对象中有3个核心东西, 是watchers,前面也说到过。简单的说它就是保存的是我们的状态属性和我们dom节点的相互依赖关系(映射),我们知道这肯定是1对X的, 是我们的订阅,subscribe,它的作用就是帮我们收集哪些节点用到了那个属性,也就是说那个节点订阅了这个属性。当这个属性发生了更改,就会通知这些订阅者最初相应的修改,subscribe作用就是给我们的watchers添加对应的内容。 是setter方法执行触发我们的emit方法,然后通知watchers对订阅者们修改。 所以我们这里写一个createObserve方法写在Reactive中。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051//创建一个observe对象 createObserve() { let _this = this; return { addWatch(k, cb) { if (!this.watchers[k]) { this.watchers[k] = [cb] } else { this.watchers[k].push(cb) } }, watchers: {}, //就是用来存储data里面属性所对应的节点的一个映射关系保存起来,也就是说一个属性的值 //可能在多个dom元素中运用了,我们就把他这中互相映射的关系保存起来。当有属性发生更 //改也就是代理对象的set方法调用时,就会通知watchers重新计算。从而致使它关联的组件 //得以更新。 subscribe(k) {//订阅,用于收集上面说到的那种关联关系,然后添加到watchers对象中。 _this._el.querySelectorAll(`[data-on=${k}]`).forEach(item => { //这个是我们定义的解析data-on的 const cb = text => item.innerHTML = text; cb(_this._proxy[k]) this.addWatch(k, cb) }) _this._el.querySelectorAll(`[i-model=${k}]`).forEach(item => { //这个是我们定义的解析imodel的 item.addEventListener('input', function () { _this._proxy[k] = item.value }) const cb = text => item.value = text; cb(_this._proxy[k]) this.addWatch(k, cb) }) }, queue: new Set, isUpdate: false, emit(k) {//更新的方法,当这个方法触发,就会更新 this.queue.add(k) this.update() }, update() { if (this.isUpdate) return //如果为真我们就return,为假我们就执行下面的代码 this.isUpdate = true;//这样我们就只会有一个异步操作 Promise.resolve().then(() => { console.log('这样就只有一次更新了'); for (let k of this.queue) { this.watchers[k].forEach(cb => cb(_this._proxy[k])) } this.isUpdate = false; this.queue.clear(); }) }, } } 同时我们还应该有一个生成代理对象的方法。写在Reactive类中 12345678910111213141516restoreProxy(data) { this._proxy = new Proxy(data, { get(target, k) { return target[k] }, set: (target, k, v) => { target[k] = v; this._ob.emit(k) return true; } }) for (let k in this._proxy) { this._ob.subscribe(k) } } 到此我们的简单demo的v-bind–v-model就模拟实现了,但是依然有一些问题,比如我们看下面的代码。异步操作的东西也已经在上面的emit方法中实现了,可以回去阅读一下。 1234567891011//给我们的按钮btn添加点击事件 btn.addEventListener('click', function () { R1.a++//我们发现我们在这里每一次++其实都是修改了data里面的值,就会让页面刷新,比如这里写了 //8次,也就意味着要页面刷新8次,如果是100个这样的操作呢,很显然这样不好,对于一些相 //同的操作我们只希望它执行一次就好了。 R1.a++//所以这里就引出了,我们的异步更新。意思就是我们让这里的一些操作在异步中一次完成。不 //重复刷新页面。 R1.a++ R1.a++ R1.count++ R1.count++ R1.count++ R1.count++ })]]></content>
<categories>
<category>Vue</category>
</categories>
<tags>
<tag>Vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[手摸手带你学JS第一篇]]></title>
<url>%2F2019%2F04%2F06%2F%E6%89%8B%E6%91%B8%E6%89%8B%E5%B8%A6%E4%BD%A0%E5%AD%A6JS%E7%AC%AC%E4%B8%80%E7%AF%87%2F</url>
<content type="text"><![CDATA[这是手摸手系列的第一篇文章,每篇文章的大致内容,数据类型及转换。我会从js基础、js高级、js-web、js-dom、其中会有许多基础知识理论,以及小练习。还会开个js-game板块,会有一些好玩的游戏。当然此系列也涉及到前端框架的知识。以及实战项目。学习完这个系列保证你会收获很多。更多请关注Github 什么是JS?JavaScript一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML(标准通用标记语言下的一个应用)网页上使用,用来给HTML网页增加动态功能。 看到这里也许你大概会觉得这篇文章会是这个套路。是什么–>为什么–>怎么做。是不是感觉回到了学习政治的时候。 所以我不打算这样写。类似的介绍js的文章太多了。我只想写出我认为简洁,看完这一篇文章能让人直接或者间接的收获到有用的知识的文章。所以进入正题,让我们进入javascript的世界。首先你需要了解一下ECMAScript 基础数据类型任何语言都有它自己的数据类型,js也不列外。它有七大数据类型,可以分为简单类型(也叫原始类型),和复杂类型(引用类型)。 简单类型分别是: boolean null undefined number string symbol 复杂类型就一个:对象(Object)类型,接下来我们分别说说这些数据类型的特点。 原始类型Number类型JavaScript不区分整数和浮点数,统一用Number表示(浮点数数值必须包含一个小数点,且小数点后面至少有一位数字)两种值。 NaN:非数字类型。特点:① 涉及到的 任何关于NaN的操作,都会返回NaN ② NaN不等于自身。 isNaN() 函数用于检查其参数是否是非数字值。至于如何转换的后面的类型转换会说到。以下都是合法的Number类型: 1234567123; // 整数1230.456; // 浮点数0.4561.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5-99; // 负数NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示isNaN(123) //false isNaN("hello") //trueInfinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity 计算机由于使用二进制,所以,有时候用十六进制表示整数比较方便,十六进制用0x前缀和0-9,a-f表示,例如:0xff00,0xa5b4c3d2,等等,它们和十进制表示的数值完全一样。 Number可以直接做四则运算,规则和数学一致: 1234561 + 2; // 3(1 + 2) * 5 / 2; // 7.52 / 0; // Infinity0 / 0; // NaN10 % 3; // 110.5 % 3; // 1.5 注意%是求余运算。 String类型字符串是以单引号’或双引号”括起来的任意文本,比如'abc',"xyz"等等。请注意,''或""本身只是一种表示方式,不是字符串的一部分,因此,字符串'abc'只有a,b,c这3个字符。字符串有length属性。 字符串转换:转型函数String(),适用于任何数据类型(null,undefined 转换后为null和undefined);toString()方法(null,undefined没有toString()方法)。 Boolean类型该类型只有两个值,true和false Undefined类型和Null类型只有一个值,即undefined值。使用var声明了变量,但未给变量初始化值,那么这个变量的值就是undefined。 null类型被看做空对象指针,前文说到null类型也是空的对象引用。 null和undefinednull表示一个“空”的值,它和0以及空字符串''不同,0是一个数值,''表示长度为0的字符串,而null表示“空”。 在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。 JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用null。undefined仅仅在判断函数参数是否传递的情况下有用。 Symbol1、ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。 2、Symbol值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 3、注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 4、Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分 5、由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。Symbol值作为对象属性名时,不能用点运算符。在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。 1234567891011121314151617181920//************************symbol数据类型 //Symbol(),每一次调用一次就会出一个symbol的唯一值 let s1 = Symbol('name'); let s2 = Symbol('age'); //为了区分在小括号里可以写个标记区别一下 let s3 = Symbol('name'); console.log(s3); console.log(s1 == s3);//false console.log(s1 == s2);//false console.log(Symbol()); console.log(Symbol.__proto__); let obj = { s3: 'zhangsan', [s2]: 55 } console.log(obj); console.log(obj.s3); 以上就是6种原始类型。 对象类型其中又分为3大引用类型: 对象 数组 函数 由于3个都是重点,所以会在后面的系列来说。 数据类型转换说完了数据类型,就该说我们的数据类型转换了。同样也分为两种类型转换。一是显式类型转换,二是隐式类型转换。 显示类型转换 首先我们怎么才能知道数据的类型改变了呢?这里有一个操作符typeof可以检测数据的类型。 typeof能返回的类型有6种:number string boolean undefined object function 数组和null类型的都是属于object。其实null并不是一种对象,只是因为历史遗留性问题,null通常用来作为对象的占位符,所以被浏览器归到了对象里面了。同时typeof返回的结果其实是一种字符串。 首先我们要知道,在 JS 中类型转换只有三种情况,分别是: 转换为布尔值 转换为数字 转换为字符串 我们先来看一个类型转换表格,然后再进入正题 注意图中有一个错误,Boolean 转字符串这行结果我指的是 true 转字符串的例子,不是说 Boolean、函数、Symblo 转字符串都是 true Number(mix)这个方法是可以把其他类型的数据转换成数字类型的数据。 12345678910111213141516171819202122232425let demo = Number('123'); console.log(typeof demo);//number类型 demo = Number(true);//布尔值转换,true-->1,false-->0 console.log(demo);//1 demo = Number(undefined); console.log(demo);//NaN demo = Number(null); console.log(demo);//0 demo = Number('a123'); console.log(demo);//NaN demo = Number('123.-456') console.log(demo);//NaN //由此可见如果要转换的数有不是数字的字符,那么结果就会是NaN parseInt(string,radix)这个方法是将字符串转换为整型类型的数字。其中第二个参数radix基底是可以选择的参数。 当radix为空的时候,这个函数的作用仅仅是将字符串转换成数字。 当参数string里面既包括数字字符串又包括其他字符串的时候,他会将看到其他字符串就停止,不会继续转换后面的数字型字符串了。 12345678910111213141516171819202122232425demo = parseInt('123asd456');//123 console.log(demo);//123 demo = parseInt('q123456'); console.log(demo);//NaN demo = parseInt('123.111'); console.log(demo);//123,会在小数的处截断 demo = parseInt(true); console.log(demo);//NaN demo = parseInt(undefined);//null '' 的结果都是NaN console.log(demo);//NaN //当radix不为空的时候,这个函数可以用来作为进制转换,第二个参数的作用则是,我们把第一个参数的是数字当成几进制的数字来转换成10进制。参数的范围是2-36 demo = 10; console.log(parseInt(demo, 16));//16,将16进制的10转成10进制的数。 console.log(parseInt(demo, 2));//2 console.log(parseInt(demo, 8));//8 console.log(parseInt(demo, 20));//20 demo = 111; console.log(parseInt(demo, 16)); console.log(parseInt(demo, 2)); console.log(parseInt(demo, 8)); console.log(parseInt(demo, 20)); parseFloat(string,radix)这个方法和parseint方法类似,是将字符串转换成浮点数,同样是碰到第一个非数字型字符停止,他会识别一个小数点及后面的数字,不识别第二个。 123456demo = parseFloat('123456.2.33'); console.log(demo);//123456.2 demo = parseFloat('123.2abc'); console.log(demo);//123.2 demo = parseFloat('123a.abc') console.log(demo);//123 toString(radix)这个方法和前面的有一点不同,他是对象上的方法,转成字符串类型,同样radix基底是可选参数,当为空的时候,仅仅代表将数据转化成字符串。 123456789demo = 123; console.log(typeof demo.toString());//string 123 console.log(typeof true.toString());//string true // console.log(undefined.toString());这两个没有toString方法 // console.log(null.toString()); //当写了radix基地时,则代表我们要将这个数字转化成几进制的数字型字符串。 demo = 10; console.log(demo.toString(16));//a console.log(demo.toString(2));//1010 String(mix)和Number类似,不过它是把任何类型转换成字符串类型。 12345String(['a', 123, true]) //a 123 trueString(undefined) //"undefined"String(null) //"null"String({}) //"[object Object]"String(()=>{}) //"()=>{}" Boolean(mix)和Number类似,它是把任何类型转换成布尔类型。undefined false 0 “” null NaN除了这6个转换为false,其他的全为true. 1234567Boolean(()=>{}) //trueBoolean(2) //trueBoolean(0)//////////falseBoolean('')////////falseBoolean(null)//////falseBoolean(undefined)/falseBoolean({})////////true 隐式类型转换isNaN()这个方法用来检测数据是不是非数。not a number. 1234isNaN(NaN);//trueisNaN('qwe')//trueisNaN(123);//false//其实这中间隐含了一个隐式转换,它会先将你传的参数先调用一下Number方法之后,在看看结果是不是NaN 各种运算符++就是先将数据调用一遍Number之后,再自加1. 12345678demo = 'abc';demo++;console.log(demo);//NaNdemo = '123';++demo;console.log(demo);//124demo = '123';console.log(demo++);//123,应为++在后,调用Number之后来没自加1就输出了。 + - * /这不是+-符号,应该叫一元正负运算符。 12345678910111213141516demo = false;console.log(+demo);//0demo = true;console.log(-demo);//-1'demo = 'abc';console.log(+demo);//NaNdemo = 1 * '2';console.log(demo);//2demo = true * false;console.log(demo);//0demo = false / false;console.log(demo);//NaNdemo = true / false;console.log(demo);//infinity无穷大demo = -true / false;console.log(demo);//-infinity 逻辑运算符&& || ! 12345678910111213141516&&和||都是先把表达式调用Boolean,换成布尔值再进行判断,看是true还是false。!取反操作符返回的结果也是调用Boolean方法之后的结果 !'abc';//false //当然也有不发生类型转换的比较运算符,===严格等于,!==严格不等于 //要想严格等于就必须,值一样,值类型也一样。 console.log(undefined == null);//true console.log(undefined === null);//false let num = new Number(123); console.log(num) //Number{123} console.log(typeof num); //object console.log(Object.prototype.toString.call(num)); //[object Number] //扩展知识。这样子是一个立即执行函数。arguments.callee返回的是这个函数本身。 let a; -function () { a = (arguments.callee); }(); console.log(a); 这是几道题 console.log(‘0||1=’ + (0 || 1));//0||1=1 console.log(‘1||2=’ + (1 || 2));//1||2=1 console*.log(‘0&&1=’ + (0 && 1));//0&&1=0 console.log(‘1&&2=’ + (1 && 2));//1&&2=2]]></content>
<categories>
<category>手摸手</category>
</categories>
<tags>
<tag>手摸手</tag>
<tag>JS基础</tag>
<tag>数据类型+转换</tag>
</tags>
</entry>
<entry>
<title><![CDATA[我是彭松]]></title>
<url>%2F2019%2F04%2F05%2F%E6%88%91%E6%98%AF%E5%BD%AD%E6%9D%BE%2F</url>
<content type="text"><![CDATA[你好,来了就是朋友,我将把这个blog越做越好]]></content>
<categories>
<category>哈哈</category>
</categories>
<tags>
<tag>PS</tag>
<tag>Boy</tag>
<tag>IOT</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hooks]]></title>
<url>%2F2019%2F04%2F05%2Fhooks%2F</url>
<content type="text"><![CDATA[这是 React 进阶系列的第一篇文章,这个系列内容会包括一些 React 的新知识以及原理内容,有兴趣的可以持续关注。 注意:Hooks 在 React 16.8 版本中才正式发布 为什么要用 Hooks组件嵌套问题之前如果我们需要抽离一些重复的逻辑,就会选择 HOC 或者 render props 的方式。但是通过这样的方式去实现组件,你打开 React DevTools 就会发现组件被各种其他组件包裹在里面。这种方式首先提高了 debug 的难度,并且也很难实现共享状态。 但是通过 Hooks 的方式去抽离重复逻辑的话,一是不会增加组件的嵌套,二是可以实现状态的共享。 class 组件的问题如果我们需要一个管理状态的组件,那么就必须使用 class 的方式去创建一个组件。但是一旦 class 组件变得复杂,那么四散的代码就很不容易维护。另外 class 组件通过 Babel 编译出来的代码也相比函数组件多得多。 Hooks 能够让我们通过函数组件的方式去管理状态,并且也能将四散的业务逻辑写成一个个 Hooks 便于复用以及维护。 Hooks 怎么用前面说了一些 Hooks 的好处,接下来我们就进入正题,通过实现一个计数器来学习几个常用的 Hooks。 useStateuseState 的用法很简单,传入一个初始 state,返回一个 state 以及修改 state 的函数。 1234// useState 返回的 state 是个常量// 每次组件重新渲染之后,当前 state 和之前的 state 都不相同// 即使这个 state 是个对象const [count, setCount] = useState(1) setCount 用法是和 setState 一样的,可以传入一个新的状态或者函数。 12setCount(2)setCount(prevCount => prevCount + 1) useState 的用法是不是很简单。假如现在需要我们实现一个计数器,按照之前的方式只能通过 class 的方式去写,但是现在我们可以通过函数组件 + Hooks 的方式去实现这个功能。 12345678910function Counter() { const [count, setCount] = React.useState(0) return ( <div> Count: {count} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div> );} useEffect现在我们的计时器需求又升级了,需要在组件更新以后打印出当前的计数,这时候我们可以通过 useEffect来实现 123456789101112131415function Counter() { const [count, setCount] = React.useState(0) React.useEffect(() => { console.log(count) }) return ( <div> Count: {count} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div> );} 以上代码当我们改变计数的时候,就会打印出正确的计数,我们其实基本可以把 useEffect 看成是 componentDidUpdate,它们的区别我们可以在下一个例子中看到。 另外 useEffect 还可以返回一个函数,功能类似于 componentWillUnmount 12345678910function Counter() { const [count, setCount] = React.useState(0) React.useEffect(() => { console.log(count) return () => console.log('clean', count) }) // ...} 当我们每次更新计数时,都会先打印 clean 这行 log 现在我们的需求再次升级了,需要我们在计数器更新以后延时两秒打印出计数。实现这个再简单不过了,我们改造下 useEffect 内部的代码即可 12345React.useEffect(() => { setTimeout(() => { console.log(count) }, 2000)}) 当我们快速点击按钮后,可以在两秒延时以后看到正确的计数。但是如果我们将这段代码写到 componentDidUpdate 中,事情就变得不一样了。 12345componentDidUpdate() { setTimeout(() => { console.log(this.state.count) }, 2000)} 对于这段代码来说,如果我们快速点击按钮,你会在延时两秒后看到打印出了相同的几个计数。这是因为在 useEffect 中我们通过闭包的方式每次都捕获到了正确的计数。但是在 componentDidUpdate 中,通过 this.state.count 的方式只能拿到最新的状态,因为这是一个对象。 当然如果你只想拿到最新的 state 的话,你可以使用 useRef 来实现。 12345678910111213function Counter() { const [count, setCount] = React.useState(0) const ref = React.useRef(count) React.useEffect(() => { ref.current = count setTimeout(() => { console.log(ref.current) }, 2000) }) //...} useRef 可以用来存储任何会改变的值,解决了在函数组件上不能通过实例去存储数据的问题。另外你还可以 useRef 来访问到改变之前的数据。 123456789101112131415161718function Counter() { const [count, setCount] = React.useState(0) const ref = React.useRef() React.useEffect(() => { // 可以在重新赋值之前判断先前存储的数据和当前数据的区别 ref.current = count }) <div> Count: {count} PreCount: {ref.current} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div> //...} 现在需求再次升级,我们需要通过接口来获取初始计数,我们通过 setTimeout 来模拟这个行为。 12345678910111213141516171819202122232425function Counter() { const [count, setCount] = React.useState(); const [loading, setLoading] = React.useState(true); React.useEffect(() => { setLoading(true); setTimeout(() => { setCount(1); setLoading(false); }, 2000); }); return ( <div> {!loading ? ( <div> Count: {count} <button onClick={() => setCount(pre => pre + 1)}>+</button> <button onClick={() => setCount(pre => pre - 1)}>-</button> </div> ) : ( <div>loading</div> )} </div> );} 如果你去执行这段代码,会发现 useEffect 无限执行。这是因为在 useEffect 内部再次触发了状态更新,因此 useEffect 会再次执行。 解决这个问题我们可以通过 useEffect 的第二个参数解决 1234567React.useEffect(() => { setLoading(true); setTimeout(() => { setCount(1); setLoading(false); }, 2000);}, []); 第二个参数传入一个依赖数组,只有依赖的属性变更了,才会再次触发 useEffect 的执行。在上述例子中,我们传入一个空数组就代表这个 useEffect 只会执行一次。 现在我们的代码有点丑陋了,可以将请求的这部分代码单独抽离成一个函数,你可能会这样写 1234567891011const fetch = () => { setLoading(true); setTimeout(() => { setCount(1); setLoading(false); }, 2000);}React.useEffect(() => { fetch()}, [fetch]); 但是这段代码出现的问题和一开始的是一样的,还是会无限执行。这是因为虽然你传入了依赖,但是每次组件更新的时候 fetch 都会重新创建,因此 useEffect 认为依赖已经更新了,所以再次执行回调。 解决这个问题我们需要使用到一个新的 Hooks useCallback。这个 Hooks 可以生成一个不随着组件更新而再次创建的 callback,接下来我们通过这个 Hooks 再次改造下代码 1234567891011const fetch = React.useCallback(() => { setLoading(true); setTimeout(() => { setCount(1); setLoading(false); }, 2000);}, [])React.useEffect(() => { fetch()}, [fetch]); 大功告成,我们已经通过几个 Hooks + 函数组件完美实现了原本需要 class 组件才能完成的事情。 总结通过几个计数器的需求我们学习了一些常用的 Hooks,接下来总结一下这部分的内容。 useState:传入我们所需的初始状态,返回一个常量状态以及改变状态的函数 useEffect:第一个参数接受一个 callback,每次组件更新都会执行这个 callback,并且 callback 可以返回一个函数,该函数会在每次组件销毁前执行。如果 useEffect 内部有依赖外部的属性,并且希望依赖属性不改变就不重复执行 useEffect 的话,可以传入一个依赖数组作为第二个参数 useRef:如果你需要有一个地方来存储变化的数据 useCallback:如果你需要一个不会随着组件更新而重新创建的 callback]]></content>
<categories>
<category>React</category>
</categories>
<tags>
<tag>React</tag>
<tag>Hooks</tag>
</tags>
</entry>
<entry>
<title><![CDATA[React极速入门2]]></title>
<url>%2F2019%2F04%2F02%2Fhello-world%2F</url>
<content type="text"><![CDATA[跟着我们上一篇内容继续走,开始实现我们的“hello,world”,本篇实现了第一个hello,world,和关于jsx以及React虚拟元素知识。 那么开始吧!最简单的hello, world就是在src文件夹下的index.js文件中这样写,其中引入的ReactDOM上的方法render是将react虚拟dom转换并渲染到页面的关键。 123456import React from 'react';import ReactDOM from 'react-dom';ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('root')); 当然这样是实现了,这标志着你这是进入react一个激动的瞬间。More info: 官方实例 我们还需要了解现在还没有进入到重点,但是我们需要明白一些react的知识。 JSX与虚拟dom1const element = <h1>Hello, world!</h1>; 这个有趣的标签语法既不是字符串也不是 HTML。 它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。 JSX 是一种语法糖, 经过 babel 转换结果如下, 可以发现实际上转化成 React.createElement() 的形式: 扩展: babel 执行机制 因此, 我们得出结论: JSX 语法糖经过 Babel 编译后转换成一种对象, 该对象即所谓的虚拟 DOM, 使用虚拟 DOM 能让页面进行更为高效的渲染。 Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。 12345const element = ( <h1 className="greeting"> Hello, world! </h1>); 上面的和下面的实例代码完全等效 12345const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!'); 所以以下两种方式在页面渲染,最终都是一样的。 12345let h1Ele = React.createElement('h1', null, 'hello world');//通过React创建一个h1的虚拟dom节点,其实就是一个h1节点的描述对象ReactDOM.render(h1Ele, document.querySelector('#root'));//使用ReactDOM的render方法将虚拟节点转换并添加到指定的节点之中ReactDOM.render(<ul>就写下中文</ul>, document.querySelector('#root'));//这个就是jsx帮我们做了剩下的事 但是你也发现了这样一个一个创建dom不是太慢了吗,当我们需要创建多个同样结构的时候怎么办? 12345678let ulEle = React.createElement('ul',//第一个参数 {//第二个参数,如果通过迭代的方式生成第三个参数的内容时,要添加key属性要唯一 key: 'ul0',//时react内部的算法会用,有唯一性和稳定性 style: { background: 'lightblue' }//样式写在这里,用{}表达式包裹 }, ['HTML', 'CSS', 'JS'].map(item => React.createElement('li', { key: item, style: { color: 'red' } }, item))) ReactDOM.render(ulEle, document.querySelector('#root'));console.log(ulEle); 但是有了jsx为什么我们不用呢? 12345678const pp = ( <ul> <li>HTML</li> <li style={{ color: 'red' }}>CSS</li> <li onClick={() => console.log(1)}>JS</li> </ul> )ReactDOM.render(pp, document.querySelector('#root')); 我们还可以再改进一下。 12345678const p1 = ( <ul> { ['HTML', 'CSS', 'JS'].map(e => <li key={e} style={{ color: '#312465' }}>{e}</li>)//li里面的内容也要用{包起来},不然内容都是e } </ul> )ReactDOM.render(p1, document.querySelector('#root')); jsx其实本质也是转换成了使用React.creatElement创建的内容,只是我们的脚手架中的工具帮我们监听着jsx语法的代码出现,就会帮我们转换了,应为脚手架中使用了webpake,webpake里面用到了babel(就是专门转换jsx语法的工具) 小结JSX 经过 babel 编译为 React.createElement() 的形式, 其返回结果就是 Virtual DOM, 最后通过 ReactDOM.render() 将 Virtual DOM 转化为真实的 DOM 展现在界面上。流程图如下: 下篇文章我们再见。]]></content>
<categories>
<category>React</category>
</categories>
<tags>
<tag>React</tag>
<tag>JSX</tag>
<tag>PS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[React极速入门]]></title>
<url>%2F2019%2F04%2F01%2FReact-%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%2F</url>
<content type="text"><![CDATA[本系列文章学习掌握可对深入学习React有很大帮助,核心内容参照React官方文档的核心概念一步一步写的。 环境准备随着科技发展,开发也变得越来越简单,易上手了,我们都知道要开发一个项目(指的是我们开发人员负责的部分)在分析了项目大致需求后,就要进行一步很关键的操作,那就是配置环境,也就是我们的开发环境。 具体可以看: 创建React App facebook提供了一个快速创建React应用的框架,create-react-app 官网上是这样描述的通过运行一个命令来设置现代Web应用程序。它没有骗人,真的只需要一个命令就下载并安装了。 1npx create-react-app my-app 这里单词没错就是npx, my-app就是我们的应用名字。静静的等待它下载完成后。我们这里在下载一个类型npm的工具叫做yarn,下载代码 1npm i yarn -g 这两个都下好之后我们先进入到my-app里面后执行yarn start.开启一个react应用,他会用默认浏览器打开页面。 此时我们的应用目录结构如下: 12345678910111213141516├── README.md ├── node_modules ├── package.json├── .gitignore├── public│ ├── favicon.ico│ ├── index.html│ └── manifest.json└── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js 没有配置或复杂的文件夹结构,只是构建应用程序所需的文件。 进一步操作在public目录下有我们应用的主页面index.html,上面有一些东西是自带有的,但是我们要弄成自己的应用,所以可以删除。 1234567891011121314151617<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="theme-color" content="#000000" /> <title>React App</title></head><body> <!-- 大概就这样,只留一个div作为应用的根节点。 --> <div id="root"></div></body></html> 再进入到我们的src文件夹将我们index.js,App.js,index.css里面的文件都干掉,后面我们一步一步用了再说。]]></content>
<categories>
<category>React</category>
</categories>
<tags>
<tag>React</tag>
<tag>Redux</tag>
<tag>Hooks</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前后端分离问题]]></title>
<url>%2F2019%2F04%2F01%2F%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%2F</url>
<content type="text"><![CDATA[前后端分离问题什么是前后端分离?首先前后端分离只会推动web行业的发展,而且是必然的事情。 简单的说就是让合适的人做合适的事情。一个项目来说分工更加的明确,做到真正的高效率的开发技术。 前后端实现两者技术的无关性,平台的无关性。也就是开发环境的分离和框架的分离,前后端可以自己选择自己合适的框架进行开发。 我们前端的不必管后端的事情,不管你是java后台,还是其他语言的后台,我们不用明白你的业务逻辑,只调用接口拿数据渲染就好。后端人员也不用管页面的渲染这些,只需要给我们提供对应的数据接口供我们调用就好。 前后端分离,实现并行开发,同时前端人员引入了后端的mvc思想,将前端工程又以mvc的思想开发,同时还出现了mvvm 前后端分离过程1.前后端在没有分离状态下,同一个文件下面代码混乱,各种语言混乱,前后端代码混 乱。 2.半分离是借助了ajax技术的出现,我们可以用ajax发请求,得到数据,在用js渲染在页 面上。做测试还必须依赖后端的数据。 3.全分离接住了node.js的出现,node.js会在浏览器和后台之间会加上一层node.js,可 以实现前端自己能做的事情更多了,在整个项目中控制权取得更多,性能优化,会话 管理等等都可以自己做。 分离带来的弊端前端学习门槛增加,SEO的难度加大,因为我们现在的方式不在是在服务端渲染好的了,所以爬虫爬取东西的时候导致获取不到有价值的东西,后端开发模式迁移增加成本 分离后的前后端人员工作分配前端的工作:实现整一个前端页面以及交互逻辑,以及利用ajax与nodejs服务器(中间层)交互 后端的工作:提供API接口,利用redis来管理session,与数据库交互]]></content>
<categories>
<category>Vue</category>
</categories>
<tags>
<tag>Web</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ts极速入门3--基础语法2]]></title>
<url>%2F2019%2F03%2F12%2FTypeScript%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A83%2F</url>
<content type="text"><![CDATA[接着上一篇文章极速入门,接下来我们就继续学习ts的基础语法。本文以以下几点开展,1,类。2,类与接口。3,泛型。这里查看 TS中文文档 类修饰符TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。 public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public的 private 修饰的属性或方法是私有的,不能在声明它的类的外部访问 protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的 例子: 1234567891011class Animal { public name: string; public constructor(name: string) { this.name = name; }}let a = new Animal('Jack');console.log(a.name);a.name = 'Tom';console.log(a.name); 上面的例子中,name 被设置为 public,所以直接访问实例的 name 属性是允许的。如果希望 name不被外部访问,这时候就可以用 private: 1234567891011class Animal { private name: string; public constructor(name: string) { this.name = name; }}let a = new Animal('Jack');console.log(a.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'.a.name = 'Tom'; // error TS2341: Property 'name' is private and only accessible within class 'Animal'.console.log(a.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'. 使用 private 修饰的属性或方法,在子类中也是不允许访问的: 123456class Cat extends Animal { constructor(name: string) { super(name); console.log(this.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'. }} 如果使用 protected 修饰,则允许在子类中访问: 12345678910111213class Animal { protected name: string; public constructor(name: string) { this.name = name; }}class Cat extends Animal { constructor(name: string) { super(name); console.log(this.name); }} 抽象类abstract 用于定义抽象类和其中的抽象方法,抽象类是不允许被实例化的: 123456789101112abstract class Animal { name: string; constructor(name: string) { this.name = name; } abstract sayHello(): void; sayName() { console.log(this.name); }}new Animal("Jack"); // error TS2511: Cannot create an instance of an abstract class. 其次,抽象类中的抽象方法,必须被子类实现: 1234567891011121314151617181920abstract class Animal { name: string; constructor(name: string) { this.name = name; } abstract sayHello(): void; sayName() { console.log(this.name); }}class Cat extends Animal { sayHello(): void { console.log("hello"); }}const cat: Cat = new Cat("Tom");cat.sayName(); // okcat.sayHello(); // ok 类与接口实现接口实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。 举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它: 123456789101112131415161718interface Alarm { alert(): void;}class Door {}class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); }}class Car implements Alarm { alert() { console.log('Car alert'); }} 一个类可以实现多个接口: 123456789101112131415161718192021interface Alarm { alert(): void;}interface Light { lightOn(): void; lightOff(): void;}class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); }} 上例中,Car 实现了 Alarm 和 Light 接口,既能报警,也能开关车灯。 接口继承接口接口与接口之间可以是继承关系: 12345678interface Alarm { alert(): void;}interface LightableAlarm extends Alarm { lightOn(): void; lightOff(): void;} 接口继承类接口也可以继承类: 12345678910class Point { x: number; y: number;}interface Point3d extends Point { z: number;}let point3d: Point3d = { x: 1, y: 2, z: 3 }; 混合类型我们知道,接口可以用来定义一个函数: 12345678interface SearchFunc { (source: string, subString: string): boolean;}let mySearch: SearchFunc;mySearch = function(source: string, subString: string) { return source.search(subString) !== -1;} 有时候,一个函数还可以有自己的属性和方法: 1234567891011121314151617interface Counter { (start: number): string; interval: number; reset(): void;}function getCounter(): Counter { const counter: Counter = start => start.toString(); counter.interval = 123; counter.reset = () => { } return counter;}let c: Counter = getCounter();c(10);c.reset();c.interval = 20; 泛型泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。 简单的例子首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值: 1234567891011type CreateArray = (length: number, value: any) => Array<any>;let createArray: CreateArray = (length, value) => { let result = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}createArray(3, 'x'); // ['x', 'x', 'x'] 上例中,我们使用了数组泛型来定义返回值的类型。这段代码不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:Array<any>允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该为 value 的类型,这时候,泛型就派上用场了: 123456789101112131415161718192021222324252627282930type CreateArray = <T>(length: number, value: T) => Array<T>;// 箭头函数const createArray: CreateArray = <T>(length: number, value: T): Array<T> => { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}// 函数表达式const createArray: CreateArray = function <T>(length: number, value: T):T[] { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}// 声明式函数function createArray<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}createArray<number>(3, 1); // ['x', 'x', 'x'] 在上例中,我们在函数中添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array[T] 中即可使用了。在调用的时候,指定他具体类型为 string, 当然,也可以不手动指定,而让类型推论自动推算出来: 1createArray(3, 1); // ['x', 'x', 'x'] 多个类型参数定义泛型的时候,可以次定义多个类型参数: 123type Swap = <T, U>(tuple: [T, U]) => [U, T];const swap: Swap = <T, U>([p1, p2]: [T, U]): [U, T] => [p2, p1];const result = swap([1, "2"]); 在上例中,我们定义了一个 swap 函数,用来交换输入的 tuple。 泛型约束在函数内部使用泛型变量的时候, 由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法: 123456function loggingIdentity<T>(arg: T): T { console.log(arg.length); return arg;}// error TS2339: Property 'length' does not exist on type 'T'. 上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。这时,我们可以对泛型进行约束,只允许这个函数传入包含 length 属性的变量。这就是泛型约束: 12345678interface ILengthwise { length: number;}function loggingIdentity<T extends ILengthwise>(arg: T): T { console.log(arg.length); return arg;} 我们使用 extends 约束了泛型 T 必须符合接口 ILengthwise 的定义,也就是必须包含 length 属性。那么这时,如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了: 12loggingIdentity(7); // error TS2345: Argument of type '7' is not assignable to parameter of type 'ILengthwise'.loggingIdentity('7'); // OK 多个类型参数之间也可以相互约束: 12345678function copyFields<T extends U, U>(target: T, source: U): T { for (let key in source) { target[key] = (<T>source)[key]; } return target;}let x = { a: 1, b: 2, c: 3, d: 4 };copyFields(x, { b: 10, d: 20 }); 上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。 泛型接口我们可以使用接口的方式来定义一个函数: 1234567interface SearchFunc { (source: string, subString: string): boolean;}let mySearch: SearchFunc = function (source: string, subString: string) { return source.search(subString) !== -1;} 当然也可以使用含有泛型的接口来定义函数: 1234567891011interface CreateArrayFunc { <T>(length: number, value: T): Array<T>;}let createArray: CreateArrayFunc = function <T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;} 进一步,我们还可以把泛型参数提到接口名上: 12345678910111213interface CreateArrayFunc<T> { (length: number, value: T): Array<T>;}let createArray: CreateArrayFunc<any> = function <T>(length: number, value: T) { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}createArray(3, "x"); 注意,此时在使用泛型接口的时候,需要定义泛型的类型。 泛型类与泛型接口类似,泛型也可以用于类的类型定义中: 12345678class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber<number>();myGenericNumber.zeroValue = 0;myGenericNumber.add = (x, y) => x + y; 泛型参数的默认类型在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。 1234567function createArray<T = string>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;} ##]]></content>
<categories>
<category>Typescript</category>
</categories>
<tags>
<tag>TS</tag>
<tag>tsx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ts极速入门2--基础语法]]></title>
<url>%2F2019%2F03%2F12%2FTypeScript%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A82%2F</url>
<content type="text"><![CDATA[接着上一篇文章我们已经把ts所需环境配置好了,接下来我们就学习ts的基础语法。本文以以下几点开展,1,基本数据类型。2,类型推论。3,联合类型。4,类型断言。5,类型别名。6,函数。7,接口。这里查看 TS中文文档 基本数据类型TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。 boolean1let isDone: boolean = false; number和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。 1234let decLiteral: number = 6;let hexLiteral: number = 0xf00d;let binaryLiteral: number = 0b1010;let octalLiteral: number = 0o744; string 和 JavaScript 一样,可以使用双引号( ")或单引号(')表示字符串。 12let name: string = "bob";name = "smith"; 同样也可以使用 字符串模板: 12345let name: string = `Gene`;let age: number = 37;let sentence: string = `Hello, my name is ${ name }.I'll be ${ age + 1 } years old next month.`; array有两种方式定义数组,第一种,在数组元素类型后面使用 []: 1let list: number[] = [1, 2, 3]; 第二种,使用数组泛型,Array<元素类型>: 1let list: Array<number> = [1, 2, 3]; TupleTuple 类型也是一个数组,我们可以用它来表示一个已知元素数量和元素类型的数组。 比如,你可以定义一对值分别为 string和number类型的元组。 123456// Declare a tuple typelet x: [string, number];// Initialize itx = ['hello', 10]; // OK// Initialize it incorrectlyx = [10, 'hello']; // Error 当访问一个已知索引的元素,会得到正确的类型: 12console.log(x[0].substr(1)); // OKconsole.log(x[1].substr(1)); // Error, 'number' does not have 'substr' 使用索引进行越界访问: 1x[3] = 'world'; // Error, Tuple type '[string, number]' of length '2' has no element at index '2'. 调用数组的方法: 12x.push("world"); // OKx.push(true); // Error, Argument of type 'true' is not assignable to parameter of type 'string | number'. 1、使用索引来访问越界元素,编译器会报错误 2、使用 push 方法新增元素,元素的类型必须满足其联合类型 enumenum 类型是对 javascript 标准数据类型的一个补充。 1enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat }; 默认情况下,枚举成员从 0 开始赋值,每次递增步长为 1,同时,可以从值到名进行反向映射: 1234567891011// key -> valueconsole.log(Days["Sun"] === 0); // trueconsole.log(Days["Mon"] === 1); // trueconsole.log(Days["Tue"] === 2); // trueconsole.log(Days["Sat"] === 6); // true// value -> keyconsole.log(Days[0] === "Sun"); // trueconsole.log(Days[1] === "Mon"); // trueconsole.log(Days[2] === "Tue"); // trueconsole.log(Days[6] === "Sat"); // true 同时,我们也可以对枚举项进行手动赋值,当值为 number 类型时,未赋值的枚举项会接着上一个枚举项依次赋值。 1234567enum Days { Sun = 2, Mon, Tue = 5, Wed, Thu, Fri, Sat };console.log(Days.Sun); // 2console.log(Days.Mon); // 3console.log(Days.Tue); // 5console.log(Days.Wed); // 6console.log(Days.Thu); // 7 如果枚举项的值有重复的话,typescript 不会提示错误,但是通过 value 获取 key 的话,key 是最后一次的枚举项: 12enum Days { Sun = 2, Mon = 2, Tue = 1, Wed, Thu, Fri, Sat };console.log(Days[2]); // Wed 在使用的时候,最好不要出现覆盖的情况。 手动赋值的枚举项可以不是 number 类型,但是,紧跟着的枚举项必须给初始值,否则会报错。 1enum Days { Sun = "s", Mon = 2, Tue = 1, Wed, Thu, Fri, Sat }; anyany 表示可以赋值为任意类型。 12let myFavoriteNumber: any = 'seven';myFavoriteNumber = 7; 针对未声明类型的变量,它会被识别为 any: 123let something;something = 'seven';something = 7; void某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void: 1function bar(): void {} 类型推论如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。 什么是类型推论以下代码虽然没有指定类型,但是会在编译的时候报错: 12let myFavoriteNumber = 'seven';myFavoriteNumber = 7; // error TS2322: Type '7' is not assignable to type 'string'. 事实上,它等价于: 12let myFavoriteNumber: string = 'seven';myFavoriteNumber = 7; TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查: 123let myFavoriteNumber;myFavoriteNumber = 'seven';myFavoriteNumber = 7; 联合类型联合类型(Union Types)表示取值可以为多种类型中的一种。 123let myFavoriteNumber: string | number;myFavoriteNumber = 'seven';myFavoriteNumber = 7; 联合类型使用 | 分隔每个类型。 访问联合类型的属性和方法当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法: 1234function getLength(something: string | number): number { return something.length;}// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'. 上例中,length 不是 string 和 number 的共有属性,所以编译器报错。 访问 string 和 number 的共有属性是没问题的: 123function getString(something: string | number): string { return something.toString();} 联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型: 12345let myFavoriteNumber: string | number;myFavoriteNumber = 'seven';console.log(myFavoriteNumber.length);myFavoriteNumber = 7;console.log(myFavoriteNumber.length); // error TS2339: Property 'length' does not exist on type 'number'. 在上例中,第 2 行 myFavoriteNumber 被推断成 string 类型,因此访问其 length 属性不会报错。而第 4 行被推断成 number,访问 length 就报错了。 类型断言类型断言(Type Assertion)可以用来手动指定一个值的类型。 语法12345<type> value // orvalue as type 在 tsx 中必须使用后面一种。 前面在联合类型中我们提到过,当 Typescript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法: 12345function getLength(something: string | number): number { return something.length;}// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'. 而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,比如: 123456789function getLength(something: string | number): number { if (something.length) { return something.length; } else { return something.toString().length; }}// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'. 在上例中,访问 something.length 的时候会报错,因为 length 并不是公共属性。此时,我们就可以使用类型断言,将 something 断言成 string: 1234567function getLength(something: string | number): number { if ((<string>something).length) { return (something as string).length; } else { return something.toString().length; }} 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的: 12345function toBoolean(something: string | number): boolean { return <boolean>something;}// error TS2352: Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type 'number' is not comparable to type 'boolean'. 类型别名类型别名用来给一个类型起个新名字,常用语联合类型。 12345678910type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }} 字符串字面量类型字符串字面量类型用来约束取值只能是某几个字符串中的一个。 1234567type EventNames = 'click' | 'scroll' | 'mousemove';function handleEvent(ele: Element | null , event: EventNames) { // do something}handleEvent(document.querySelector('hello'), 'scroll');handleEvent(document.querySelector('world'), 'dbclick'); // error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'. 上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。 类型别名与字符串字面量类型都是使用 type 进行定义。 函数声明式函数 123function sum(x: number, y: number): number { return x + y;} 输入多余的(或者少于要求的)参数,都是不被允许的。 12sum(1, 2, 3); // error TS2554: Expected 2 arguments, but got 3.sum(1); //Expected 2 arguments, but got 1. 函数表达式如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样: 1const sum = (x: number, y: number): number => x + y; 这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行类型定义,而等号左边的 sum,是通过赋值操作进行 类型推论 推断出来的。如果我们需要手动给 sum 添加类型,则应该是这样: 1const sum: (x: number, y: number) => number = (x: number, y: number): number => x + y; 不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。 使用接口定义函数类型我们可以通过接口来定义函数的类型: 12345interface ISum { (x: number, y: number): number}const sum: ISum = (x, y) => x + y; 可选参数前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢? 与接口中的可选属性类似,我们用 ? 表示可选的参数: 123456789function buildName(firstName: string, lastName?: string) { if (lastName) { return firstName + ' ' + lastName; } else { return firstName; }}let tomcat: string = buildName('Tom', 'Cat');let tom: string = buildName('Tom'); 需要注意的是,可选参数必须接在确定参数后面。换句话说,可选参数后面不允许再出现确定参数。 12345678function buildName(firstName?: string, lastName: string) { if (firstName) { return firstName + ' ' + lastName; } else { return lastName; }}// error TS1016: A required parameter cannot follow an optional parameter. 参数默认值在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数: 123function buildName(firstName: string, lastName: string = 'Cat') { return firstName + ' ' + lastName;} 此时就不受「可选参数必须接在必需参数后面」的限制了: 123function buildName(firstName: string = 'Tom', lastName: string) { return firstName + ' ' + lastName;} 剩余参数ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数): 12345function push(array, ...items) { items.forEach(function (item) { array.push(item); });} 事实上,items 是一个数组,所以我们可以用数组的类型来定义: 12345function push<A, B>(array: A[], ...items: B[]): void { items.forEach(item => { console.log(item); })} 重载重载允许一个函数接收不同数量或类型的参数时,作出不同的处理。 比如,我们需要实现一个函数 reverse,输入数字 123 时,返回反转的数字 321,输入字符串 hello 时,返回反转的字符串 olleh,利用联合类型,我们可以这样实现: 123456789type Reverse = string | number;function reverse(x: Reverse): Reverse { if (typeof x === "number") { return Number(x.toString().split('').reverse().join('')); } else { return x.split('').reverse().join(''); }} 然而这样做有一个缺点,就是不能 精确 的表达,输入数字的时候,返回也是数字,输入字符串的时候,也应该返回字符串。这时,我们可以使用重载定义多个 reverse 函数类型: 123456789function reverse(x: number): number;function reverse(x: string): string;function reverse(x: number | string) { if (typeof x === "number") { return Number(x.toString().split('').reverse().join('')); } else { return x.split('').reverse().join(''); }} 以上代码,我们重复多次定义了 reverse 函数,前几次都是函数的定义,最后一次是函数的实现,这时,在编译阶段的提示中,就可以正确的看到前两个提示了。 TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。 接口在 typescript 中,我们可以使用 interface 来定义复杂数据类型,用来描述形状或抽象行为。如: 12345678910111213interface IPerson { name: string; age: number; sayName(): void;}const p: IPerson = { name: "tom", age: 21, sayName() { console.log(this.name); }}; 接口名称首字母大写,同时加上 I 前缀。 变量 p 的类型是 IPerson,这样就约束了它的数据结构必须和 IPerson 保持一致,多定义和少定义都是不被允许的。 赋值的时候,变量的形状必须和接口的形状保持一致。 可选属性有时,我们希望不要完全匹配接口中的属性,那么可以用可选属性: 1234567891011121314interface IPerson { name: string; age: number; gender?: string; // 可选属性 sayName(): void;}const p: IPerson = { name: "tom", age: 21, sayName() { console.log(this.name); }}; 在进行赋值时, gender 属性是可以不存在的。当然,这时仍然不允许添加接口中未定义的属性。 只读属性有时候我们希望对象中的一些属性只能在创建的时候被赋值,那么可以用 readonly 定义只读属性: 1234567interface IPerson { readonly id: number; // 只读属性 name: string; age: number; gender?: string; sayName(): void;} 只读约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。 因此,在对象初始化的时候,必须赋值,之后,这个属性就不能再赋值。 12345678const p: IPerson = { id: 1, name: "tom", age: 21, sayName() { console.log(this.name); }}; const vs readonly:变量用 const,对象属性用 readonly 任意属性有时候,我们希望一个接口允许有任意属性: 12345678interface IPerson { readonly id: number; name: string; age: number; gender?: string; sayName(): void; [propsName: string]: any; // 任意属性} [propsName: string]: any;通过 字符串索引签名 的方式,我们就可以给 IPerson 类型的变量上赋值任意数量的其他类型。 12345678910const p: IPerson = { id: 1, name: "tom", age: 21, email: "102376640@qq.com", // 任意属性 phone: 1234567890, // 任意属性 sayName() { console.log(this.name); },}; email 和 phone 属性没有在 IPerson 中显性定义,但是编译器不会报错,这是因为我们定义了字符串索引签名。 一旦定义字符串索引签名,那么接口中的确定属性和可选属性的类型必须是索引签名类型的子集。 12345678interface IPerson { name: string; age?: number; [propName: string]: string;}// Property 'age' of type 'number | undefined' is not assignable to string index type 'string'.ts(2411)// (property) IPerson.age?: number | undefined [propName: string]: string;字符串索引签名类型为 string,但是可选属性 age 是 number 类型,number 并不是 string 的子集, 因此编译报错。 表示数组接口除了可以用来描述对象以外,还可以用来描述数组类型,也就是数字索引签名: 1234interface NumberArray { [index: number]: number;}let fibonacci: NumberArray = [1, 1, 2, 3, 5]; 变量 fibonacci 的类型是 NumberArray,如果还想调用数组的方法,则: 1234interface NumberArray<T> extends Array<T> { [index: number]: T;}let fibonacci: NumberArray<number> = [1, 1, 2, 3, 5]; 表示函数接口还可以用来描述函数,约束参数的个数,类型以及返回值: 12345678interface ISearchFunc { (source: string, subString: string): boolean}let mySearch: ISearchFunc = (source, subString) => { let result = source.search(subString); return result > -1;} ##]]></content>
<categories>
<category>Typescript</category>
</categories>
<tags>
<tag>TS</tag>
<tag>tsx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ts极速入门1--环境搭建]]></title>
<url>%2F2019%2F03%2F11%2FTypeScript%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A81%2F</url>
<content type="text"><![CDATA[此篇文章为,ts的环境配置,跟着此系列文章走,你可以快速学习到typescript的一些知识。关于typescript是什么,可以做什么,在上一篇里可以找到。 环境配置安装 ts123456// 安装npm install -g typescript// 查看版本tsc -v // 更新npm update -g typescript 安装 Typingstypings 主要是用来获取 .d.ts 文件。当 typescript 使用一个外部 JavaScript库时,会需要这个文件。 1npm install -g typings 安装 node 的 .d.ts 库1234567891011121314151617181920typings install dt~node --global# 安装Typings的命令行代码. npm install typings --global# 搜索对应模块的typings定义. typings search tape# 根据名称寻找一个可获得的typings定义. typings search --name react# 如果你用一个独立包的模块: # 或者并不是安装全局模块# 比如并不是在命令行通过输入npm install -g typings这种方式安装的. typings install debug --save# 如果是通过script标记# 或者是子环境的一部分# 或者全局typings命令不可用的时候: typings install dt~mocha --global --save# 从其他版本处安装typings定义(比如env或者npm). typings install env~atom --global --savetypings install npm~bluebird --save# 使用该文件`typings/index.d.ts` (在`tsconfig.json`文件使用或者用 `///` 定义). cat typings/index.d.ts 项目初始化我们建一个文件夹Ts,在小黑屏打开,或者编辑器终端打开进入到Ts目录下,然后执行以下命令。 1234567// 项目初始化npm init -f// tsconfig 初始化tsc -init// 安装 dt~nodetypings install dt~node --global// 使用 shift + ctrl + B 监视文件 or shift + command + B 在新建两个文件夹dist存放的是转换好的js文件,src是放ts文件的。上面的最后一个命令,就是一直监视着src目录下的文件,并实时转换在dist下自动创建js文件。此时我们的文件结构如下图: 我们还需要修改tsconfig.json文件,具体修改如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960{ "compilerOptions": { /* Basic Options */ "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }} 覆盖就好,自此我们的开发环境就搭建完成了。具体基础语法学习我们下章再说。]]></content>
<categories>
<category>Typescript</category>
</categories>
<tags>
<tag>TS</tag>
<tag>tsx</tag>
</tags>
</entry>
</search>