时允

时允 查看完整档案

北京编辑  |  填写毕业院校美团网  |  高级前端工程师 编辑填写个人主网站
编辑

美团成都部门招聘 RD,有意者联系我

个人动态

时允 发布了文章 · 2019-04-01

怎么避免写出烂代码

命名

规则:除非在小于 5 行的函数里,否则不要使用单字命名变量

说明:含义不清晰,不能做到「望文生义」

BadCode

var l = data.length;

GoodCode

// 闭包只有一行代码,可以使用单字变量
data.map(d => d.length)

规则:不要使用名词加数字的命名方法

说明:含义不清晰,不能做到「望文生义」

BadCode

var obj = {};
var obj2 = {...obj, key: 1};

GoodCode

var obj = {};
var objWithKey = {...obj, key: 1};

规则:应该且只有方法和函数以动词开头

此处动词没有包含时态
变量名应该是名词或者名词短语。

例外:

  • 回调函数
  • 生命周期函数
  • 框架级别函数
  • getter/setter

说明:

  • 函数的命名需要体现内部工作
  • 值变量以动词命名容易让人误解为是一个匿名函数

BadCode

// 以名词开头,看不明白什么有什么功能
function option() {}

// 时态不对
function updatedTime() {}

GoodCode

function selectOption() {}
function updateTime() {}

规则:避免使用拼音或者缩写命名。

例外:

  • 专有名词:weixin/POI
  • 传统约定:i/j/k 表示循环索引

说明:含义不清晰,不能做到「望文生义」

BadCode

var uo = function updateOrder(){}
var as = [].slice;
var ex = Object.extends;
var nu = number

GoodCode

// weixin/wx 是专有名词
var weixinUser = {};
var wx = weixin;
// POI 是专有名词
var poi = {};

规则:名称长短应与其作用域大小相对应。

例外:专有 API,如 alert
说明:在上层作用域下的代码,会被更多函数使用到。其名称应该尽量长或者通用,以保证能够搜索到。

BadCode

// 在全局变量上定义了一个 item 变量,但是很难从命名上理解其作用是什么。
window.item = {}

GoodCode

window.primaryProductItem = {};

不要在变量/函数尾部加符号、数字

说明:变量中加符号,往往是为了约定其优先级或者作用域。符号应该在变量名前面。

BadCode

function getDot_(){}
function privateFn$$ (){}

GoodCode

function _getDot() {}
function $$privateFn() {}

规则:实例名称要和类名相关

说明:类作为实例的所属,其名称表达的含义要一脉相承

BadCode

class Person() {}
var dog = new Person(); // dog is a Person ?

GoodCode

class Person() {}
var jack = new Person();

规则:避免直白的中英文翻译

说明:粗暴的翻译,更容易造成误解,还不如写拼音

BadCode

// 渲染「页面顶部的执行人」
// 还是渲染「执行砍头的人」?
function renderHeadExecutantPeople(){}

GoodCode

function renderHeader() {}

规则:概念的命名要一以贯之

说明:避免通一个概念在不同的代码用多种不同的单词描述。

BadCode

// 远程请求这个概念,先后用了 get/fetch/query
// 三个名词去描述
function getUserInfo() {}
function fetchProductInfo() {}
function queryPayment() {}

GoodCode

// 统一用 get 描述
// 阅读代码的人能体会到其中的共性
function getUserInfo() {}
function getProductInfo() {}
function getPayment() {}

规则:别用双关语

例外:专有词
说明:双关语容易引起歧义

BadCode

// 订单类型,还是排序类型?
var orderType

GoodCode

var sortType

规则:命名需要和实现一致

说明:命名往往是实现的隐喻,如果存在差异则会让阅读者看不懂代码。

BadCode

// empty 的命名含义和实现截然相反
// (我真的见过这种代码)
function getProduct(id) {
    axios.delete('/product', {id}); 
}

GoodCode

function deleteProduct(id) {
    axios.delete('/product', {id}); 
}

规则:对于布尔值的命名,需要默认其为「真」

说明:布尔变量的名称中,如果加上 「not」之类的否定词,则相当于做了一次了逻辑判断。

BadCode

const notEmpty = !!array.length;

GoodCode

const empty = !array.length;

函数

规则:长度不能超过 20 行

说明:代码太长说明做的事情不够专一,同时也会让阅读变得很困难。

规则:Don't repeat yourself

说明:同样功能的代码不要重复三次

规则:每个函数只做一件事情,并做好这件事

说明:代码里的逻辑分支要尽量少,只做一件事情,并且要处理好边界和异常情况。

规则:尽量减少函数的参数,包括 opitons、config 等参数

说明:函数的输入越多,往往就代表功能约复杂

规则:注释出来项目/业务的坑

说明:对于比较奇怪的业务逻辑,或者因为系统、接口原因而写的比较奇怪的逻辑。要通过注释标注出来

BadCode

framework.doSomeThing();
framework.reset(); // 阅读者内心 OS:这里为啥要做一次 reset?
framework.continueSomeThing();

GoodCode

framework.doSomeThing();
// framework 有个 bug,这里必须要做一次 rest 附链接: http://github.com/issuse/***
framework.reset(); 
framework.continueSomeThing();

规则:函数要尽量「纯」没有副作用

说明:纯函数比较好测试,逻辑也比较清晰,可以放心的引入和删除。

BadCode

let status;
function method() {
  if (status) { ... } 
}

GoodCode

function method(status) {
  if (status) { ... } 
}

规则:函数最好不要修改参数内的数据

说明:修改参数会导致函数的作用变得不可预测

BadCode

function updateObj(obj, value) {
  obj.key = value;
  return obj;
}

GoodCode

function updateObj(obj, value) {
  return {...obj, key: value};
}

规则:除非是 class 的方法,否则不要访问 this

说明:this 的指向经常不固定,会导致代码难以理解。如果调用方不熟悉的话,很容易引起 Bug。

BadCode

function method() {
    console.log(this.value); 
}

method() // 报错
var obj = { method, value: 1}
obj.method() // 输出 1

GoodCode

function method(value) {
    console.log(value); 
}

规则:处理错误

说明:错误也是一种逻辑分支,如果不处理的话,代码就不够健壮。前端代码处理错误的方式一般为提示用户有异常发生。如果错误不影响业务流程,则写入日志里并上报。

BadCode

function method(data) {
    try { return JSON.parse(data) }
  catch (e) {}
}

GoodCode

function method(data) {
    try { return JSON.parse(data) }
  catch (e) {
      alert('数据处理失败')
  }
}

数据

规则:不要有 Magic Number

说明:magic number 是指直接在代码中硬编码的数字,往往具有一些业务含义。

这样会导致:

  • 数字的意义难以理解
  • 数值要改动时,要改很多地方

BadCode

if (status === 1) {
    ...
} else if (type === 4) {
  ...
}

GoodCode

enum Status {
    Closed 
}
enum Type {
    Array 
}

if (status === Status.Closed) {
    ...
} else if (type === Type.Array) {
  ...
}

规则:不管是 react state 还是 vue data 存放的业务数据都要具备原子性。

说明:原子性意味着独立,且不可分割。其它属性都由原子业务属性推导、计算而来,这样能保证状态的一致。

BadCode

