在此前我使用的前端框架是 Angular,使用过 TypeScript 后你就会讨厌 JS 了,我学习 Vue 时的最新版本是 2.5,相信大部分同学都不会认为 Vue 那样又细又长的代码很美观吧,简单看了一些网络博客后,我毅然决然引入了 TypeScript 进行开发,本文仅整理记录我自己遇到的一些坑。
使用 Cli
脚手架是一个比较方便的工具,这里需要注意的是@vue/cli和vue-cli是不一样的,推荐使用npm i -g @vue/cli安装。
安装完成后,可以直接使用vue create your-app创建项目,你可以选择使用默认配置亦或是自己手动选择配置,按提示一步一步向下走即可,它会根据你的选择自己创建比如tsconfig.json等等配置文件。这里推荐使用less开发样式,sass老是在安装的过程中出问题。
当然你也可以使vue ui命令启动一个本地服务,它是一个 Vue 项目管理器,提供了一个可视化的页面供你管理自己的项目,它的样子如下图所示,还是比较清新的。
使用 vue-property-decorator
Vue 官方维护了 vue-class-component 装饰器,vue-property-decorator 则是在vue-class-component基础上增强了更多结合Vue特性的装饰器,它可以让 Vue 组件语法在结合了 TypeScript 语法后变得更加扁平化。
截止本文时间,vue-property-decorator共提供了 11 个装饰器和 1 个Mixins方法,下面用@Prop举个例子,是不是看起来引起极度舒适。
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
// 上面的内容将会被解析成如下格式
export default {
props: {
propA: {
type: Number
},
propB: {
default: 'default value'
},
propC: {
type: [String, Boolean]
}
}
}
使用 Vuex
关于怎么使用Vuex此处就不再做过多说明了,需要注意的一点是,如果你需要访问$store属性的话,那么你必须得继承Vue类,坑的地方是在某些情况下即使你没有继承Vue,它也能通过编译,只有在程序运行起来的时候才报错。
class ExampleApi extends Vue {
public async getExampleData() {
if (!this.$store.state.exampleData) {
const res = await http.get('url/exampleData');
if (res.result) {
this.$store.commit('setExampleData', res.data);
return res.data;
} else {
promptUtil.showMessage('get exampleData failed', 'warning');
}
} else {
return this.$store.state.exampleData;
}
}
}
使用自己的配置(含代理)
vue.config.js是一个可选的配置文件,如果项目的根目录中存在这个文件,那么它会被@vue/cli-service自动加载,它的配置项说明可以查看配置参考。
我们再开发过程中都会使用代理来转发请求,代理的配置也是在这个文件中,它的官方说明在devserver-proxy中,下面是一个简单的vue.config.js文件例子。
module.exports = {
filenameHashing: true,
outputDir: 'dist',
assetsDir: 'asserts',
indexPath: 'index.html',
productionSourceMap: false,
transpileDependencies: [
'vue-echarts',
'resize-detector'
],
devServer: {
hotOnly: true,
https: false,
proxy: {
"/statistics": {
target: "http://10.7.213.186:3889",
secure: false,
pathRewrite: {
"^/statistics": "",
},
changeOrigin: true
},
"/mail": {
target: "http://10.7.213.186:8888",
secure: false,
changeOrigin: true
}
}
}
}
让 Vue 识别全局方法和变量
我们在项目中都会使用一些第三方 UI 组件,比如我自己就使用了 Element,但是在使用它的$message、$notify等方法时就直接报错了,究其原因就是$message等属性并没有在 Vue 实例中声明。
官方对此给出了很明确的解决方案,使用的是 TypeScript 的 模块补充特性,可以查看增强类型以配合插件使用。既然知道是因为没有声明导致的错误,那我们就给它声明一下好了,在src/shims-vue.d.ts文件中添加如下代码即可,如果没有该文件请自行创建。
看到网上也有一部分人说的是src/vue-shim.d.ts,反正不管是怎么命名这个文件的,它们的作用是一样的。
declare module 'vue/types/vue' {
interface Vue {
$message: any,
$confirm: any,
$prompt: any,
$notify: any
}
}
这里顺道提一下,src/shims-vue.d.ts文件中的如下代码是为了让你的 IDE 明白以.vue结尾的文件是什么玩意儿。
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
路由懒加载
Vue Router 官方有关于路由懒加载的说明,但不知道为什么官方给的这个说明在我的项目里面都没有生效,但使用require.ensure()按需加载组件可以生效。
// base-view 是模块名,写了相同的模块名则代码会被组织到同一个文件中
const Home = (r: any) => require.ensure([], () => r(require('@/views/home.vue')), layzImportError, 'base-view');
// 路由加载错误时的提示函数
function layzImportError() {
alert('路由懒加载错误');
}
上面的方式会在编译的时候把文件自动分成多个小文件,编译后的文件会以你自己命名的模块名来命名,如果代码之间有相互依赖,依赖部分代码编译后的文件会以两个模块名相连后进行命名。
但是需要注意的是,这样拆分小文件之后引入了另外一个新的问题,因为客户端会缓存这些编译后的 js 文件,如果功能 A 同时依赖了a.js和b.js两个文件,但用户在使用其它功能时已经把a.js缓存到本地了,使用功能 A 时需要请求b.js文件,这时程序就很容易报错,因为此时在客户端这两个文件不是同一个版本,所以可能导致a.js调用b.js中的方法已经被删了,进而导致客户端页面异常。
关于引入第三方包
项目在引入第三方包的时候经常会报出各种奇奇怪怪的错误,这里仅提供我目前找到的一些解决办法。
/*
引入 jquery 等库可以尝试下面这种方式
只需要把相应的 js 文件放到指定文件夹即可
**/
const $ = require('@/common/js/jquery.min.js');
const md5 = require('@/common/js/md5.js');
引入一些第三方样式文件、UI 组件等,如果引入不成功可以尝试建一个 js 文件,将导入语句都写在 js 文件中,然后再在main.ts文件中导入这个 js 文件,这个方法能解决大部分的问题。例如我先建了一个lib.js,然后在main.ts中引入lib.js就没有报错。
// src/plugins/lib.js
import Vue from 'vue';
// 树形组件
import 'vue-tree-halower/dist/halower-tree.min.css';
import {VTree} from 'vue-tree-halower';
// 饿了么组件
import Element from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// font-awesome 图标
import '../../node_modules/font-awesome/css/font-awesome.css';
import VueCookies from 'vue-cookies';
import VueJWT from 'vuejs-jwt';
Vue.use(VueJWT);
Vue.use(VueCookies);
Vue.use(VTree);
Vue.use(Element);
// src/main.ts
import App from '@/app.vue';
import Vue from 'vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
import './plugins/lib';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
因为第三方包写的各有特点,在引入不成功的时候基本也只能是见招拆招,当然如果你的功底比较深厚,你也可以自己写一个index.d.ts文件,实在不行的话,那个特殊的组件不使用 TypeScript 来写也能解决,我目前还没有找一个可以完全解决第三方包引入错误的方法,如果您已经有相关的方法了,希望能与你一起探讨交流。
Read More ~
标签:#
JavaScript
JavaScript 进阶知识、技巧
对象
Js 共有number、string、boolean、null、undefined、object六种主要类型,除了object的其它五中类型都属于基本类型,它们本身并不是对象。但是null有时会被当做对象处理,其原因在于不同的对象在底层都表示为二进制,在 js 中二进制前三位都为 0 的话就会被判定为object类型,而null的二进制表示全是 0, 所以使用typeof操作符会返回object,而后续的 Js 版本为了兼容前面埋下的坑,也就没有修复这个 bug。
"I'm a string"本身是一个字面量,并且是一个不可变的值,如果要在这个字面量上执行一些操作,比如获取长度、访问某个字符等,那就需要将其转换为String类型,在必要的时候 js 会自动帮我们完成这种转换,也就是说我们并不需要用new String('I'm a string')来显示的创建一个对象。类似的像使用42.359.toFixed(2)时,引擎也会自动把数字转换为Number对象。
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。对于Object、Array、Function和RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
Array 类型
数组类型有一套更加结构化的值存储机制,但是要记住的是,数组也是对象,所以有趣的是你也可以给数组添加属性。
var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
数组类型的length属性是比较有特点的,它的特点在于不是只读的,也就是说你可以修改它的值。因此可以通过设置这个属性从数组末尾删除或添加新的项。
var colors = ["red", "blue", "green"];
colors.length = 2;
console.info(colors[2]); // undefined
colors.length = 4;
console.info(colors[4]); // undefined
// 向后面追加元素
colors[colors.length] = "black";
数组还有一些很方便的迭代方法,比如every()、filter()、forEach()、map()、some(),这些方法都不会修改数组中包含的值,传入这些方法的函数会接收三个参数:数组项的值、该项在数组中的位置、和数组对象本身。
Function 类型
在 ECMAScript 中,每个函数都是Function类的实例,而且都与其它引用类型一样具有属性和方法。由于函数时对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
在函数的内部有两个特殊的对象,this和arguments。arguments对象有callee和caller属性。caller用来指向调用它的function对象,若直接在全局环境下调用,则会返回null;callee用来指向当前执行函数,所以我们可以通过下面的方式来实现阶乘函数。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num-1);
}
}
每个函数都包含两个非继承而来的方法,apply()和call(),这两个方法都是在特定作用域中调用函数,实际上等于设置函数体内this对象的值。首先,apply()方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组,其中第二个参数可以是Array的实例,也可以是arguments对象。call()方法与apply()方法的作用相同,它们的区别仅仅在于接收参数的方式不同,在使用call()方法时必须逐个列举出来。
window.color = "red";
var o = {color: "blue"};
function sayColor() {
console.info(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
sayColor.apply(o); // blue
需要注意的是,在严格模式下未指定环境对象而调用函数,则this值不会转型为window,除非明确把函数添加到某个对象或者调用apply()或call()。
安全的类型检查
Js 内置的类型检查机制并不是完全可靠的,比如在 Safari(第5版前),对正则表达式应用typeof操作符会返回function;像instanceof在存在多个全局作用域(包含 frame)的情况下,也会返回不可靠的结果;前文提到的 Js 一开始埋下的坑也会导致类型检查出错。
我们可以使用toString()方法来达到安全类型检查的目的,在任何值上调用Object原生的toString()方法都会返回一个[object NativeConstructorName]格式的字符串,下面以检查数组为例。
Object.prototype.toString.call([]); // "[object Array]"
function isArray(val) {
return Object.prototype.toString.call(val) == "[object Array]";
}
作用域安全的构造函数
构造函数其实就是一个使用new操作符调用的函数,当使用new操作符调用时,构造函数内用到的this对象会指向新创建的对象实例,比如我们有下面的构造函数。
function Person(name, age) {
this.name = name;
this.age = age;
}
现在的问题在于,要是我们不使用new操作符呢?会发生什么!
let person = Person('name', 23);
console.info(window.name); // name
console.info(window.age); // 23
很明显,这里污染了全局作用域,原因就在于没有使用new操作符调用构造函数,此时它就会被当作一个普通的函数被调用,this就被解析成了window对象。我们需要将构造函数修改为先确认this是否是正确类型的实例,如果不是则创建新的实例并返回。
function Person(name, age) {
if (this instanceof Person) {
this.name = name;
this.age = age;
} else {
return new Person(name, age);
}
}
高级定时器
大部分人都知道使用setTimeout()和setInterval()可以方便的创建定时任务,看起来好像 Js 也是多线程的一样,实际上定时器仅仅是计划代码在未来的某个时间执行,但是执行时机是不能保证的。因为在页面的生命周期中,不同时间可能有其它代码控制着 JavaScript 进程。
这里需要注意一下setInterval()函数,仅当没有该定时器的任何其他代码实例时,Js 引起才会将定时器代码添加到队列中。这样可以避免定时器代码可能在代码再次被添加到队列之前还没有完成执行,进而导致定时器代码连续运行好几次的问题。但是这也导致了另外的问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会比预期小。
假设某个click事件处理程序使用setInterval()设置了一个 200ms 间隔的重复定时器。如果这个事件处理程序花了 300ms 多的时间完成,同时定时器代码也花了差不多了的时间,就会同时出现跳过间隔切连续运行定时器代码的情况。
为了避免setInterval()的重复定时器的这两个缺点,我们可以使用如下模式的链式setTimeout(),代码一看就懂什么意思了。
setTimeout(function() {
// 处理中
setTimeout(arguements.callee, interval);
}, interval)
消息队列与事件循环
如下图所示,左边的栈存储的是同步任务,就是那些能立即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不需要回调函数的操作都可归为这一类。
右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每个异步任务都和回调函数相关联。
JS引擎线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
来看个例子:执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(> 5s)后,再点击两下,整个过程的输出结果是什么?
setTimeout(function(){
for(var i = 0; i < 100000000; i++){}
console.log('timer a');
}, 0)
for(var j = 0; j < 5; j++){
console.log(j);
}
setTimeout(function(){
console.log('timer b');
}, 0)
function waitFiveSeconds(){
var now = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}
document.addEventListener('click', function(){
console.log('click');
})
console.log('click begin');
waitFiveSeconds();
首先,先执行同步任务。其中waitFiveSeconds是耗时操作,持续执行长达 5s。然后,在 Js 引擎线程执行的时候,'timer a'对应的定时器产生的回调、'timer b'对应的定时器产生的回调和两次 click 对应的回调被先后放入消息队列。由于 Js 引擎线程空闲后,会先查看是否有事件可执行,接着再处理其他异步任务,最后,5s 后的两次 click 事件被放入消息队列,由于此时 Js 引擎线程空闲,便被立即执行了。因此会产生下面的输出顺序。
0
1
2
3
4
click begin
finished waiting
click
click
timer a
timer b
click
click
Read More ~
深入理解 JavaScript——变量提升与作用域
参考内容:
lhs rhs是啥意思
《Javasript 高级程序设计(第三版)》
《你不知道的 JavaScript(上卷)》
几乎所有的编程语言都能够存储变量当中的值,并且可以在之后对该值进行访问或修改。很明显需要一套良好的规则来存储这些变量,并且之后可以方便的找到这些变量,这套规则我们称之为作用域。
编译原理
我们一般把 js 归为「动态」或「解释执行」语言,但是它也会经历编译阶段,不过它不像传统语言那样是提前编译的,它的编译发生在代码执行前的几微秒内。
传统语言在执行之前会经历三个步骤:分词/词法分析、解析/语法分析、代码生成,关于这三个步骤的具体工作,可以查看编译原理相关的文献,我们可以把这三个步骤统称为编译。不过 js 引擎要复杂的多,它会在编译的时候对代码进行性能优化,尽管给 js 引擎优化的时间非常少,但是它用尽了各种办法来保证性能最佳。
我们需要先了解三个名词。引擎:从头到尾负责整个 js 程序的编译及执行过程;编译器:负责词法分析及代码生成;作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
var a = 2;,我们以这段程序为例,它首先声明了变量a,然后将2赋值给变量a。前一个阶段在编译器处理,后一个阶段由 js 引擎处理。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
变量提升
用过 js 的人都知道 js 存在变量提升,那么它到底是如何提升的呢?我们看下面的一段代码
console.log(a);
var a = 2;
上述代码在a声明之前访问了变量a,按我们的逻辑它应该会抛出 ReferenceError 异常;或是变量提升直接输出 2。但是这两种答案都不对,输出的是undefined。
回顾一下前文的关于编译的内容,引擎会在解释 js 代码之前对其进行编译,编译阶段的一个重要工作就是找到所有的声明,并用合适的作用域将它们关联起来,包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理。所以我们前面列出来的代码实际上会变成下面这个样子。
var a;
console.log(a);
a = 2;
这个过程就好像变量和函数声明会从它们的代码中出现的位置被移动到最上面一样,这个过程就是提升。但是需要注意的是,函数声明会首先被提升,然后才是变量提升。
foo(); // 1
var foo;
function foo() {
console.info(1);
}
foo = function() {
console.info(2);
}
这段代码输出 1 而不是 2 ,它会被引擎理解为下面的形式。
function foo() {
console.log(1);
}
foo(); // 1
foo = function() {
console.log(2);
};
可以看到,虽然var foo出现在function foo()之前,但是它是重复的声明,因此会被忽略掉,因为函数函数声明会提升到普通变量前。所以在在同一个作用域中进行重复定义是一个很糟糕的做法,经常会导致各种奇怪的问题。
LHS 和 RHS 查询
LHS 和 RHS 是数学领域内的概念,意为等式左边和等式右边的意思,在我们现在的场景下就是赋值操作符的左侧和右侧。当变量出现在赋值操作符的左边时,就进行 LHS 查询;反之进行 RHS 查询。
RHS 查询与简单的查找某个变量的值没什么区别,它的意思是取得某某的值。而 LHS 查询则是试图找到变量容器的本身,从而可以对其进行赋值。
console.info(a);我们深入研究一下这句代码。这里对a的引用是 RHS 引用,因为这里a并没有赋予任何值,相应的需要查找并取得a的值,这样才能传递给console.info()。
a = 2;对a的引用则是一个 LHS 引用,因为实际上我们并关心a当前的值是什么,只是想为= 2这个赋值操作找到一个目标。
function foo(a) {
console.info(a);
}
foo(2);
为了加深印象,我们再来分析一下上述代码中的 RHS 和 LHS 引用。最后一行foo()函数的调用需要对foo进行 RHS 引用。这里有一个很容易被忽略的细节,2 被当作参数传递给foo()函数时,2 会被分配给参数a,为了给参数a(隐式地)分配值,需要进行一次 LHS 查询,也就是说代码中隐含了a = 2的语句。
前文已经说过了console.info(a);会对a进行一次 RHS 查询,需要注意的是console.info()本身也需要一个引用才能执行,因此会对console对象进行 RHS 查询,并检查得到的值中是否有一个log方法。
为什么区分 LHS 和 RHS
我们考虑下面的一段代码,就可以为什么要区分 LHS 和 RHS 查询了,而且区分它们是分厂有必要的。
function foo(a) {
console.info(a + b);
b = a;
}
foo(2);
第一次对b进行 RHS 查询时是无法找到该变量的,这是一个未声明的变量,在任何相关的作用域中都无法找到它。如果 RHS 查询在所有嵌套作用域中都找不到该变量,引擎就会抛出 ReferenceError 异常。
引擎在执行 LHS 查询时,如果在全局作用域中也无法找到目标变量,全局作用域就会创建一个具有该名称的变量,并将其返还给引擎。
需要注意的是,在严格模式下是禁止自动或隐式地创建全局变量的,因此在严格模式中 LHS 查询失败时,引擎同样会抛出 ReferenceError 异常。
接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个值进行不合理的操作,比如对一个非函数类型的值进行函数调用,那么引擎就会抛出另一种叫做 TypeError 的异常。
作用域链
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中,在 Web 浏览器中,全局执行环境被认为是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。
每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,这个函数调用的压栈出栈是一样的。
当代码在环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终都是当前执行的代码所在环境的变量对象,说的比较抽象,我们可以看下面的示例。
var color = "blue";
function changeColor() {
var anotherColor = "red";
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问 color、anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors();
}
// 这里只能访问 color
changeColor();
下面的图形象的展示了上述代码的作用域链,内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。函数参数也被当做变量来对待,因此其访问规则与执行环境中的其它变量相同。
window
|-----color
|-----changeColor()
|----------anotherColor
|----------swapColors()
|----------tempColor
作用域链还用于查询标识符,当某个环境中为了读取或写入而引入一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符,如果在局部环境中找到了该标识符,搜索过程就停止,变量就绪;如果在局部环境没有找到这个标识符,则继续沿作用域链向上搜索,如下所示:
var color = "blue";
function getColor() {
var color = "red";
return color;
}
console.info(getColor()); // "red"
在getColor()中沿着作用域链在局部环境中已经找到了color,所以搜索就停止了,也就是说任何位于局部变量color的声明之后的代码,如果不使用window.color都无法访问全局color变量。
Read More ~
JavaScript 性能优化——惰性载入函数
参考资料:
《JavaScript 高级程序设计(第三版)》
JavaScript专题之惰性函数
深入理解javascript函数进阶之惰性函数
因为不同厂商的浏览器相互之间存在一些行为上的差异,很多 js 代码包含了大量的if语句,将执行引导到正确的分支代码中去,比如下面的例子。
function createXHR() {
if (typeof XMLHttpRequest != 'undefined') {
return new XMLHttpRequest();
} else if (typeof ActiveXObject != 'undefined') {
if (typeof arguments.callee.activeXString != 'string') {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
var i, len;
for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
} catch (e) {
// skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error('No XHR object available.');
}
}
我们可以发现,在浏览器每次调用createXHR()的时候,它都要对浏览器所支持的能力仔细检查,但是很明显当第一次检查之后,我们就应该知道浏览器是否支持我们所需要的能力,因此除第一次之外的检查都是多余的。即使只有一个if语句也肯定要比没有if语句慢,所以if语句不必每次都执行,那么代码可以运行的更快一些,惰性载入就是用来解决这种问题的技巧。
函数重写
要理解惰性载入函数的原理,我们有必要先理解一下函数重写技术,由于一个函数可以返回另一个函数,因此可以在函数内部用新的函数来覆盖旧的函数。
function sayHi() {
console.info('Hi');
sayHi = function() {
console.info('Hello');
}
}
我们第一次调用sayHi()函数时,控制台会打印出Hi,全局变量sayHi被重新定义,被赋予了新的函数,从第二次开始之后的调用都会打印出Hello。惰性载入函数的本质就是函数重写,惰性载入的意思就是函数执行的分支只会发生一次。
惰性载入
我们来看一个例子(例子来源于冴羽所写的JavaScript专题之惰性函数)。现在需要写一个foo函数,这个函数返回首次调用时的Date对象,注意是首次。
方案一
var t;
function foo() {
if (t) return t;
t = new Date()
return t;
}
// 此方案存在两个问题,一是污染了全局变量
// 二是每次调用都需要进行一次判断
方案二
var foo = (function() {
var t;
return function() {
if (t) return t;
t = new Date();
return t;
}
})();
// 使用闭包来避免污染全局变量,
// 但是还是没有解决每次调用都需要进行一次判断的问题
方案三
function foo() {
if (foo.t) return foo.t;
foo.t = new Date();
return foo.t;
}
// 函数也是一种对象,利用这个特性也可以解决
// 和方案二一样,还差一个问题没有解决
方案四
var foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
};
// 利用惰性载入技巧,即重写函数
惰性载入函数有两种实现方式,第一种是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一种按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行分支了。
第二种实现方式是在声明函数时就指定适当的函数。这样第一次调用时就不会损失性能了,而是在代码首次加载时会损失一点性能,即是利用闭包写一个自执行的函数。
改进 createXHR
有了上面的基础,我们就可以将createXHR()改进为下列形式,这样就不用每次调用都进行判断了。
// 第一种实现方式
function createXHR() {
if (typeof XMLHttpRequest != 'undefined') {
createXHR = function() {
return new XMLHttpRequest();
}
} else if (typeof ActiveXObject != 'undefined') {
createXHR = function() {
if (typeof arguments.callee.activeXString != 'string') {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
var i, len;
for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
} catch (e) {
// skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
createXHR = function() {
throw new Error('No XHR object available.');
}
}
}
// 第二种实现方式
function createXHR() {
if (typeof XMLHttpRequest != 'undefined') {
return function() {
return new XMLHttpRequest();
}
} else if (typeof ActiveXObject != 'undefined') {
return function() {
if (typeof arguments.callee.activeXString != 'string') {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
var i, len;
for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
} catch (e) {
// skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
return function() {
throw new Error('No XHR object available.');
}
}
}
Read More ~
Bootstrap-table 如何合并相同单元格
Bootstrap-table 官方提供了合并单元格方法 mergeCells,它根据四个参数可以合并任意个单元格,我们要做的只是告诉它怎么合并。
要合并同一列相同的单元格,无非两种办法,一种是一边遍历一边合并,遍历完了再合并。这里采用第二种办法,这里不需要遍历所有数据,因为用户只能看到当前页的数据,所以只遍历当前页的数据更省时间。
下面是我实现的获取合并信息算法,最终返回的是一个哈希表,比如下面的这个表格,如果要对「性别」这一列进行合并,很明显前面两个“男”需要合并成一个单元格,再去看下 Bootstrap-table 提供的 API,它需要的是从哪个单元格开始,合并多少个单元格,也就是它需要的是两个数值类型的参数。
姓名
性别
年龄
张三
男
23
李四
男
19
王二
女
20
麻子
男
21
所以我把哈希表设置为,键存的是索引,值存的是从这个索引开始后面连续有多少个和它一样的单元格,那么上述表格性别这一列所得到的合并信息哈希表就为:
{
0: 2,
2: 1,
3: 1
}
下面算法很简单,使用两个指针遍历指定的列,如果两个指针所指向的数据相同,那么就将键所对应的值进行加一操作,整个方法只会对该列数据遍历一边,所以时间复杂度为 O(n)。
let getMergeMap = function (data, index: number) {
let preMergeMap = {};
// 第 0 项为表头,索引从 2 开始为了防止数组越界
for (let i = 2; i < data.length; i++) {
let preText = $(data[i-1]).find('td')[index].innerText;
let curText = $(data[i]).find('td')[index].innerText;
let key = i - 2;
preMergeMap[key] = 1;
while ((preText == curText) && (i < data.length-1)) {
preMergeMap[key] = parseInt(preMergeMap[key]) + 1;
i++;
preText = $(data[i - 1]).find('td')[index].innerText;
curText = $(data[i]).find('td')[index].innerText;
}
// while循环跳出后,数组最后一项没有判断
if (preText == curText) {
preMergeMap[key] = parseInt(preMergeMap[key]) + 1;
}
}
return preMergeMap;
}
上述算法得到了单列数据的合并信息,下一步就是按照这个信息进行相同单元格的合并了,因此封装了下面的方法按照指定哈希表进行合并。
let mergeCells = function (preMergeMap: Object, target, fieldName: string) {
for (let prop in preMergeMap) {
let count = preMergeMap[prop];
target.bootstrapTable('mergeCells', { index: parseInt(prop), field: fieldName, rowspan: count });
}
}
到目前为止,我们实现的都只是对单列数据进行合并,要实现对多列数据进行合并,那么只需要对所有列都进行相同的操作即可。
export let mergeCellsByFields = function (data: Object[], target, fields) {
for (let i = 0; i < fields.length; i++) {
let field = fields[i];
// 保证 field 与 i 是相对应的
let preMergeMap = getMergeMap(data, i);
let table = target.bootstrapTable();
mergeCells(preMergeMap, table, field);
}
}
因为我在程序中做了一点处理,保证了fields中每个值得索引与对应表头的索引是一样的,因此不需要额外传入索引信息。简单来说就是我所实现的表格会根据fields的顺序,实现列之间的动态排序。你需要注意的是这一点很可能和你不一样。
到现在已经能够合并所有的列了,查看 Bootstrap-table 的配置信息发现,它有个属性是 onPostBody 它会在 table body 加载完成是触发,所以把这个属性配置成我们的合并单元格方法即可。
// groups 为要合并的哪些列
onPostBody: function () {
mergeCellsByFields($('#table' + ' tr'), $('#table'), groups);
}
再说一点不太相关的,我实现的是让用户可以自己选可以合并多少列,即用了一个可多选的下拉列表框供用户选择,根据用户选择的数量去合并,所以传入了一个groups参数。
最后推荐一个排序插件 thenBy,你可以用它进行多字段排序,比如用在合并相同单元格的场景,在绘制表格前先对数据进行排序,那么最后合并的结果就是把所有相同的数据聚合到一起了,并且还将它们合并到一起了,起到了一个隐形的过滤查询功能。
Read More ~
学习 Angulr 容易忽略的知识点
参考内容:
《Angulr5 高级编程(第二版)》
函数声明式和表达式
// 第一种:函数声明式
myFunc();
function myFunc(){
...
}
// 第二种:函数表达式
myFunc();
let myFunc = function(){
...
}
虽然上面两种函数声明方式在大部分情况下是一样的,第一种可执行,第二种却不可以执行,这是因为浏览器在解析 js 时找到函数声明,并在执行剩余语句之前设置好函数,此过程称为函数提升,但是函数表达式却不会受到提升,因此无法正常工作。
js 不具备多态性
js 重不能创建名称相同但参数不同的两个函数,它不具备这个多态性,比如你定义的函数中有两个形参,调用函数时只传一个参数,第二形参的值就是 undefined ,如果传的参数大于 3 个,那么会自动忽略多余的参数。可以使用下列方法来处理函数定义参数数量和用于调用函数实际参数数量之间不匹配的问题。
// 使用默认参数
let func = function(age, sex='男'){
...
}
func(23);
// 使用可变长参数
let func = function(age, sex, ...extraArgs){
...
}
func(23, '女', '张三', '深圳');
// 最后一个参数是一个数组,任何额外的实参都会被赋给这个数组
let 和 war 的区别
使用 let 和 var 声明变量的区别,使用 let 声明变量会把变量的作用范围限定在它所在的代码区域内。而使用 var 所创建的变量的作用域是它所在的函数。
function func(){
if(false){
var age = 23;
}
}
// 上面的代码会被解析成下面的形式,使用 let 则不会出现这样的结果
function func(){
var age;
if(false){
age = 23;
}
}
相等 == 和恒等 === 以及 连接操作符 +
相等操作符尝试将操作数强制转换为相同的类型,再评估是否相等,实质上相等操作符==是测试二者的值是否相等,而与二者的类型无关;如果要测试值和类型是否都相等则应该用恒等操作符===。
5 == '5' // 结果为 true
5 === '5' // 结果为 false
在 js 中,连接操作符的优先级高于加法操作,也就是说5 + '5'的结果是55。
不同的模块指定方式
import { Name } from "./modules/NameUtil";// 第一种
import { Compont } from "@angular/core";// 第二种
上面两种导入模块的方式有所不同,第一种是相对模块,第二种是非相对导入。第一种告诉的 TypeScript 编译器,该模块所在的位置是相对于包含 import 语句的文件而言;第二种非相对导入,编译器会用 node_modules 文件夹中的 npm 包来解析它。
如果在导入模块时,出现需要导入两个不同模块但是名字却相同的情况,可以使用as关键字给导入的模块取一个别名。
import { Name as otherName } from "./modules/Name";//取别名
还有一种方法是将模块作为对象导入,如下 import 所示,导入 Name 模块的内容,并创建一个名为 otherName 的对象,然后就可以使用该对象的属性了。
import * as otherName from "./modules/NameUtil";
let name = new otherName.Name("Admin", "China");// Name 是 NameUtil 中的类
多类型和类型断言
在 ts 中允许指定多个类型,使用字符|进行分隔。看下面的的方法,其功能是把华氏温度转换为摄氏温度。
// 使用多类型,该函数可以传入 number 和 string 类型的参数
static convertFtoC(temp: number | string): string {
/*
尝试使用 <> 声明一个类型断言,将一个对象转换为指定类型,也可以使用 as 关键字实现下列相同的效果
let value: number = (temp as number).toPrecision ? temp as number : parseFloat(temp as string);
*/
let value: number = (<number>temp).toPrecision ? <number>temp : parseFloat(<string>temp);
return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
元组是固定长度的数组,数组的每一项都是指定的类型;可索引类型可以将键与值关联起来,创建类似于 map 的集合。
// 元组
let tuple: [string, string, string];
tuple = ["a", "b", "c"];
// 可索引类型
let cities: {[index: string] : [string, string]} = {};
cities["Beijing"] = ["raining", "2摄氏度"];
数据绑定
[target]="expr"// 方括号表示单向绑定,数据从表达式流向目标;
(target)="expr"// 圆括号表示单向绑定,数据从目标流向表达式,用于处理事件的绑定;
[(target)]="expr"// 圆方括号组合表示双向绑定,数据在表达式与目标之间双向流动;
{{ expression }}// 字符串插入绑定。
[] 绑定有很多不同的形式,下面介绍不同表现形式的效果。
<!--
标准属性绑定(dom对象有的属性),将 input 的 value 属性绑定到一个表达式的结果
因为 model.getProduct(1) 可能返回 null ,所以使用模板空条件操作符 ? 浏览返回结果
如果返回不为空,那么将读取 name 属性,否则由 null 合并操作符 || 将结果设置为 None
字符串插入绑定也可以使用这种表达式
-->
<input [value]="model.getProduct(1)?.name || 'None'">
<!--
元素属性绑定,有时候我们需要绑定的属性在 DOMAPI 上面没有
可以使用通过在属性名称前加上 attr 前缀的方式来定义目标
-->
<td [attr.colspan]="model.getProducts().length">
{{ model.getProduct(1)?.name || 'None' }}
</td>
<!-- 还有其他的 ngClass,ngStyle 等绑定,理解大体上和上面差不多 -->
内置指令
<!--
ngIf指令,如果表达式求值结果为 true ,那么 ngIf 将宿主元素机器内容包含在 html 文件中
指令前面的星号表示这是一条微模板指令
组要注意的是,ngIf 会向 html 中添加元素,也会从中删除元素,并非只是显示和隐藏
如果只是控制可见性,可以使用属性绑定挥着样式绑定
-->
<div *ngIf="expr"></div>
<!--
ngSwitch指令,
-->
<div [ngSwitch]="expr">
<span *ngSwitchCase="expr"></span>
<span *ngSwitchDefault></span>
</div>
<!--
ngFor指令,见名知意,为数组中的每个对象生成同一组元素
ngFor 指令还支持其他的一系列可赋给变量的值,有如下局部模板变量
index:当前对象的位置
odd:如果当前对象的位置为奇数,那么这个布尔值为 true
even:同上相反
first:如果为第一条记录,那么为 true
last:同上相反
-->
<div *ngFor="let item of expr; let i = index">
{{ i }}
</div>
<!--
ngTemplateOutlet指令,用于重复模板中的内容块
其用法如下所示,需要给源元素指定一个 id 值
<ng-template #titleTemplate>
<h1>我是重复的元素哦</h1>
</ng-template>
<ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
...省略若万行 html 代码
<ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
-->
<ng-template [ngTemplateOutlet]="myTempl"></ng-template>
<!--
下面两个指令就是见名知意了,不解释
-->
<div ngClass="expr"></div>
<div ngStyle="expr"></div>
事件绑定
事件绑定使用 (target)="expr",是单向绑定,数据从目标流向表达式,用于响应宿主元素发送的事件。
当浏览器触发一个时间时,它将提供一个对象来描述该事件,对于不同类型的事件有不同类型的事件对象,事件对象被赋给一个名为$event的模板变量,但是所有事件对象都有下面三个属性:
type:返回一个 string 值,用于标识已触发事件类型;
target:返回触发事件的对象,一般是 html元素对象。
timeStamp:返回事件触发事件的 number 值,用 1970.1.1 毫秒数表示。
下面举几个例子,作为理解帮助使用。
<!-- 当数鼠标在上面移动时,就会触发 mouseover 事件 -->
<td *ngFor="let item of getProducts()" (mouseover)="selectedProduct = item.name"></td>
<!-- 当用户编辑 input 元素的内容时就会触发 input 事件 -->
<input (input)="selectedProduct=$event.target.value" />
<input (keyup)="selectedProduct=product.value" />
<!-- 使用事件过滤,上面的写法按下任何一个键都会触发事件,而下面的写法只有回车事件才会触发事件 -->
<input (keyup.enter="selectedProduct=product.value") />
表单验证
Angular 提供了一套可扩展的系统来验证表单元素的内容,总共可以向 input表元素中添加 4 个属性,每个属性定义一条验证规则,如下所示:
required:用于指定必须填写值;
minlength:用于指定最小字符数;
maxlength:用于指定最大字符数,(不能在表单元素直接使用,因为它与同名的 H5 属性冲突);
pattern:该属性用于指定用户填写的值必须匹配正则表达式
<!--
Angular 要求验证的元素必须定义 name 属性
由于 Angular 使用的验证属性和 H5 规范使用的验证属性相同,
所以向表单元素中添加 novalidate 属性,告诉浏览器不要使用原生验证功能
ngSubmit 绑定表单元素的 submit 事件
-->
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<button type="submit">提交</button>
</form>
Angular 提供了 3 对验证 CSS 类,这些类可以用于样式化表单元素,向用户提供验证反馈,具体说明如下所示。
ng-untouched ng-touched:如果一个元素未被用户访问,就将其加入到 nguntouched 类中;一旦访问就加入到 ngtouched 类中。
ng-prisstine ng-dirty:元素内容没有被改变被加入到 ng-prisstine 类中,否则将其加入到 ng-dirty 类中。
ng-valid ng-invalid:如果满足验证规则定义的条件,就加入到 ng-valid 类中,否则加入到 ng-invalid 类中。
在实际使用过程中,直接定义对应的样式即可,如下所示:
<style>
input.ng-dirty.ng-invalid{
border: 2px solid red;
}
input.ng-dirty.ng-valid{
border: 2px solid green;
}
</style>
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<button type="submit">提交</button>
</form>
上面的验证方式无法给用户提供更加具体的信息,用户不知道应该做什么,可以使用 ngModel 指令来访问宿主元素的验证状态,当存在验证错误的时候,使用该指令向用户提供指导性信息。
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
#nameRef="ngModel"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<ul class="text-danger list-unstyled"
*ngIf="name.dirty && name.invalid">
<li *ngIf="name.errors?required">
you must enter a product name
</li>
<li *ngIf="name.errors?.pattern">
product name can only contain letters and spases
</li>
<li *ngIf="name.errors?minlength">
<!--
Angular 表单验证错误描述属性
required:如果属性已被应用于 input 元素,此属性返回 true
minlength.requiredLength:返回满足 minlength 属性所需的字符数
minlength.actualLength:返回用户输入的字符数
pattern.requiredPattern:返回使用 pattern 属性指定的正则表达式
pattern.actualValue:返回元素的内容
-->
product name must be at least {{ name.errors.minlength.requiredLenth }} characters
</li>
</ul>
<button type="submit">提交</button>
</form>
如果在用户尝试提交表单时就显示大量的错误信息,给人的体验感就会很差,所以可以让用户提交表单时再验证整个表单,示例代码如下所示。
export class ProductionCompont {
// ...省略若万行代码
formSubmited: boolean = false;
submitForm(form: ngForm) {
this.formSubmited = true;
if(form.valid) {
this.addProduct(this.newProduct);
this.newProduct = new Product();
form.reset();
this.formSubmited = true;
}
}
}
<form novalidate #formRef="ngForm" (ngSubmit)="submitForm(formRef)">
<div *ngIf="formsubmited && formRef.invalid">
there are problems with the form
</div>
<!-- 禁用提交按钮,验证成功提交按钮才可用 -->
<button [disabled]="formSubmited && formRef.valid">提交</button>
</form>
fromSubmited 属性用于指示表单是否已经提交,并将用于在用户提交整个表单之前阻止表单验证。当用户提交表单时,调用 submitForm 方法,并将 ngForm 对象作为实参传入,ngForm 提供了 reset 方法,该方法可以重置表单的验证状态,使其返回到最初的未访问状态。
更高级的还有使用基于模型的表单验证,可以自行查阅相关资料。
使用 json-server 模拟 web 服务
因为json-server会经常用到,建议使用全局安装命令npm install -g json-server。因为开发后端的同学太慢了,而我们如果要等他们把接口都提供给我们的时候再开发程序的话,那效率就太低了,所以使用 json-server 来模拟后端服务。只需要建好一个 json 文件,比如下面的格式:
{
"user" : [
{
"name" : "张三",
"number" : "1234",
},
{
"name" : "王二",
"number" : "5678",
}
],
"praise": [
{"info":"我是一只小老虎呀!"},
{"info":"我才是大老虎"}
]
}
启动服务使用命令json-server [你的 json 文件路径],然后就可以根据提示访问了,你甚至可以使用http://localhost:3000/user?number=5678去过滤数据。这样就能模拟 web 服务,而不必等后端同学的进度了。
解决跨域请求问题
Angular 跨域请求问题可以通过 Angular 自身的代理转发功能解决,在项目文件夹下新建一个 proxy.conf.json 并在其中添加如下内容。
// 可以通过下列配置解决
"/api": {
"target": "http://10.9.176.120:8888",
}
在启动时使用npm start,或者使用ng serve --proxy-config proxy.conf.json,Anular 中的/api请求就会被转发到 http://10.9.176.120:8888/api,从而解决跨域请求问题。
使用第三方 js 插件
共有三种方式引入第三方插件,第一种很简单,直接在 html 中引入插件就可以了;第二种在angular.json中进行配置;第三种在 ts 文件中使用 import 导入库即可。
// 第一种(需要重启服务)
"scripts": ["src/assets/jquery-3.2.1.js","src/assets/jquery.nicescroll.js","src/assets/ion.rangeSlider.js"]
// 第二种
<script type="text/javascript" src="assets/jquery-3.2.1.js"></script>
<script type="text/javascript" src="assets/jquery.nicescroll.js"></script>
// 第三种
import "assets/jquery-3.2.1.js";
import "assets/jquery.nicescroll.js";
import "assets/ion.rangeSlider.js";
深拷贝与浅拷贝
深拷贝与浅拷贝是围绕引用类型变量说的,其本质区别是不可变性,基本类型是不可变得,而引用类型是可变的。
直接使用赋值操作符,就是浅拷贝,如果对拷贝源进行操作,会直接影响在拷贝目标上,因为这个赋值行为本质是内存地址的赋值,为了获得与拷贝源完全相同但又不会影响彼此的对象就要使用深拷贝。
let objA = {
x: 1,
y: -1
}
let objB = objA;
objA.x++;
console.log("objA.x:"+objA.x, "objB.x:"+objB.x);
//打印结果如下:
objA.x : 2
objB.x : 2
Typescript 提供了一种方法来实现引用类型的深拷贝,即Object.assign(target, ...source),此方法接受多个参数,第一个参数为拷贝目标,剩余参数为拷贝源,同名属性会进行覆盖。
let objA = {
x: 1,
y: -1,
c: {
d: 1,
}
}
let objB = {};
Object.assign(objB, objA);
objA.x++;
console.log("objA.x:"+objA["x"], "objB.x:"+objB["x"]);
//打印结果如下:
objA.x : 2
objB.x : 1
需要注意的是,Typescript 提供的深拷贝方法不能实现嵌套对象的深拷贝,会出现下面的情况。
let objA = {
x: 1,
y: -1,
c: {
d: 1,
}
}
let objB = {};
Object.assign(objB, objA);
objA.c.d++;
console.log("objA.c.d:"+objA["c"].d, "objB.c.d:"+objB["c"].d);
//打印结果如下:
objA.c.d : 2
objB.c.d : 2
要实现嵌套对象的深拷贝,可以使用 JSON 对象提供的方法,JSON 对象提供了两个方法,分别为:stringify()和parse(),前者将对象 JSON 化,后者将 JSON 对象化,使用这种方式可以实现嵌套深拷贝,但是也有缺点:破坏原型链,不能拷贝属性值为 function 的属性。
let objA = {
a: 1,
b: {
c: 1
}
}
let objB = JSON.parse(JSON.stringify(objA));
objA.b.c++;
console.log("objA.b.c:"+objA.b.c, "objB.b.c:"+objB.b.c);
//打印结果如下:
objA.b.c:2
objB.b.c:1
Read More ~