// 当 status 为 open 的时候展示弹窗
// 其它状态则隐藏弹窗
{
    data() {
    return {
        showAlert: false,
      status: 'closed',
    }
  },
  onStatusChange() {
      if (status === 'open') {
        this.showAlert = true;
    } else {
        this.showAlert = false; 
    }
  }
}

GoodCode

// showAlert 为非原子的状态
// 其状态可以由 status 推导而来
{
    data() {
    return {
      status: 'closed',
    }
  },
  computed: {
      showAlert() {
        return this.status === 'open';
    }
  }
}

规则:对于 react state 和 vue data,应当区分业务状态和 UI 状态

说明:

  • 状态和 UI 存储在一起,有时候传给后端的数据里会夹杂着没有必要的 UI 状态。
  • 业务代码和 UI 代码耦合在一起,业务代码没法复用。

BadCode

// 在一个列表中,用户可以对数据做多选
// 然后删除他们
class extends React.Component {
     async componentDidMount() {
      const listData = getData();
    this.setState({ listData })
  }
  
  check = (item) => {
    const listData = this.state.listData.map(i => {
        if (i === item) {
        return {...item, checked: true}
      }
      return i;
    });
    
    this.setState({ listData });
  }
  
  delete() {
    // 返回给后端的数据结构,会多出一个 checked 字段
    deleteItems(this.state.listData.filter(i => i.checked));
  }
  
    render() {
    const list = this.state.listData.map(item => {
      const className = ['item'];
      if (item.checked) className.push('active');
        return <label
          className={className}
          onClick={() => this.check(item)}
      >{item.naem}</label>;
    });
    
    return <>
      {list}
        <button onClick={this.delete}>delete</button>
    </>
  }
}

GoodCode

// 在一个列表中,用户可以对数据做多选
// 然后删除他们
class extends React.Component {
     async componentDidMount() {
      const listData = getData();
    // 使用独立的 selected 来保存 UI 状态
    this.setState({ listData, selected: [] })
  }
  
  check = (item) => {
    let { selected } = this.state;
    selected = selected.findOrInsert(s => s.id, item);
    this.setState({ selected });
  }
  
  delete() {
    const { selected, listData } = this.state;
    deleteItems(listData.filter(i => selected.includes(i.id))));
  }
  
    render() {
       const { selected, listData } = this.state;
    const list = listData.map(item => {
      const className = ['item'];
      if (selected.includes(item.id)) className.push('active');
        return <label
          className={className}
          onClick={() => this.check(item)}
      >{item.naem}</label>;
    });
    
    return <>
      {list}
        <button onClick={this.delete}>delete</button>
    </>
  }
}

规则:对于 react 应用,避免在 render 的时候修改状态

说明:react 的 render 应该是纯函数,在 render 里运行 setState 会导致重复渲染,或者死循环。

BadCode

// 如果 type 为 http 的话,则自动转换为 https
class extends React.Component {
  render() {
      const { type } = this.state;
    if (type === 'http') {
        this.setState({ type: 'https'}) 
    }
    return <label>{type}</label>;
  }
}

GoodCode

// 如果 type 为 http 的话,则自动转换为 https
class extends React.Component {
  get type() {
    const { type } = this.state;
    if (type === 'http') return 'https';
    return type;
  }
  render() {
      const type = this.type;
    return <label>{type}</label>;
  }
}

规则:对于双向绑定应用,避免数据循环依赖。

说明:

  • 循环依赖轻则导致页面相应慢,重则导致出现脏数据。
  • 避免循环依赖的前提是理清业务逻辑,搞清楚数据之间的依赖关系。
  • 循环依赖也是双向绑定技术的诟病之一。

BadCode

// foo 和 bar 互相依赖,导致了死循环
{
    data() {
      return {
        foo: 1,
    }; 
  },
  computed: {
    bar() {
        return this.foo + 1;
    }
  },
  watch() {
    bar() {
        this.foo = this.bar + 1;
    },
  }
}

规则:访问数据时,需要考虑边界情况和 JS 弱类型的特性。

说明:比如用双等号做判断

BadCode

const foo = '0';
const bar = 0

// 做数据判断时不能用双等号
foo == bar // true
foo ? 1 : 2 // 1
bar ? 1 : 2 // 2

// 仅通过变量有没有 length 来判断是否为数组
if(obj.length) {
    obj.forEach(...) 
}

GoodCode

const foo = '0';
const bar = 0

foo === bar // false

if (Array.isArray(obj)) {
     obj.forEach(...) 
}

规则:不要在遍历数组的同时,改变数组数据

说明:这样做会导致数据的异常。如果需要做这种操作,最好使用数组函数,或者操作拷贝数据。

BadCode

const array = [1,2,3,4,5,6,7,8,9,10];

// 删除数组中的偶数
for (var i = 0; i < array.length; i++) {
    if (array[i] % 2 == 0) array.splice(i);
}
// array 变成了 [1]

GoodCode

const array = [1,2,3,4,5,6,7,8,9,10];
array.filter(a => !(a % 2))

API

规则:对 setTimeout 调用时,传递的时间参数必须有意义。

说明:
大多数场景下,setTimeout 后面传递一个时间是为了先执行后续的 A 代码,再延后执行代码闭包里的 B 代码,如右边示例代码。

但如果随着业务迭代,A 被改成异步,或者执行时间很长的话。之前做的延迟执行的防御措施就时效了,也许反而 B 会比 A 先执行。

BadCode

// 代码的本来意图是让 B 延后执行
setTimeout(() => {
  B();
}, 1000);
A();

// 代码的意图是让 render 在下一帧执行
// 但是不同设备,一帧时间是不固定的
setTimeout(() => {
  render()
}, 16);

GoodCode

// A 函数内要想办法使用 Promise 串接起来
await A();
b();

// 使用系统提供的 API 执行动画帧
requestAnimationFrame(() => {
 render(); 
});

规则:不要使用陈旧的 API

说明:陈旧的 API 往往有很多问题,比如安全、性能、不易读等。

BadCode

// 判断是否为数组
Object.prototype.toString.call(array) === "[object Array]" 
// 查找数组里第一个偶数
for (var i = 0; i < array.length; i++) {
    if (array[i] % 2 === 0) return array[i]; 
}
// 遍历对象的 key
for (var key in obj) {
    console.log(key); 
}
// 判断字符串/数组是否包含
'some text'.indexOf('some') >= 0
// 去除首位空格
' some text '.replace(/(^\s+|\s+$)/g, '')
// 新建对象/数组
const array = new Array();
const obj = new Object();

GoodCode

Array.isArray(array)

array.find(a => a % 2 === 0);

Object.keys(obj).forEach(console.log)

'some text'.includes('some')

' some text '.trim()
const array = [];
const obj = {};

规则:对于 99.9% 的场景,你都不需要使用 React ref

说明:
React Ref 一般是用来处理和原生 DOM 交互的场景,比如 canvas。

大部分对于 React ref 的使用都是错误的,大多都拿来用来控制子元素。这种场景我们更推荐用数据流(redux,mobx)或者用状态提升去做。

React 官方有对「状态提升」的描述 https://react.docschina.org/d...

BadCode

class List extends React.Component {
    async refresh() {
      this.setState({
      items: getItems(),
    });
  }
  
  render() {
      return this.state.items.map(i => <label>{i}</label>); 
  }
}
class extends React.Component {
  onRefresh = () => {
    // 用 ref 去调用子元素的方法
    this.list.refresh();
  }
    render() {
    return <>
      <List ref={l => this.list = l}></List>
        <button onClick={this.onRefresh}/>
    </>;
      
  }
}

GoodCode

class List extends React.Component {
  render() {
      return this.props.items.map(i => <label>{i}</label>); 
  }
}
class extends React.Component {
 // 把数据状态提升到父组件做操作                              
  refresh = async () => {
      this.setState({
      items: getItems(),
    });
  }

    render() {
    return <>
      <List items{this.state.items}></List>
        <button onClick={this.refresh}/>
    </>;
      
  }
}

规则:不要用字符串拼接 url

说明:
字符串拼接 url 需要处理 encode 或者 decode 的情况,还有对于 ?和 # 的判断不对的话,很容易造成漏洞或者 Bug。

目前浏览器和 Node 都已经提供了标准的 URL 解析方法。

https://developer.mozilla.org...

BadCode

// 这段代码既没有对 key、value 做 encode
// 也没有考虑 url 中 # 出现的情况
const url = location.href;
if (url.indexOf('?') >= 0) {
    return  url + key + '=' + value;
} else {
  return  url + '?' + key + '=' + value;
}

GoodCode

// 使用标准的 URL 解析,风险会降低很多
const url = new URL(urlStr);
url.searchParams.set(key, value);
return url.toString();

逻辑

规则:判真不判假

说明:
我们应该期望 if 条件内是个「真」值,而不是一个「假」值。

第二种情况会导致代码不易理解。

解决办法参考 布尔逻辑

BadCode

// if 条件内期望的是一个「假」值
if (!(status !== Closed) { ... }
if (!(status !== Closed || type !== Array)) { ...} 

GoodCode

if (status === Closed) { ... }
if (status === Closed && type === Array) { ... }

规则:if 条件中,不易出现超过 3 个逻辑操作符。

例外:if 条件里可以被 「且」(&&)逻辑拆分成多个子条件
说明:复杂的条件判断会让代码不易理解,逻辑上有漏洞的话容易引起 Bug。
解决办法:声明中间变量

BadCode

if (srcElem != dropElem && (srcElem.nextSibling || srcElem.nextElementSibling) != dropElem) {...}
if (selectedItem || (selectedEmployee && selectedEmployee.empId && selectedEmployee) || employee) { ... }

GoodCode

const nextSibling = srcElem.nextSibling || srcElem.nextElementSibling
if (srcElem != dropElem &&  nextSibling != dropElem ) {
  ...
}
  
// 复杂的逻辑判断可以通过 && 做拆分
if (
     !Array.isArray(cur)
  && cur != null
  && typeof src[key] === 'object'
  && typeof cur === 'object'
) { ... }

规则:不要用嵌套的三元表达式

说明:
人们阅读嵌套三元表达式时,容易混淆语法的优先级,进而导致理解错代码的含义。

对于这种情况,建议改成 if else。

如果是在 react render 里,则建议独立成函数。

BadCode

function render(props) {
  const value = props.value;
    return <>
    {value < 10 ? value > 0 ? value : 200 - value : 100 - value}
    </>;
}

GoodCode

function getValue(value) {
  if (value < 10) {
    if (value > 0) return value;
    return 200 - value;
  }
  return 100 - value;
}

function render(props) {
  const value = props.value;
    return <>
    {getValue(value)}
    </>;
}

规则:if 条件逻辑嵌套不要超过三层

说明:过深的嵌套会导致理解困难。
解决办法:合并判断条件,或者独立成函数。

BadCode

if (status = Opened) {
    if (type = 'array') {
            if (code = Success) {
            doSomething();
        }
    }
}

GoodCode

if (status = Opened && type = 'array' &&code = Success) {
    doSomething();
}
查看原文

赞 46 收藏 33 评论 2

时允 发布了文章 · 2018-07-14

前端教学讲义:闭包、高阶函数、原型链

上一章:前端教学讲义:JS基础

闭包、高阶函数

闭包是一种打通两个作用域的能力,在 Java 和 OC 中都有类似的功能,JS 中的闭包和他们没有太大的差别。

不过因为 JS 的函数是一等公民,所以使用起来会更加灵活。

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

接受一个或多个函数作为输入

输出一个函数

下面的例子就是个典型的高阶函数

const multiple = function(a, b) {
    return a * b;
}

const plus = function(a, b) {
    return a + b;
}

const express = function(operator, a, b) {
    return operator(a)(b);
}

[
    [plus, 1, 2],
    [multiple, 3, 4],
].map(function(formula) {
  return express.apply(null, formula)
});
 
// [3, 12]

call,apply,bind

在 JS 中经常会遇到 this 指向错乱的问题:

var obj = {
    value: 1234,
    log: function() {
      console.log(this.value)
    }
}
var log = obj.log;
log(); // 这个函数执行的时候没有上下文,this 会默认为 window 或者 global
 
// undefined

JS 提供了两个方法,call 和 apply,能够指定一个函数执行时候的上下文

log.call(obj);
 
// 1234
 
log.apply(obj);
 
// 1234

call 和 apply 同时还可以指定传给函数的参数是什么,他们的区别就是后面的传参数形式不一样;

function fn(arg1, arg2){}
 
fn.call(obj, arg1, arg2)
fn.apply(obj, [arg1, arg2]) // apply 是将参数当作数组传入

bind 函数通过闭包的形式,来强制的将某个上下文绑定到函数上:

log = bind(log, obj); // 返回一个新函数,此函数的 this 强制被绑定为 obj
log()
 
1234

bind 的实现大概如下(省略传参的功能):

function bind(fn, ctx) {
  return function() {
    return fn.apply(ctx);
  }
}

和上面举例有点区别的是 JS 提供的 bind 方法是 Function 类的一个方法,调用方法如下:

function log() {}
 
log = log.bind(obj)

原型链

JS 的原型链(prototype chain)是为了实现面向对象而存在的,当访问一个对象的方法的时候,首先会遍历对象本身,是否存在这个键,然后在查找父类是否有这个键,再一直追溯到顶层。

「父类」中包含的方法和属性,都存放在对象的 「__proto__」对象上(在旧的 JS 规范中,「__proto__」是隐藏属性,但是 ES6 将它标准化并暴露出来了)。

var c = {
  somekey: 1,
}
var b = {
  __proto__: c,
}
var a = {
__proto__: b
}
a.somekey
 
// 1

当访问 「a.somekey」 的时候,会沿着原型链一直向上查找,相当于做了如下计算:

function getValue(obj, key) {
  while (obj) {
    if (key in obj) {
      return obj[key]
    }
    if (obj.__proto__) {
      obj = obj.__proto__;
    } else {
      return undefined;
    }
  }
}
 
getValue(a, 'somekey')

在最早期的 JS 版本中是没有类的,后来加入了原型链和 new 关键词才实现了类。

function SubClass(value) {
    this.value = value;
}
 
SubClass.prototype = {
    log: function() {
        console.log(this.value);
    }
}

var sub = new SubClass(1234);
sub.log();
 
// 1234

在 JS 中任何函数都可以当作构造函数并搭配 new 关键词使用。

new 关键词的作用就是新建一个空对象 a,然后把构造函数上的 prototype 值挂载到 a.__proto__ 上。

再将 a 作为 context 运行构造函数。

如果我们将 new 作为一个函数,它就是这样:

function new(class, ...params) {
  var instance = {};
  instance.__proto__ = class.prototype;
  var result = class.apply(instance, params);
  if (typeof result !== 'undefined') {
    return result;
  }
  return instance;
}
 
var sub = new(SubClass, 1234);

从上面的代码可以看出,在构造函数中,可以指定 new 关键词返回的类型,也就是说 new A() 返回的结果不一定就是 A 的实例,这要看 A 的构造函数内部是如何实现的。

function A() {
    return 1234;
}
var a = new A();
console.log(a)
 
// 1234

JS 用原型链的方式,曲线的实现了类的行为和写法,所以在 JS 中,「类」就是构造函数。

instanceof

如何判断一个对象是否是一个类的实例呢?

JS 实现的方法很粗糙,就是判断实例的原型链上是否存在类的原型。

function A() {
}
var a = new A();
a instanceof A
 
true

我们甚至可以让任意一个类成为实例的父类

function C() {}
function A() {}
var a = new A();
C.prototype = A.prototype;
a instanceof A
// true
 
a instanceof C
// true

我们甚至也可以让一个父类不是实例的父类

function A() {}
var a = new A();
A.prototype = {}; // 对原型重新赋值
a instanceof A
 
false

总之就是很神奇

ES6 中对类的改进

在 ES6 中新增了 class 关键词,使得声明一个类更加简单,但只是写法上有改变,运行机制还是一样。

class A {
    constructor(value) {
        this.value = value;
    }
    log() {
        console.log(this.value);
    }
}
var a = new A(1234);
a.log();
 
// 1234
查看原文

赞 24 收藏 31 评论 0

时允 评论了文章 · 2018-07-14

​前端教学讲义:JS基础

讲义内容:JS 诞生的背景、基本类型、运算符

以下内容只涉及 ES5 标准,ES6 增加的新内容可以在网上查找到。

JS 诞生的背景

上世纪 90 年代网景公司开发的浏览器独步天下

一个叫做 Brendan Eich 的工程师受命于开发一款脚本语言,来增强浏览器的功能。

这名工程师花费了 10 天时间设计出了第一个版本,名叫 LiveScript。

后来因为当时 Java 正红,公司将其改名为 JavaScript,所以 JS 和 Java 其实没有什么关系。

这名工程师在设计语言之初有几个目标

  • 简单易用,不需要专业的计算机知识就能使用
  • 因为个人的喜爱,所以增加对函数式编程的支持
  • 因为公司压力,所以也要支持面向对象

后来 JS 因为市场认同,打败了 VBScript,Java plugin 等一系列增强浏览器的方案,成了事实标准。

网景公司倒闭之后,JS 的标准制定由 ECMA(欧洲电脑制造商协会)制定,所以又叫做 ECMAScript, 新的标准叫做 ES6、ES7、ES2018 … 每年会收集各种语言规范建议,通过 草案、stage1、stage2 等几个阶段,并在浏览器中有实现后会成为正式标准。

所以 JS 具有以下特点:

  • 向下兼容,包括兼容 bug(typeof null,大数)
  • 奇怪、蹩脚的规范,从技术上来说缺点很多
  • 运用场景广泛
  • 迭代速度快,

会在项目里使用还在草案阶段的新语法

变种、超集、子集 很多(coffee script, typescript, Action Script, flow, AssembleScript)

基本类型

JS 中的基本类型有以下几种:

  • undefined
  • null
  • Boolean
  • Number
  • String

另外有三种引用类型:

  • Function
  • Array
  • Object

和 Java 一样,所有类型都是基于 Object(除了 undefined ?)

通过 「var」关键字声明一个变量,方法和大多数语言一样。

JS 是动态语言,变量不用区分类型,不同类型之间的变量可以相互赋值。

var foo = 1;
var bar;
var a, b, c;
bar = foo = “abcd”
undefined 和 null

当一个值没有初始值的时候,默认值为 undefined。

undefined 在 JS 中是个语言设计的缺陷例子。

undefined 的行为在大多数情况下和 null 一样,都是不可访问的值,但是在 typeof 的时候结果不同

var und;
var nl = null;
typeof und === ’undefined’
typeof nl == ‘object’

数字

Boolean, Number,String 的行为和大多数语言一样。

可以直接声明也可以通过构造函数、工厂模式声明,三者没什么区别。

var a = 123
var a = new Number(123)
var a = Number(123)

在 JS 中 数字的取值范围是 -253 - 1 到 253 - 1,范围小于 long 型,并且计算时有精度问题

1.2 / 0.2
// 5.999999999999999

声明数字的方式和其它语言类似,科学计数法、八进制、十六进制

var a = 1e3
// 1000
var a = 0o12
// 10
var a = 0xa
// 10

通过 「toString」方法可以转换成字符串,并设置进制

var a = 1000
a.toString(10)
// “1000”
a.toString(8)
// “1750”
a.toString(16)
// “3e8”

在 JS 中,有两个特殊的数字,NaN 和 Infinity。

当字符串或者其他类型转换成数字失败的时候,就会返回 NaN,但是 NaN 的类型依然是 number。

NaN 和任何数字计算后,结果都是 NaN。

当数字除以 0 的时候,就会返回 Infinity。

Number(undefined)
// NaN
Number(null) // 我也很费解,为什么 null 转换成数字就是 0
// 0 
Number("asdf")
// NaN
10 / 0
//Infinity

字符串

在 JS 中字符串是可变长,而且可以通过 「+」 操作符做拼接。

字符串在内容中应该是双字节储存,可以通过索引访问每个字符,而不是每个 byte。

在浏览器中编码格式根据网页的编码格式而设置(在 node 中默认是 UTF8?)

在 JS 中,声明一个字符串通过单引号或者双引号都可以,二者没有区别。通常习惯是通过单引号访问,因为可以少按一个 shift。

var a = ‘123’
var b = “123”
a == b
a[0]  // 通过索引访问的结果仍然是个字符串
// “1”

函数

函数在 JS 中是一等公民,可以赋值给变量,有两种声明方式

function plus(a, b) { return a + b}
var plus = function(a, b) { return a + b }

第二种方法其实是将一个「匿名函数」复制给了变量 plus。

这两种方式在使用上没太大区别,除了在「声明前置」的场景下会有点区别

匿名函数还有一种方法叫做「立即执行函数」。

(function(a, b){ return a + b })(10, 20)
// 30

函数只会将 return 声明的表达式的值返回出去,在函数没有声明 return 的时候,默认返回 undefined

function plus (a, b) { a + b }
plus(1, 2)
// undefined

函数和作用域
函数和变量一样,可以在任何地方声明、使用,

每个函数独立出了一个作用域,作用域可以互相嵌套。

函数作为一个变量,本身也在父作用域下

function foo(b) {
    var a = 10
    // 这里属于 foo 的作用域,内部有 bar, b, a 三个变量
    function bar() {
        var c = 20;
        return a + b + c // 这里属于 bar 的作用域,因为在 foo 之内,所以可以访问 a 和 b
    }
    // 这里不能访问 c
    return bar();
}

由于 JS 在设计的时候,怕用户不理解变量需要先声明再使用,所以对变量会有「声明前置」,帮助大家提前声明所需要的变量。

下面我们用立即执行函数创建一个闭包来解释什么叫「声明前置」

(function() {
    console.log(fn, foo);
    function fn(a, b){ return a + b };
    var foo = 1234;
    console.log(fn, foo);
})()
// function (a, b){ return a + b } undefined
// function (a, b){ return a + b } 1234

上面的代码相当于:

(function() {
    function fn(a, b){ return a + b };
    var foo;
    console.log(fn, foo);•
    foo = 1234;
    console.log(fn, foo);
})()

如果我们用匿名函数赋值给一个变量,那么会有下面效果:

(function() {
    console.log(fn, foo);
    var fn = function(a, b){ return a + b };
    var foo = 1234;
    console.log(fn, foo);
})()

// undefined undefined
// function (a, b){ return a + b } 1234

上面的代码相当于:

(function() {
    var fn, foo;
    console.log(fn, foo);
    fn = function(a, b) { return a + b};
    foo = 1234;
    console.log(fn, foo);
})()

数组

在 JS 中数组是可变的,而且数组内可以放任何值。

数组的长度是最后一个元素的索引加一。

如果访问没有设置的元素,将会返回 unfined

var array = [];
array[0] = 1;
array[2] = “1234”;
array[4] = function(){};
// [1, undefined, “1234”, function(){}]
array[1]
// undefined

js 数组中自带了很多方法,方便我们对数组做操作,比如说 map, reduce 等等

[1, 2, 3].map(function(d) { return d + 1 })
// [2, 3, 4]
[1, 2, 3].reduce(function(r, d) { return r + d }, 0)
// 6

如果想要删除数组中的某个元素的话,我们可以用 delete 关键词,或者直接将索引的位置设置成 undefined

var array = [1, 2, 3]
delete array[1]
// [1, undefined, 3]

如果我们需要删除数组中某个元素,并且缩短数组长度的话,就需要用到 splice。

var array = [1, 2, 3]
array.splice(1, 1)
console.log(array)
// [1, 3]

一些常用的数组方法还有 slice, forEach, find, every, includes 等等

对象

和 java 一样,对象(Object) 是所有类型的基类。它类似 JSON 里的键值对结构体,可以动态的挂载任何属性。

声明的方法有两种,{} 和 new

var a = {}
var b = new Object()
a.num = 1
a.str = "1234"
console.log(a)
// { num: 1, str: "1234"}

对象(Object)有点类似 Java 里的 HashMap,但 Key 只能是字符串。可以通过类似数组索引的方式来访问对象上的键。

这种方法和点操作符是一样的效果,当键名由一些特殊的字符组成的时候,我们可以通过索引方式访问。

var obj = {value: 1234}
console.log(obj["value"])
// 1234

这样声明或者访问会报错:

var obj = { some-value: 1234 }
obj.some-value

我们需要让编译器知道 some-value 是一个键名,所以需要将它们用引号括起来

var obj = { "some-value": 1234 }
obj["some-value"]

当一个对象中挂载了一个函数,在函数中可以通过 this 来访问对象内的其他属性,this 也可以理解为「执行上下文」

function fn () { console.log(this.value) }
var obj = { fn: fn, value: 1234 }
obj.fn() // 此时 fn 的执行上下文就是 obj
// 1234

var otherObj = { fn: fn, value: "abcd"}
otherObj.fn() // 此时 fn 的执行上下文就是 otherObj
// "abcd"

由于函数和数组是所有类型的基类,所以可以像对象一样随意的扩展属性。

但是字面量(如数字、布尔值、字符串、null、undefined) 是不能随意扩展的。

function plus(a, b) { return a + b}
plus.value = 1234
plus(12, plus.value)
// 1246
 
var array = []
array.value = 1234
console.log(array.value)
// 1234
 
var a = true;
a.value = 1234
console.log(a.value)
// undefined

运算符

JS 的运算符和其他语言基本类似,就+-*%/这些,外加一些二进制操作符。

但因为 JS 弱类型的特性,有些场景是其他语言没有的,比如一个数字加一个字符串,结果是什么?

1+"2"
// "12"

这种场景还可以理解,数字和字符串相加的时候,数字通过调用 toString 被转换成了字符串。

1 + {}
// "1[object Object]"

数字和对象相加的时候,对象和数字都调用了 toString,被转换成了字符串

{} + []
// 0

但是空对象和空数组相加结果却是 0,我也不知道为什么

var obj = {}
if (obj) console.log("obj is true")
// "obj is true"

obj == true
// false

上面这个例子中,obj 是个空对象,在 if 条件判断中被当作真值,但是和 true 对比的时候却返回 false。

JS 的隐式转换规则很诡异,很多都是为了兼容早期版本中错误的实现,这里没必要细究转换规则是什么。

我们只需要记住以后用严格相等符(===)作对比,避免不同类型的变量相互运算。

"1" == 1
// true

"1" === 1
// false

循环

JS 的循环和其它类似,也都是 for, do..while 这种写法。

但是,我们一般不用 for 作循环,而是用数组方法 forEach,map(因为 for 写起来很麻烦,而且还需要单独声明一个变量)

[1, 2, 3].forEach(function(value) {
  return value + 1;
})
// [2, 3, 4]

另外还可以通过 for...in 来遍历一个对象

var obj = {a:1, b:2, c: 3}
for (var key in obj) {
  console.log(key)
}
// "a"
// "b"
// "c"

但我们一般也不这么用,而是用 Object.keys 取得包含对象所有键的数组

var obj = {a:1, b:2, c: 3}
Object.keys(obj)
// ["a", "b", "c"]
 
Object.keys(obj).forEach(console.log)
// "a"
// "b"
// "c"

课后习题

第一题:JS 的运用领域都有哪些,作为弱类型的语言执行效率如何,为什么?

第二题:typeof 关键字的返回值有几种结果?

第三题:下面两种写法有什么区别

{
  0: "a",
  1: "b",
  2: "c"
}

["a","b","c"]

第四题:下面代码的运行时会不会报错,this 指向哪里?

function fn () { console.log(this.value) }
fn()

第五题:声明的变量是如何被回收的

查看原文

时允 发布了文章 · 2018-07-13

​前端教学讲义:JS基础

讲义内容:JS 诞生的背景、基本类型、运算符

以下内容只涉及 ES5 标准,ES6 增加的新内容可以在网上查找到。

JS 诞生的背景

上世纪 90 年代网景公司开发的浏览器独步天下

一个叫做 Brendan Eich 的工程师受命于开发一款脚本语言,来增强浏览器的功能。

这名工程师花费了 10 天时间设计出了第一个版本,名叫 LiveScript。

后来因为当时 Java 正红,公司将其改名为 JavaScript,所以 JS 和 Java 其实没有什么关系。

这名工程师在设计语言之初有几个目标

  • 简单易用,不需要专业的计算机知识就能使用
  • 因为个人的喜爱,所以增加对函数式编程的支持
  • 因为公司压力,所以也要支持面向对象

后来 JS 因为市场认同,打败了 VBScript,Java plugin 等一系列增强浏览器的方案,成了事实标准。

网景公司倒闭之后,JS 的标准制定由 ECMA(欧洲电脑制造商协会)制定,所以又叫做 ECMAScript, 新的标准叫做 ES6、ES7、ES2018 … 每年会收集各种语言规范建议,通过 草案、stage1、stage2 等几个阶段,并在浏览器中有实现后会成为正式标准。

所以 JS 具有以下特点:

  • 向下兼容,包括兼容 bug(typeof null,大数)
  • 奇怪、蹩脚的规范,从技术上来说缺点很多
  • 运用场景广泛
  • 迭代速度快,

会在项目里使用还在草案阶段的新语法

变种、超集、子集 很多(coffee script, typescript, Action Script, flow, AssembleScript)

基本类型

JS 中的基本类型有以下几种:

  • undefined
  • null
  • Boolean
  • Number
  • String

另外有三种引用类型:

  • Function
  • Array
  • Object

和 Java 一样,所有类型都是基于 Object(除了 undefined ?)

通过 「var」关键字声明一个变量,方法和大多数语言一样。

JS 是动态语言,变量不用区分类型,不同类型之间的变量可以相互赋值。

var foo = 1;
var bar;
var a, b, c;
bar = foo = “abcd”
undefined 和 null

当一个值没有初始值的时候,默认值为 undefined。

undefined 在 JS 中是个语言设计的缺陷例子。

undefined 的行为在大多数情况下和 null 一样,都是不可访问的值,但是在 typeof 的时候结果不同

var und;
var nl = null;
typeof und === ’undefined’
typeof nl == ‘object’

数字

Boolean, Number,String 的行为和大多数语言一样。

可以直接声明也可以通过构造函数、工厂模式声明,三者没什么区别。

var a = 123
var a = new Number(123)
var a = Number(123)

在 JS 中 数字的取值范围是 -253 - 1 到 253 - 1,范围小于 long 型,并且计算时有精度问题

1.2 / 0.2
// 5.999999999999999

声明数字的方式和其它语言类似,科学计数法、八进制、十六进制

var a = 1e3
// 1000
var a = 0o12
// 10
var a = 0xa
// 10

通过 「toString」方法可以转换成字符串,并设置进制

var a = 1000
a.toString(10)
// “1000”
a.toString(8)
// “1750”
a.toString(16)
// “3e8”

在 JS 中,有两个特殊的数字,NaN 和 Infinity。

当字符串或者其他类型转换成数字失败的时候,就会返回 NaN,但是 NaN 的类型依然是 number。

NaN 和任何数字计算后,结果都是 NaN。

当数字除以 0 的时候,就会返回 Infinity。

Number(undefined)
// NaN
Number(null) // 我也很费解,为什么 null 转换成数字就是 0
// 0 
Number("asdf")
// NaN
10 / 0
//Infinity

字符串

在 JS 中字符串是可变长,而且可以通过 「+」 操作符做拼接。

字符串在内容中应该是双字节储存,可以通过索引访问每个字符,而不是每个 byte。

在浏览器中编码格式根据网页的编码格式而设置(在 node 中默认是 UTF8?)

在 JS 中,声明一个字符串通过单引号或者双引号都可以,二者没有区别。通常习惯是通过单引号访问,因为可以少按一个 shift。

var a = ‘123’
var b = “123”
a == b
a[0]  // 通过索引访问的结果仍然是个字符串
// “1”

函数

函数在 JS 中是一等公民,可以赋值给变量,有两种声明方式

function plus(a, b) { return a + b}
var plus = function(a, b) { return a + b }

第二种方法其实是将一个「匿名函数」复制给了变量 plus。

这两种方式在使用上没太大区别,除了在「声明前置」的场景下会有点区别

匿名函数还有一种方法叫做「立即执行函数」。

(function(a, b){ return a + b })(10, 20)
// 30

函数只会将 return 声明的表达式的值返回出去,在函数没有声明 return 的时候,默认返回 undefined

function plus (a, b) { a + b }
plus(1, 2)
// undefined

函数和作用域
函数和变量一样,可以在任何地方声明、使用,

每个函数独立出了一个作用域,作用域可以互相嵌套。

函数作为一个变量,本身也在父作用域下

function foo(b) {
    var a = 10
    // 这里属于 foo 的作用域,内部有 bar, b, a 三个变量
    function bar() {
        var c = 20;
        return a + b + c // 这里属于 bar 的作用域,因为在 foo 之内,所以可以访问 a 和 b
    }
    // 这里不能访问 c
    return bar();
}

由于 JS 在设计的时候,怕用户不理解变量需要先声明再使用,所以对变量会有「声明前置」,帮助大家提前声明所需要的变量。

下面我们用立即执行函数创建一个闭包来解释什么叫「声明前置」

(function() {
    console.log(fn, foo);
    function fn(a, b){ return a + b };
    var foo = 1234;
    console.log(fn, foo);
})()
// function (a, b){ return a + b } undefined
// function (a, b){ return a + b } 1234

上面的代码相当于:

(function() {
    function fn(a, b){ return a + b };
    var foo;
    console.log(fn, foo);•
    foo = 1234;
    console.log(fn, foo);
})()

如果我们用匿名函数赋值给一个变量,那么会有下面效果:

(function() {
    console.log(fn, foo);
    var fn = function(a, b){ return a + b };
    var foo = 1234;
    console.log(fn, foo);
})()

// undefined undefined
// function (a, b){ return a + b } 1234

上面的代码相当于:

(function() {
    var fn, foo;
    console.log(fn, foo);
    fn = function(a, b) { return a + b};
    foo = 1234;
    console.log(fn, foo);
})()

数组

在 JS 中数组是可变的,而且数组内可以放任何值。

数组的长度是最后一个元素的索引加一。

如果访问没有设置的元素,将会返回 unfined

var array = [];
array[0] = 1;
array[2] = “1234”;
array[4] = function(){};
// [1, undefined, “1234”, function(){}]
array[1]
// undefined

js 数组中自带了很多方法,方便我们对数组做操作,比如说 map, reduce 等等

[1, 2, 3].map(function(d) { return d + 1 })
// [2, 3, 4]
[1, 2, 3].reduce(function(r, d) { return r + d }, 0)
// 6

如果想要删除数组中的某个元素的话,我们可以用 delete 关键词,或者直接将索引的位置设置成 undefined

var array = [1, 2, 3]
delete array[1]
// [1, undefined, 3]

如果我们需要删除数组中某个元素,并且缩短数组长度的话,就需要用到 splice。

var array = [1, 2, 3]
array.splice(1, 1)
console.log(array)
// [1, 3]

一些常用的数组方法还有 slice, forEach, find, every, includes 等等

对象

和 java 一样,对象(Object) 是所有类型的基类。它类似 JSON 里的键值对结构体,可以动态的挂载任何属性。

声明的方法有两种,{} 和 new

var a = {}
var b = new Object()
a.num = 1
a.str = "1234"
console.log(a)
// { num: 1, str: "1234"}

对象(Object)有点类似 Java 里的 HashMap,但 Key 只能是字符串。可以通过类似数组索引的方式来访问对象上的键。

这种方法和点操作符是一样的效果,当键名由一些特殊的字符组成的时候,我们可以通过索引方式访问。

var obj = {value: 1234}
console.log(obj["value"])
// 1234

这样声明或者访问会报错:

var obj = { some-value: 1234 }
obj.some-value

我们需要让编译器知道 some-value 是一个键名,所以需要将它们用引号括起来

var obj = { "some-value": 1234 }
obj["some-value"]

当一个对象中挂载了一个函数,在函数中可以通过 this 来访问对象内的其他属性,this 也可以理解为「执行上下文」

function fn () { console.log(this.value) }
var obj = { fn: fn, value: 1234 }
obj.fn() // 此时 fn 的执行上下文就是 obj
// 1234

var otherObj = { fn: fn, value: "abcd"}
otherObj.fn() // 此时 fn 的执行上下文就是 otherObj
// "abcd"

由于函数和数组是所有类型的基类,所以可以像对象一样随意的扩展属性。

但是字面量(如数字、布尔值、字符串、null、undefined) 是不能随意扩展的。

function plus(a, b) { return a + b}
plus.value = 1234
plus(12, plus.value)
// 1246
 
var array = []
array.value = 1234
console.log(array.value)
// 1234
 
var a = true;
a.value = 1234
console.log(a.value)
// undefined

运算符

JS 的运算符和其他语言基本类似,就+-*%/这些,外加一些二进制操作符。

但因为 JS 弱类型的特性,有些场景是其他语言没有的,比如一个数字加一个字符串,结果是什么?

1+"2"
// "12"

这种场景还可以理解,数字和字符串相加的时候,数字通过调用 toString 被转换成了字符串。

1 + {}
// "1[object Object]"

数字和对象相加的时候,对象和数字都调用了 toString,被转换成了字符串

{} + []
// 0

但是空对象和空数组相加结果却是 0,我也不知道为什么

var obj = {}
if (obj) console.log("obj is true")
// "obj is true"

obj == true
// false

上面这个例子中,obj 是个空对象,在 if 条件判断中被当作真值,但是和 true 对比的时候却返回 false。

JS 的隐式转换规则很诡异,很多都是为了兼容早期版本中错误的实现,这里没必要细究转换规则是什么。

我们只需要记住以后用严格相等符(===)作对比,避免不同类型的变量相互运算。

"1" == 1
// true

"1" === 1
// false

循环

JS 的循环和其它类似,也都是 for, do..while 这种写法。

但是,我们一般不用 for 作循环,而是用数组方法 forEach,map(因为 for 写起来很麻烦,而且还需要单独声明一个变量)

[1, 2, 3].forEach(function(value) {
  return value + 1;
})
// [2, 3, 4]

另外还可以通过 for...in 来遍历一个对象

var obj = {a:1, b:2, c: 3}
for (var key in obj) {
  console.log(key)
}
// "a"
// "b"
// "c"

但我们一般也不这么用,而是用 Object.keys 取得包含对象所有键的数组

var obj = {a:1, b:2, c: 3}
Object.keys(obj)
// ["a", "b", "c"]
 
Object.keys(obj).forEach(console.log)
// "a"
// "b"
// "c"

课后习题

第一题:JS 的运用领域都有哪些,作为弱类型的语言执行效率如何,为什么?

第二题:typeof 关键字的返回值有几种结果?

第三题:下面两种写法有什么区别

{
  0: "a",
  1: "b",
  2: "c"
}

["a","b","c"]

第四题:下面代码的运行时会不会报错,this 指向哪里?

function fn () { console.log(this.value) }
fn()

第五题:声明的变量是如何被回收的

查看原文

赞 23 收藏 26 评论 4

时允 发布了文章 · 2018-07-10

前端需要什么样的方案设计

背景

对于我们日常工作来说,写代码可能占去了大部分时间,但当我们回头看以前代码的时候,我们往往会觉得以前自己写的很糟糕。

或者在向前看看资历比较老的同事,往往都可以写出比较清晰的代码,有一种莫名的优雅。
也许通过时间的历练,在几年之后自己也可以达到对方的水平,但是这个时间太久。
可不可以总结出一些可以复制的方法、套路,让新人也能尽早写出一套漂亮的代码?

本篇文章就是针对前端的研发情况,讨论这些方法、套路的。

现状

我们往往都会陷入这样一种循环,在拿到需求文档时候,我们会大致看一下,感觉开发上没有什么太大的问题,然后就看看交互、UI 稿上有哪些页面,接下来就开始着手写了。

在写代码的过程中,我们也许会发现有些实现不合理的地方,这时我们会稍微动一下我们的大脑,然后抽象一下流程和模块,重构一下部分代码。遇到简单点的情况,我们发现加个参数传过去做一下判断也能实现,于是我们也就这么做了。

开发完后,我们会觉得比较满足,代码在脑中的逻辑比较清晰,里面也许有些地方写的不太妥当,但是也无妨吧。

然后项目就开始测试了,QA 会针对各种边界问题给我们提 bug,在改 bug 的过程中我们痛苦不堪,之前的代码逻辑变得支离破碎,我们不得不为了各种 bug 去为代码修修补补,方法的参数加了一个又一个,逻辑里的条件判断加了又加。往往到这个时候,我们开始对 QA 提出的 bug 表示质疑,我们会用“现在的表现是符合逻辑的”,“这种情况没问题”等等这样的说辞来避免对代码的更改。

过去几个月之后,我们接到一个需求,要对这部分代码增加几个新功能,翻开代码的时候,整个人都崩溃了。以前代码的逻辑自己早就忘记了,面对自己已经看不懂的代码,我们会问“这个方法什么意思?为什么逻辑这么复杂?”,“代码在不同页面之间跳来跳去,他们的逻辑到底是什么样的?”,“为什么会有这么多标识位,他们都是干嘛的?”。注释什么的是不存在的,即使存在,也不明白在讲些什么。

在经历过阅读源码的痛苦过程之后,我们看了看排期,嗯。。。还有 30% 的 buffer,于是我们决定把这部分代码重写掉,然后再重复这个痛苦的循坏。

那么我们应该如何从这个循环里跳出来呢?

对比

我们相比一下后端,会发现他们在写代码之前都会做方案设计。方案设计是软件工程里的一个最佳实践,通常做技术方案的过程中会体现出软件整体的架构,当对核心流程梳理完成之后,最后基本都能落实到代码上。也就是说好的技术方案能体现出最后代码的逻辑,通过看方案就能知道代码怎么写。这样就防止了在写代码过程中边写边改,最后导致代码结构混乱的问题。

但是我们尝试按照后端的方法来做方案设计发现根本写不出来什么内容:

  • 后端的流程图是系统间交互的流程,而我们大都数场景下只需要和一个后端交互
  • 后端需要列举出来数据库表结构,而我们都是把数据丢给后端
  • 后端有业务模型,而对我们来说,从后端拿数据就行了
  • 后端有核心业务流程,而我们调用他们 API 就行了

从上面我们可以看出,前端只运行在浏览器里,业务上只和后端有交互,而且 API 都是后端定好的,所以按照后端方案设计的套路,前端是写不出来什么东西的。

所以我们会发现,我们大多数设计文档都是偏 Node 层的东西,因为我们往往都是按照后端设计方案的模式去做的,但是浏览器内部运行的代码却没有文档去描述。

分析

这时候我们要重新审视方案设计的套路,来发现前后端的不同:

  • 业务分析
  • 业务模型
  • 核心流程
  • 逻辑架构、类图、部署图
  • 接口
  • 实施方案
  • 风险控制

业务分析与业务模型可能前后端都是一致的,毕竟我们是解决同一个业务问题。但其中也有稍许差别,前端有些数据不是从后端获取的,或者说不一定非要从后端获取,这点我们需要在做设计的时候考虑进去,比如同样是获取地理位置,我们可以通过 GPS 也可以通过访问后端服务通过 IP 地址来判断,google 甚至有通过 WIFI 唯一识别码来定位的。

前后端对于「核心流程」的定义也不同,对于后端来说核心流程是数据的产生、流转、消费,但是提到流程,在前端来说更多的是页面的流转、组件的交互。同样一件事情,在前后端来看完全是两个东西,比如保存一项数据,后端需要关注的可能是如何校验,如何存储,如何索引,如何关联。但前端要关注的却是校验结果的展现形式如何,生成一个结构化数据需要调用多少页面,这些页面在不同的屏幕下面如何展现,是跳转还是覆盖或者弹窗。

后端其实更注重逻辑架构与部署图,因为后端需要为服务化,服务间边界的定义要非常清晰、具体,对于一个服务内的代码后端有 Spring 等明确的框架去规范如何写。前端与微服务对应的,应该就是组件 了,但是组件覆盖的范围太广,从一个按钮到一个页面都可以称之为组件,所以前端的组件需要被划分成模块、控件等不同封装水平的单位。在这个划分的过程中,逻辑架构和类图就自然体现出来了。同时前后端解决的问题不同,导致关注点也不同,前端需要关注页面的还原,比如页面的元素应该如何抽象,样式应该如何复用,这个是后端不用考虑的。

后端需要关注暴露出去的 thrift 或者 http 接口,因为这体现了系统间交互的逻辑。而对于前端来说对应的也应该明确独立模块或者页面之间的交互逻辑,所以也就需要明确这些接口。

关于实施方案和风险控制,各方的关注点也有稍许不同,后端更关心系统之间的集成,旧数据的兼容。而前端应该关心的是和移动 Native 的集成,或者微信、支付宝的集成,如果是桌面 web 的话就该考虑 iframe 集成的场景。

结论

作为软件工程的最佳实现,方案设计在前端开发过程中还是十分必要的,那么为什么前端领域长时间不注重这个事情呢,我觉得有以下原因:

  1. 方案设计依赖技术能力,而前端技术栈变化太快,今天的设计套路放在明天可能就失效了
  2. 前端业务变化太快,经过半年的迭代之后,可能第一版的方案就反应不出现有代码逻辑了
  3. 前端的业务流程、交互流程比后端复杂太多,而且可复用性差,需要花费大量时间去思考和整理,而且对抽象能力有比较高的要求
  4. 前端开发效率高,不会有历史包袱,有时候直接重写的效率反而更高

但是这些原因其实都不是我们不做方案设计的理由,方案设计是个结构化思维的过程,他不光是能让项目更好执行,也能提升开发者本身的架构能力和宏观意识。

所以同学们在平时开发的时候多想一想如何做设计吧。

查看原文

赞 22 收藏 14 评论 4

时允 发布了文章 · 2017-09-18

面试宝典之沟通能力

清晰

  • 回答问题的时候没有吞吞吐吐。
  • 眼神专注,不会到处乱「漂」。
  • 语调平稳,不急不慢。

简练

很多候选人喜欢回答问题的时候进行扩展,从天南讲到地北。
这个时候要及时打断候选人,并且表示「后续的问题尽量在 5 句话之内回答完」。

有逻辑

  • 用词准确且前后一致。
  • 能围绕观点展开谈话。(反例:在谈论一个问题的时候突然绕到了其它方面)
  • 对于问题胸有成竹,表达的时候能从前到后,从因到果来描述整件事情。(反例:没有讲背景先讲技术细节)
  • 对于推断有严谨的逻辑性。(反例:因为项目总是出 Bug, 所以要加人)

其它

  • 能意识到沟通不是「一次性」行为,而是「持续性」行为
  • 沟通时能运用肢体语言和表情
  • 积极聆听,适时的提出意译、反映感受、总结
  • 学会发现别人的优点
查看原文

赞 2 收藏 3 评论 0

时允 发布了文章 · 2017-09-18

面试宝典之学习能力

学习方式

以下学习方法从上到下按有效程度排序。

  • 看:看视频
  • 听:听老师讲
  • 读:读书
  • 写:写笔记、总结
  • 教:自己学会后教别人

所以一般人看书或者看博客都只是第三层「读」的阶段,并不能很好的体现出学习能力。

驱动因素

以下驱动因素从上到下按有效程度排序。

  • 业务驱动:因为项目需要才去了解新知识
  • 环境驱动:身边都是大牛,自己深受影响
  • 自我驱动:能从学习中得到成就感,学习的范围不限于本专业或者技术

学习结果

根据候选人学习的内容,提出相应的问题让候选人回答。
如果候选人对于学习范围内所涉及到的问题都能一一回答,并有自己的理解,那么说明学习能力确实不错,否则视为学习能力一般。

查看原文

赞 0 收藏 2 评论 0

时允 回答了问题 · 2017-09-01

解决使用vux框架,发现this.$vux获取不到,导致Loading 组件报错怎么解决?

是不是 Vuex 初始化时间太晚了

关注 7 回答 5

时允 回答了问题 · 2017-09-01

解决js obj对象如何清空

感觉提主要的是这个答案:

var c = Object.assign({}, a);

关注 8 回答 7

时允 发布了文章 · 2017-09-01

前端面试提纲

前端基础

  • JS 原型链机制的理解
  • 设计模式:了解基本的前端设计模式,单例、适配器、工厂、观察者。
  • 跨域的方式、同源策略、为什么有同源策略、如何做安全防范:新的H5的跨域方式(cors、postmessage)。
  • 安全,对攻击方式、安全的防范上的了解 。
  • http、TCP 协议的知识,如:什么是无状态,http 状态码的分类。
  • 知晓 CSS 布局原理,什么是BFC,如何实现垂直居中,绝对定位相对位置。
  • 如何做自适应布局,怎么计算 REM
  • websocket, WebRTC, EventSource 的区别

框架、组件化

  • 架构分层
  • 模块解耦:理解接口、事件通讯的两种方式。
  • 组件化趋势: shadow dom,react和vue 。
  • Virtual DOM 的优势以及缺陷
  • 实践中如何解耦 UI 状态和领域状态
  • 目录结构如何规划

前端构建方案

  • 工程化的理解以及解决的问题如 gulp。
  • gulp与其他构建工具的对比。
  • 如何拆分 SPA 中的大型代码
  • 如何做异步加载
  • 有没有写过 webpack loader, 以及这个 loader 是为了解决什么问题

性能优化

  • webview的优化:对静态资源缓存到native的原理和流程 ,webview缓存、版本号管理、线下调试。
  • 如何加快首屏加载速度,Server Render 的实践。
  • 网页渲染性能优化,layout, paint, compose 三步骤的理解。
  • css 动画、SVG、canvas 的运用
  • 针对前端框架的性能优化,如 showComponentUpdate 的使用
  • 如何带领团队优化:制定量化指标,寻找性能瓶颈,集中优化。

质量保障

  • eslint、tslint 如何跟开发流程集成
  • 单元测试覆盖率
  • 如何面对需求变更带来的测试用例失效
  • 前端灰度方案
  • 如何排查内存泄漏

其他

  • 期望:偏技术或偏管理
  • 觉得一个前端专家应具备的技能和素质:综合、系统能力,需要理解系统和框架的原理,对前端前沿技术有所关注。
  • 前沿技术的了解
  • 对前端未来走向的判断
  • 对领域设计的理解
  • 视觉评定能力
查看原文

赞 37 收藏 132 评论 7

认证与成就

  • 获得 472 次点赞
  • 获得 57 枚徽章 获得 5 枚金徽章, 获得 24 枚银徽章, 获得 28 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-02-26
个人主页被 2.8k 人浏览