Little H title

this is subtitle


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益404

js函数

发表于 2018-12-14 | 分类于 javascript教程

js函数

  1. 只定义一次, 但可能被执行或调用任意次. js的函数是参数化的, 就是函数的定义会包括一个称为形参的标识符列表, 这些参数像局部参数一样工作. 函数调用时为形参提供实参的值. 函数用实参值来计算返回值, 成为函数调用表达式的值

  2. 每次调用除了拥有一个值外, 还有本地调用的上下文, 就是this关键字的值 this用法可以看只在运行时确定 javascript中this指向由函数调用方式决定

  3. 如果函数挂载在一个对象上, 作为对象的一个属性, 就成为他是对象的方法. 用于初始化一个新创建的对象的函数成为构造函数.

  4. js中, 函数就是对象.所以可以给他们设置属性, 甚至调用他们的方法.

  5. js函数还能嵌套在其他函数中定义, 这样他们就能访问他们被定义时所处的作用域中的任何变量. 构成闭包

函数定义

function关键字, 在函数定义表达式和函数声明语句中用.

3部分组成

  1. 函数名称标识符: 这个是函数声明语句必需的. 用途就像变量的名字, 新定义的函数对象会赋值给这个变量name. 对函数表达式来说这个名字是可选的, 如果存在, 该名字只存在于函数体中并指代该函数对象本身, 和函数声明有点区别. 一个不在函数体中, 一个在函数体{}中.
  2. 一对圆括号(): 用包含0个或多个用逗号隔开的标识符组成的列表. 都是函数参数名称, 就像函数体中的局部变量一样(就当成局部变量好了).
  3. 一对花括号{}: 包含0到多条js语句, 这些语句构成函数体.

一条函数声明语句实际上声明了一个变量, 并把一个函数对象赋值给它.函数定义表达式并没有声明一个变量.

所以函数声明中的函数名是一个指代自己的名称. 函数表达式中如果包含名称, 函数的局部作用域将会包含一个绑定到函数对象的名称.实际上函数的名称将会成为函数内部的一个局部变量. 通常而言, 函数表达式是不需要名称的. 特别适合来定义那些只会用到一次的函数.

还有就是提升问题, 声明会, 表达式不会.

函数大多有一条return语句, 用来导致函数停止执行,并返回表达式的值. 如果只有return就返回undefined, 如果函数不包含return就只是执行完函数后返回undefined, 有些需要返回值, 有些不需要. 不需要个也叫过程.

函数声明语句并不是真正的语句, ECMA只是允许它作为顶级语句, 不能出现在循环, 条件判断, 或try/catch/finally和with中. 函数表达式可以出现在任何地方.

8.2 函数调用

作为函数主体的js代码在定义之时并不会执行, 只有调用该函数时他们才会执行. 有4种方式来调用js函数.

  • 作为函数
  • 作为方法(保存在对象那个属性里的js函数)
  • 作为构造函数
  • 通过他们的call()和apply()方法简介调用

8.2.1 函数调用

使用调用表达式(由多个函数表达式组成)可以进行普通的函数调用也可以进行方法调用.

函数表达式还可以是一个属性访问表达式[](是对象的一个属性o.m()或数组的一个元素)

1
var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5)

注意this, 以函数形式调用的函数通常不使用this关键字. 不过this可以用来判断当前是否是严格模式

1
var strict = (function() { return !this }())

8.2.2 方法调用

一个方法就是保存在一个对象属性里的js函数. 比如一个对象o和一个函数f

1
2
3
4
5
// 这里说的就是给对象o定义一个名为m()的方法
o.m = f

//调用的时候就
o.m()

对方法调用的参数和返回值的处理和普通函数调用完全一致. 只有一个区别而已, 就是调用上下文 this的区别

方法和this关键字是面向对象编程的核心. 任何函数只要作为方法调用实际上都会传入一个隐式的实参-实参是个对象(就是那个o), 方法调用的母体就是这个对象. 函数基于一个对象进行操作.

例如:

1
rect.setSize(width, height)

第一行的方法调用语法非常清晰地表明这个函数执行的载体就是rect对象, 函数中的所有操作都讲基于这个对象.

方法链: 当方法的返回值是一个对象, 这个对象还可以再调用它的方法. 当方法不需要返回值时, 最好直接返回this.
不要将方法的链式调用和构造函数的链式调用混为一谈

在说下this: 是一个关键字, 不是变量,也不是属性名. 不允许赋值诶.

所以this和变量不同, this没有作用域的限制, 嵌套的函数不会从调用它的函数中继承this:(很多人误以为调用嵌套函数时this会指向调用外层函数的上下文)

  1. 如果嵌套函数作为方法调用, 其this的值指向调用它的对象.
  2. 如果嵌套函数作为函数调用, 其this的值不是全局对象(非严格)就是undefined(严格)

如果你想访问外部函数的this值, 需要将this保存在一个变量中, 这个变量和内部函数都在同一个作用域内.

javascript中this指向由函数调用方式决定

8.2.3 构造函数调用

如果函数或方法调用之前带有关键字new, 它就构成构造函数调用.

构造函数调用和普通的函数调用以及方法调用在实参处理, 调用上下文和返回值方面都有不同

如果构造函数调用圆括号内包含一组实参列表, 先计算这些实参表达式, 然后传入函数内, 这和函数调用和方法调用时一致的.
但如果构造函数没有形参, js构造函数的语法允许省略实参列表和圆括号, 就留一个名字.

1
2
3
// 两者等价
var o = new Object()
var o = new Object

构造函数调用创建一个新的空对象, 这个对象继承自构造函数的prototype属性, 构造函数试图初始化这个新创建的对象, 并将这个对象用作其调用上下文, 因此构造函数可以使用this关键字来引用这个新创建的对象.

注意, 尽管构造函数看起来像一个方法调用, 他依然会使用这个新对象作为调用上下文, 也就是说,在表达式new o.m()中,调用上下文并不是o, 而是新建好的那个对象.

构造函数通常不使用return关键字, 他们通常初始化新对象, 当构造函数的函数体执行完毕事, 它会显示返回.
当然使用return返回这个对象,就是这个对象咯.(返回别的对象就是别的对象)
如果只给了return但没有指定返回值, 或者返回一个原始值, 那么这时将忽略返回值, 同时使用这个新对象作为调用结果.

8.2.4 间接调用

js的函数也是对象, 函数对象也有方法啊. 其中两个方法就是call()和apply()可用来间接地调用函数.
两个方法都允许显示指定调用所需的this值, 也就是说,任何函数可以作为任何对象的方法来调用, 哪怕这个函数不是那个对象的方法.

8.3 函数的实参和形参

js函数定义没有指定函数形参的类型啊, 对传入的实参也没有做类型检查, 甚至数量也没有检查.

8.3.1 可选形参

当传入的实参比函数声明指定的形参个数少时, 剩下的形参将设为undefined. 所以可以给省略的参数赋一个合理的默认值.

当然可以用一个undefined或者null来作为一个占位符.a

8.3.2 可变长的实参列表: 实参对象

当传入的实参比函数声明指定的形参个数多时, 没办法直接获取未命名值的引用. 参数对象解决这个问题.
标识符arguments指向是实参对象的引用, 实参对象是一个类数组对象, 可以用下标,不必非要用名字来得到实参.

1
2
arguments[0]
arguments.length

一般js是省略的实参将都是undefined, 多余的实参自动省略.

实参对象有一个重要用处,就是让函数可以操作任意数量的实参. 类似这种可以接受任意个数的是实参, 这种函数也称为 不定实参函数

注意:不定实参函数的实参个数不能为0. arguments[]对象最适合的场景就是一类函数: 这类函数包括固定个数的命名和必须参数, 以及随后个数不定的可选实参.

类数组对象, arguments并不是真正的数组, 是一个实参对象. 每个实参对象都包含以数字为索引的一组元素以及length属性, 但他毕竟不是真正的数组. 可以理解为他是一个对象, 只是碰巧具有以数字为索引的属性.

数组对象包含一个非同寻常的特性.非严格模式下, 当一个函数包含若干形参, 实参对象的数组元素是函数形参对应实参的别名, 实参对象中以数字索引, 并且形参名称可以认为是相同变量的不同命名.

1
2
3
4
5
6
7
function f(x) {
console.log(x); // 输出实参初始值
arguments[0] = null; // 修改实参数组对象元素, 对应形参别名x也修改了
console.log(x); // 输出null
}

f(1)

如果实参对象是一个真正的数组的话, 那么修改arguments[0] = null是不会影响到console.log(x)的值的.
这里arguments[0]和x指代同一个值, 修改其中一个的值会影响到另一个.

es5中移除了这个特殊特性. 不过浏览器上还是能跑的
非严格模式下arguments是个标识符, 可以用, 但在严格模式下, arguments是一个保留字

callee和caller属性

arguments实参对象除了数组元素, 还定义了callee和caller属性. 严格模式下对这两个属性读写都会长生一个类型错误. 非严格模式下, ECMA宝追规范规定callee属性指代当前正在运行的函数, caller是非标准的, 指代当前正在执行的函数的函数.
callee属性在某些时候非常有用, 比如匿名函数中通过callee来递归地调用自身.

8.3.3 将对象属性用作实参

当一个函数包含超过3个形参时, 对于程序员来说, 记住调用函数中实参的正确顺序实在让人头疼. 最好用键值对的形式来传入参数, 这样参数的顺序就无关紧要了. 所以在定义函数的时候, 传入的实参都写入一个单独的对象中, 对象中的k/v就是真正需要的实参数据.

8.3.4 实参类型

js中形参并没有声明类型, 在形参传入函数体之前也没有做任何类型检查.

8.4 作为值的函数

函数可以定义, 也可以调用, 这是函数最重要的特性. js中函数不经是一种语法, 也是值, 也就是说可以将函数赋值给变量, 存储在对象的属性或数组的元素中, 作为参数传入另外一个函数等

看一个函数定义:

1
function square(x) { return x*x}

这个定义创建一个新的函数对象, 并将其赋值给变量square. 函数的名字实际上是看不见的,square仅仅是变量的名字, 这个变量指点函数对象. 函数还能赋值给其他的变量, 并且仍可以正常工作.

1
2
3
var s = square
square(4)
s(4)

除了可以将函数赋值给变量, 同样可以将函数赋值给对象的属性. 当函数作为对象属性调用时, 函数就成为方法.

1
2
var o = {square: function(x) {return x*x}}
var y = o.square(16)

函数甚至不需要带名字, 当把它们赋值给数组元素时:

1
2
var a = [function(x) { return x*x }, 20]
a[0](a[1]) // 合法的函数调用表达式

看一下函数用作值的, 就是要给给定操作符, 操作数的函数

1
2
3
4
5
function add(x, y) { return x + y}

function operate(operate, operand1, operand2) {
return operate(operand1, operand2)
}

又比如Array.sort()来对数组元素进行排序, 但排序的规则有很多(比如基于数值大小, 字母表排序, 日期大小, 从小到大, 从大到小等等), sort()可以接受一个函数作为参数, 用来处理具体的排序操作.

自定义函数属性

js中的函数并不是原始值, 而是一种特殊的对象. 是对象就可以有属性, 所以函数可以有属性. 当函数需要一个静态变量在调用时保持某个值不变, 最方便的就是给函数定义属性, 而不是定义全局变量, 显然定义全局量会让命名空间变得更加杂乱无章.

比如你想返回一个唯一整数的函数, 不管在哪调用都会返回这个整数.

1
2
3
4
5
6
7
8
9
// 初始化函数对象的计数器属性
// 由于函数声明被提前了, 因此这里是可以在函数声明之前给他的成员赋值的
uniqueInteger.count = 0

// 每次调用这个函数都会返回一个不同的整数
// 它使用一个属性来记住下一次要返回的值
function uniqueInteger() {
return uniqueInteger.count++
}

计算阶乘

1
2
3
4
5
6
7
8
9
function factorial(n) {
if (isFinite(n) && n > 0 && n === Math.round(n)) {
if (!(n in factorial))
factorial[n] = n* factorial(n-1)
return factorial[n]
}
else return NaN
}
factorial[1] = 1

8.5 作为命名空间的函数

js的函数作用域概念: 在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中), 在函数外部是不可见的. 不在任何函数内声明的变量是全局变量, 在整个js程序中都是可见的.

js无法声明只在一个代码块内可见的变量, es6中可以了. 这里的话可以简单地定义一个函数用作临时的命名空间, 不会污染全局命名空间.

1
2
3
4
5
function mymodule() {
// 局部变量
}

mymodule() // 调用

上面定义了一个单独的全局变量, 叫mymodule的函数, 可以直接定义一个匿名函数, 并在单个表达式中调用它.

1
2
3
(function() {   // 匿名函数

}()) // 结束函数定义并立即调用它

定义匿名函数并立即在单个表达式中调用它的写法非常常见. 使用(才会正确得把他解析为函数定义表达式.

8.6 闭包

js也使用词法作用域, 也就是说,函数的执行依赖于变量作用域, 这个作用域是在函数定义时决定的, 而不是函数调用时决定的, 为了实现词法作用域, js函数对象的内部状态不仅包含函数的代码逻辑, 还必须引用当前的作用域链. 函数对象可以通过作用域链相互关联起来, 函数体内部的变量都可以保存在函数作用域内, 称为闭包

从技术角度讲, 所有的js函数都是闭包: 他们都是对象, 他们都关联到作用域链. 定义大多数函数时的作用域链在调用函数时依旧有效, 但这并不影响闭包.
当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域时, 事情就变得非常微妙. 当一个函数嵌套了另外一个函数, 外部函数讲嵌套的函数对象作为返回值返回的时候往往会发生这种事情.

理解闭包首先要了解嵌套函数的词法作用域规则.

1
2
3
4
5
6
7
var scope = "global scope"          // 全局变量
function checkscope() {
var scope = "local scope" // 局部变量
function f() { return scope}
return f() // 注意这里有() 直接返回结果
}
checkscope() // => "local scope"
1
2
3
4
5
6
7
var scope = "global scope"          // 全局变量
function checkscope() {
var scope = "local scope" // 局部变量
function f() { return scope}
return f // 注意这里没() 返回的是函数内嵌套的一个函数对象.
}
checkscope()() // => "local scope"

词法作用域的基本规则: js函数的执行用到了作用域链, 这个作用域链是函数定义的时候创建的. 嵌套的函数f()定义在这个作用域里, 其中的变量scope一定是局部变量, 不管在何时何地执行函数f(), 这种绑定在执行f()时依旧有效, 因此最后一样代码返回local scope而不是global scope.
简言之, 闭包的这个特性强大到让人吃惊: 他们可以捕捉到局部变量(和参数), 并一直保存下来, 看起来像这些变量绑定到了其中定义他们的外部函数.

更底层, 了解基于栈的CPU架构: 如果一个函数的局部变量定会在CPU的栈中, 那么当函数返回时他们的确就不存在了.

1
2
3
4
var uniqueInteger = (function() {
var counter = 0;
return function() { return counter++}
})

也可以嵌套多个, 多个嵌套函数都共享一个作用域链.

1
2
3
4
5
6
7
function counter() {
var n = 0;
return {
count: function() { return n++},
reset: function() { n = 0}
}
}

一个对象下的共享, 不同对象下的不影响.

从技术角度看, 其实可以将这个闭包合并为属性存取器方法getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function counter(n) {   //  函数参数n是一个私有变量, count函数并没有声明局部变量
return {
// 属性getter方法返回并给私有计数器var递增1
get count() { return n++; },
set count(m) {
if (m >= n) n = m;
else throw Error("count can only be set to a large value")
}
}
}

var c = counter(1000)
c.count
c.count = 2000
1
2
3
4
5
6
7
8
9
10
11
12
13
function addPrivateProperty(o, name, preficate) {
var value
// getter
o["get" + name] = function() { return value }

// setter
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw Error("set" + name + ": invalid value " + v)
else
value = v
}
}

注意咯, 这个函数的getter和setter函数, 所操作的属性值并没有存储在对象o中, 这个值仅仅保存在函数中的局部变量中.
也就是说, 对于两个存取器方法来说这个变量是私有的, 没有办法绕过存取器方法来设置或修改这个值.

要注意如果用循环创建很多个闭包, 会犯一个错误. 所有的闭包都共享一个值. 关联到闭包的作用域链都是”活动的”, 嵌套的函数不会将作用域内的私有成员复制一份, 也不会对所绑定的变量生成静态快照.

书写闭包的时候还需要注意, this是js的关键字, 而不是变量. 每个函数调用都包含一个this值, 如果闭包在外部函数里是无法访问this的, 除非外部函数讲this转存为一个变量.
arguments类似, 他并不是一个关键字, 但在调用每个函数时都会自动声明它, 由于闭包具有自己所绑定的arguments, 因此闭包内无法直接访问外部函数的参数数组, 除非外部函数将参数数组保存到另一个变量中.

8.7 函数属性, 方法和构造函数

js中函数是值, 用typeof运算符返回字符串'function', 但函数又是js中特殊的对象. 所有有属性和方法, 甚至可以用Function()构造函数来创建新的函数对象.

8.7.1 length属性

函数体里的arguments.length表示传入函数的实参的个数. 而在函数本身的length属性则有不同的含义. 函数的length属性是只读属性, 它代表函数是参数的数量, 这里的参数指的是’形参’而非’实参’, 也就是函数定义时给出的形参个数, 通常也是在函数调用时期望传入函数的参数个数.

例子是一个check()的函数, 从另外一个 函数给他传入arguments数组, 它比较arguments.length(实际传入的实参个数)和arguments.callee.length(期望传入的实参个数)来判断所传入的实参个数是否正确.

1
2
3
4
5
6
7
8
9
10
11
function check(args) {
var actual = args.length; // 实参的真实个数
var expected = args.callee.length; // 期望的是从哪个数
if (actual !== expected)
throw Error("Expected " + expected + "args; got" + actual)
}

function f(x, y, z) {
check(arguments);
return x + y + z;
}

8.7.2 prototye属性

每个函数都有一个prototype属性, 这个属性是指向一个对象引用, 这个对象称作原型对象(prototype object). 每一个函数都包含不同的原型对象, 当将函数用作构造函数的时候, 新创建的对象会从原型对象上继承属性.

8.7.3 call()方法和apply()方法

深入浅出 妙用Javascript中apply、call、bind

我们可以将call()和apply()看做某个对象的方法, 通过调用方法的形式来简介调用函数. call()和apply()的第一个实参是要调用函数的母对象, 他是调用上下文, 在函数体内通过this来获得对他的引用. 想要以对象o的方法来调用函数f(), 可以这样使用过call()和apply()

1
2
f.call(o)
f.apply(o)

每行代码和下面代码的功能类似(假设对象o中预先不存在名为m的属性)

1
2
3
o.m = f;        // 将f存储为o的零临时方法
o.m(); // 调用它, 不传入参数
delete o.m; // 将临时方法删除

在ECMAScript5的严格模式中, call()和apply()的第一个实参都会变为this值, 哪怕传入的实参是原始值甚至是null或undefined. 在ECMASceipt3和费严格模式中, 传入的null或undefined都会被全局对象代替, 而其他原始值则会被相应的包装对象所替代.

对于call()来说, 第一个调用上下文实参之后的所有实参就是要传入待调用函数的值. 比如, 以对象o的方法的形式来调用函数f(), 并传入两个参数, 可以使用这样的代码:

1
f.call(o, 1, 2)

apply()方法和call()类似, 但传入实参的形式和call()有所不同, 它的实参都放入一个数组中

1
f.apply(o, [1, 2])

如果一个函数的实可以是任意数量, 给apply()传入的参数数组可以是任意长度的. 比如, 为了找出数组中最大的数组元素, 调用Math.max()方法的时候可以给apply()传入一个包含任意个元素的数组:

1
var biggest = Math.max.apply(Math, array_of_numbers);

需要注意的事, 传入apply()的参数数组可以是类数组对象, 也可以是真实数组. 实际上, 可以将当前函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数参数

1
2
3
4
5
6
7
8
9
10
11
// 将对象o中名为m()的方法替换为另一个方法
// 可以砸调用原始的方法之前和之后记录日志消息
function trace(o, m) {
var original = o[m]
o[m] = function() {
console.log(new Date(), "Entering:", m)
var result = original.apply(this, arguments)
console.log(new Date(), "Exiting:", m)
return result
}
}

trace()这个函数接收两个参数, 一个对象和一个方法名, 他将执行的方法替换为一个新方法, 这个新方法是”包裹”原始方法的另一个泛函数. 这种动态修改已有方法的做法有时称作”monkey-patching”

8.7.4 bind()方法

bind()在ECMAScript5中新增的方法,但在ECMAScript3中可以轻易模拟bind(). 从名字上就可以看出, 这个方法的主要作用就是将函数绑定至某个对象. 当在函数f()上调用bind()方法并传入一个对象o作为参数, 这个方法将返回一个新的函数, (已函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用. 传入新函数的任何实参都将传入原始函数.

1
2
3
4
function f(y) { return this.x + y } // 待绑定的函数
var o = { x : 1} // 将要绑定的对象
var g = f.bind(o) // 通过调用g(x)来调用o.f(x)
g(2) // => 3

可以通过如下代码轻易地实现这种绑定

1
2
3
4
5
6
7
// 返回一个函数, 通过调用它来调用o中的方法f(), 传递它所有的实参
function bind(f, o) {
if (f.bind) return f.bind(o) // 如果bind()方法存在的话, 就使用bind()方法
else return function() {
return f.apply(o, arguments)
}
}

EMCAScript 5中的bind()不仅仅是讲函数绑定至一个对象, 他还附带一些其他应用: 除了第一个实参之外, 传入bind()的实参也会绑定至this, 这个附带的应用是一种常见的函数式编程技术, 有时也被称为”柯里化”(currying)

1
2
3
4
5
6
7
8
9
var sum = function(x, y) { return x + y }   // 返回两个实参的和值
// 创建一个类似sum的新函数, 但this的值绑定到null
// 并且第一个参数绑定到1, 这个新的函数期望只传入一个实参
var succ = sum.bind(null, 1)
succ(2) // => 3 x绑定到1, 并传入2作为实参y

function f(y, z) { return this.x + y + z } // 另外一个做累加计算的函数
var g = f.bind({ x:1 }, 2) // 绑定this和y
g(3) // => 6 this.x绑定到1, y绑定到2, z绑定到3

我们可以绑定this的值并在ECMAScript 3 中实现这个附带的应用.

ES3的bind写法

8.7.5 toString()方法

和所有js对象一样, 函数也有toString()方法, 返回一个字符串, 这个字符串和函数声明语句的语法相关. 大部分的toString()方法都返回函数的完整源码. 内置函数往往返回一个类似 “[native code]” 的字符串作为函数体.

8.7.6 Function()构造函数

不管是通过函数定义语句还是函数直接量表达式, 函数的定义都要使用function关键字. 但函数还可以通过Function构造函数来定义,

1
var f = new Function("x", "y", "return x*y")

这一行代码创建一个新的函数, 这个函数和通过下面代码定义的函数几乎等价:

1
var f = function(x, y) { return x*y }

Function()构造函数可以传入任意数量的字符串实参, 最后一个是实参所表示的文本就是函数体, 他可以是包含任意的js语句, 每两条语句之间用分号隔开. 传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串. 如果定义的函数不包含任何参数, 只需给构造函数简单地传入一个字符串-函数体-即可.

注意: Function()构造函数并不需要通过传入实参以指定函数名. 就想函数直接量一样, Function()构造函数穿件一个匿名函数.

几点注意:

  • Function()构造函数语序js在运行时动态地创建并编译函数
  • 每次调用Function()构造函数都会解析函数体, 并创建新的函数对象. 如果实在一个循环或多次调用的函数中执行这个构造函数, 执行效率会受影响. 相比之下, 循环中的嵌套函数和函数定义表达式则不会每次执行时都重新编译.
  • 最后一点, 也就是Function()构造函数, 他所创建的函数并不是使用词法作用域, 相反, 函数体代码的编译总是会在顶层函数执行(全局作用域)

我们可以将Function()构造函数认为是在全局作用域中执行的eval(), eval()可以在自己的私有作用域内定义新变量和函数, Function()构造函数在实际编程过程中很少会用到.

8.7.7 可调用的对象

“类数组对象:并不是真正的数组, 但大部分场景下可以将其当做数组来对待. 对于函数也存在类似的情况, “可调用的对象”(callable object)是一个对象, 可以在函数调用表达式中调用这个对象. 所有的函数都是可调用的额, 但并非所有的可调用对象都是函数.

可调用对象在2个jd实现中不能算作函数. 首先IE Web浏览器实现了客户端方法(诸如Window.alert()和Document.getElementById()), 使用了可调用的宿主对象, 而不是内置函数对象. IE中的这些方法在其他浏览器中也都存在, 但他们本质上不是Function()对象. IE9件=将他们是吸纳为真正的函数, 因此这类可调用的对象将越来越罕见.

另一个常见的可调用对象是RegExp对象(在众多浏览器中均有实现), 可以直接调用RegExp对象, 这比调用它的exec()方法更快捷一些. 在js中这是一个彻头彻尾的非标准特性. 所以代码最好不要对可调用的RegExp对象有太多依赖, 这个特性在不久的将来可能会废弃并删除. 对RegExp执行typeof运算的结果并不统一, 有些是function, 有些返回object

想检测一个对象是否是真正的函数对象(并且具有函数方法), 可以参照代码检测它的class属性

1
2
3
function isFunction(x) {
return Object.prototype.toString.call(x) === "[object Function]"
}

8.8 函数式编程

js并非函数时编程语言, 但js中可以像操控对象一样操控函数, 也就是说可以在就是中应用函数式编程技术. ES5中的数组方法就可以非常适合用于函数式编程风格.

8.8.1 使用函数处理数组

比如计算平均值和标准差. 不使用函数式编程风格是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var data = [1, 1, 3, 5, 5];

// 平均数是所有元素的累加和值除以元素个数
var total = 0;
for(var i = 0; i < data.length; i++) {
total += data[i];
}
var mean = total/data.length;

// 计算标准差, 首先计算每个数据减去平均数之后偏差的平方然后求和
total = 0;
for(var i = 0; i < data.length; i++) {
var deviation = data[i] - mean;
total += deviation * deviation;
}
var stddev = Math.sqrt(total/(data.length-1))

可以使用数组方法map()和reduce()来实现同样的计算, 这种实现及其简介

1
2
3
4
5
6
7
8
9
// 首先定义两个简单的函数
var sum = function(x, y) { return x+y };
var square = function(x) { return x*x };

// 然后将这些函数和数组方法配合使用个计算出平均值和标准差
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum)/data.length;
var deviations = data.map(function(x) { return x-mean })
var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1))

如果用ES3呢, 要自定义map()和reduce()函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对于每个数组元素调用函数f(), 并返回一个结果数组
// 如果Array/prototype.map定义了话, 就使用这个方法
var map = Array.prototype.map
? function(a, f) { return a.map(f) }
: function(a, f) {
var results = []
for(var i = 0, length = a.length; i < len; i++) {
if (i in a) results[i] = f.call(null, a[i], i, a)
}
return results;
}

// 使用函数f()和可选的初始值将数组a减至一个值

8.8.2 高阶函数

指操作函数的函数, 他接受一个或多个函数作为参数, 并返回一个新函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这个高阶函数返回一个新的函数, 这个新函数将他的实参传入f()
// 并返回f的返回值的逻辑非
function not(f) {
return functin() { // 返回一个新函数
var result = f.apply(this, arguments) // 调用f()
return !result // 对结果求反
}
}

var enen = function(x) {
return x%2 === 0
}

var odd = not(even) // 一个新函数, 所做的事情和even相反
[1, 1, 3, 5, 5].every(odd)

上面的not()函数就是一个高阶函数, 因为他接受一个函数作为参数, 并返回一个新函数.

1
2
3
4
5
6
7
8
9
10
// 所返回的函数的参数应当是一个实参数组, 并对每个数组元素执行函数f()
// 并返回所有计算结果组成的数组
// 可以对比一下这个函数和上下文提到的map()函数
function mapper(f) {
return function(a) { return map(a, f) }
}

var increment = function(x) { return x+1 }
var incrementer = mapper(increment)
incrementer([1, 2, 3])

后面还有partial()和memoize()都挺重要的高阶函数.

8.8.3 不完全函数

这里讨论的是一种函数变换技巧, 即把一次完整的函数调用拆成多次函数调用, 每次传入的实参都是完整实参拿得一部分, 每个拆分开的函数叫做不完全函数(partial function), 每次函数调用叫做不完全调用(partial application), 这种函数变换的特点是每次调用都返回一个函数, 知道得到最终运行结果为止. 比如对函数f(1,2,3,4,5,6)的调用修改为等价的f(1,2)(3,4)(5,6), 后者包含三次调用, 和每次调用相关的函数就是”不完全函数”

函数f()的bind方法返回一个新函数, 给新函数传入特定的上下文和一组指定的参数, 然后调用函数f(). 我们说它把函数”绑定至“对象并传入一部分参数. bind()方法只是将实参放在(完整实参列表的)左侧, 也就是说传入bind()的实参都是放在传入原始函数的实参列表开始的位置, 但有时候我们期望将传入bind()的实参放在(完整实参列表的)右侧:

8.8.4记忆memorization

比如阶乘函数, 他可以将上次的计算结果缓存起来. 在函数式编程当中, 这种缓存技巧叫做 记忆, 下面的代码展示一个高阶函数, memorize()节后一个函数作为实参, 并返回带有记忆能力的函数.

记忆只是一种编程技巧, 本质上是牺牲算法的空间复杂度以换取更优的时间复杂度, 在客户端js中代码的执行时间复杂度往往成为瓶颈, 因此大多说场景下, 牺牲空间换时间是非常可取.

1
2
3
4
5
6
7
8
9
10
11
// 返回f()的带有记忆功能的版本
// 只有当f()的实参的字符串表示都不相同时他才会工作
function memorize(f) {
var cache = {} // 将值保存在闭包内
return function() {
// 将实参转换为字符串形式, 并将其用作缓存的键
var key = arguments.length + Array.prototype.join.call(arguments, ",")
if (key in cache) return cache[key]
else return cache[key] = f.apply(this, arguments)
}
}

memorize()函数创建一个新的对象, 这个对象被当做缓存(的宿主)并赋值给一个局部变量, 因此对于返回的函数来说他是私有(在闭包中). 所返回的函数将他的实参数组转换成字符串, 并将字符串用作缓存对象的数组名, 如果在缓存中存在这个值, 则直接返回它.

否则, 就调用既定函数对实参进行计算, 将计算结果缓存起来并返回.

参考

docker的简单使用

发表于 2018-12-10 | 分类于 后端

docker 的简单使用

docker 简介

Flux7 Docker 系列教程(一):Docker 简介 主要看英文版的好.
Docker — 从入门到实践

层叠的只读文件系统, 联合加载.

Docker 3 个组件/3 个元素

3 个组件是 Docker Client, Docker Daemon, Docker index/Registry.
3 个元素是 Docker Container, Docker Image, DockerFile.

他们如何交互呢?

d1.png
d2.png
d3.png

  1. Docker Client:用户和 Docker 守护进程进行通信的接口,也就是 docker 命令。
  2. Docker Daemon 守护进程:宿主机上用于用户应答用户请求的服务。
  3. Docker Index:用户进行用户的私有、公有 Docker 容器镜像托管,也就是 Docker 仓库。
  4. Docker constainer 容器:用于运行应用程序的容器,包含操作系统、用户文件和元数据。
  5. Docker image 镜像:只读的 Docker container 容器模板,简言之就是系统镜像文件。
  6. DockerFile:进行镜像创建的指令文件。

在学习 Docker 组件之前,先来看一下 Docker 底层到底是由什么组成的(暂时别看)

  1. Namespace:隔离技术的第一层,确保 Docker 容器内的进程看不到也影响不到 Docker 外部的进程。
  2. Control Groups:LXC 技术的关键组件,用于进行运行时的资源限制。
  3. UnionFS(文件系统):容器的构件块,创建抽象层,从而实现 Docker 的轻量级和运行快速的特性。

如何运行咯

运行任何应用都必须按照以下两个步骤来: 先有镜像再有容器, container 就是一个 image 的实例 instance

  1. 创建一个 image 镜像文件
  2. 运行 container 容器

1. 创建一个镜像文件

Docker image相当于一个只读的模板文件,保存着运行container所需要的所有的配置、文件;每次启动,都会以基础的 Docker image为模板,按照 Dockerfile 的指令,建立一个新的适用于你自己的 Dokcer image:实际上是在这个基础镜像上建立了一个新的应用层。

自己创建的 Docker image可以推送到 Docker Index 中心,然后提供给他人使用。

2. 运行容器

container被运行后,会在原有的image上创建一个可读写的层,容器设置完毕网络之后便可以运行应用了。

docker 本身的启动, 本身就可以看做是一个守护进程 service docker start/stop/restart,在/etc/default/docker配置
查看 docker 运行没, ps -ed | grep docker 或 status docker

重点看 3 个概念 image container registry

Docker — 从入门到实践

在说下概念

Docker 镜像

我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

Docker 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。

Docker Registry

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。

镜像

获取, 查看, 创建, 删除

  1. 获取镜像 docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
    1. 不给用户名会默认指定是 library
  2. 使用 docker run -it --rm ubuntu:18.04 bash 进入后可以用 exit 或 ctrl+d 退出并停止容器.
    1. 这里主要是配合容器看
  3. 列出顶层 docker image ls [option] [名[:tag]] 或 docker images 有个 s 和没有 s 的区别
    1. 看到 <none> 的是 虚悬镜像(dangling image)
    2. 用 -f 指定过滤 docker image ls -f dangling=true 删除这些没用的虚悬镜像 docker image prune
    3. 列出中间(所有镜像) docker image ls -a
    4. 只列出 id docker image ls -q 常配合删除用
    5. 自定义 ls 出来的表格格式 docker image ls --format ""
  4. 删除 docker image rm [选项] <镜像1> [<镜像2> ...]
    1. 镜像可以是 镜像短 ID、镜像长 ID、镜像名 或者 镜像摘要, 查看摘要 docker image ls --digests
    2. 删除行为分为两类,一类是 Untagged,另一类是 Deleted. 镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签. 无标签指向, 无镜像依赖和无容器依赖时就是 deleted
    3. 批量删除, 配合 ls 一起用 docker image rm $(docker image ls -q redis)

创建的 dockerfile 后面说, docker commint 不用

容器

查看, 启动和停止, 删除

  1. 启动: 有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动
    1. docker run -t -i ubuntu:18.04 /bin/bash
    2. docker container start 将一个已经终止的容器启动运行
  2. 后台运行 docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" 加个 -d 参数
    1. docker container ls 看容器信息
    2. 获取容器的输出信息 docker container logs [container ID or NAMES] 和 docker logs 一样
  3. 终止 docker container stop 来终止一个运行中的容器. 此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
    1. 只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止. 所以加参数 -d 和进入后再推出是有区别的
    2. docker container ls -a 可以看终止状态的
    3. docker container restart 命令会将一个运行态的容器终止,然后再重新启动它
  4. 进入容器: 在使用 -d 参数时,容器启动后会进入后台。
    1. 使用 docker attach 命令或 docker exec 命令
    2. attach 的 如果用 exit,会导致容器的停止
    3. docker exec -it 69d1 bash 用 exit,不会导致容器的停止
  5. 删除 docker container rm trusting_newton
    1. docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器, docker container prune 清理掉所有处于终止状态的容器
  6. 导入导出: docker export 和 docker import
    1. docker load这个是镜像存储文件, 前面的 容器快照文件将丢弃所有的历史记录和元数据信息

仓库

仓库(Repository)是集中存放镜像的地方, 一个容易混淆的概念是注册服务器(Registry)

  1. docker login [地址] 和 docker logout
    1. docker search centos 和 docker pull
  2. 打完 tag 后 push docker tag ubuntu:18.04 username/ubuntu:18.04, docker push username/ubuntu:18.04

自动构建

数据和网络

数据就是 volume, 数据卷和主机目录

  1. 查看 docker volume ls
    1. docker volume inspect my-vol
    2. 在容器中 docker inspect web 的 "Mounts" 字段
  2. 创建 docker volume create my-vol
  3. 删除 docker volume rm my-vol
    1. docker volume prune 数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令
  4. 在创建容器时挂上卷 -v --mount source=my-vol,target=/webapp 缩写 -v my-vol:/wepapp

上面是创建了数据卷挂上, 还有是不用创建, 就是挂主机目录

  1. --mount type=bind,source=/src/webapp,target=/opt/webapp 简写 -v /src/webapp:/opt/webapp
    1. 默认是读写的, 可以指定只读 -v /src/webapp:/opt/webapp:ro

所以只需要用 -v 就好, 也不用指定 type 了

网络就是参数 -p

Docker 的 14 个命令,注意区分 image 和 container

docker_command

  1. docker info: 检查 Docker 是否安装, 显示 Docker 信息 docker version
  2. docker images: 检查本机有多少个 docker image list, top level 的. -a看完整的, 包括中间层 images, -q只看 id
  3. docker inspect看 top level layer 的 image 详情 MD 信息,也可以看 container, 如 IP,port, 配置信息
  4. docker search <imageName>: 在 docker index 搜索镜像 -sstar
  5. docker pull <image name>: 下载镜像
  6. docker run busybox /bin/echo Hello worrld: 运行 container,(-d -p) 是docker create和docker start结合, 创建 container 并运行 container docker create:是为指定 image 添加一个可读写层,构成一个新的 container. 从 image => container. docker start: 是为 container 文件系统创建了一个进程隔离空间, 每个容器只有一个. 里面跑不跑进程随意.
  7. docker ps: 列出运行中的 container, -a 是所有不运行的也列出, 就是只看start不看create的咯和docker container ls,docker container ls --all
  8. docker logs <ID/>name>: 查询输出,
  9. docker stop <ID/>name>: 停止 container,优雅退出,回收进程空间 start 和 stop 可以多个 docker kill <ID/>name>直接杀. docker container stop这么写太繁琐
  10. ctl+p ctl+q: 将容器后台, 也就是变为守护, attach, exit后ctl+d: 退出容器
  11. docker start/restart <ID/>name>: 运行/重启 container docker attach重新进入-d 的守护容器
  12. docker exec: 在运行中的 container 中(有了进程空间的)运行 command, 执行进程
  13. docker rm <ID/>name>: 删除构成 container 的可读写层,要先 stop. docker rmi <ID/>name>是删 top level layer image, docker rmi $(docker images -q): 删了所有层 . 就是docker container rm和docker image rm 简写
  14. docker commit <ID/>name> <newImageName>: 保存 contaner 为新 image(将可读写层变为只读层, 无论容器运不运行, 注意层的 id 会变), docker build: 从一个 DockerFile 中建立 image(里面是 4 步,先 FROM 获取到 image, 然后重复地 1. docker run新建一层读写,然后分配进程空间, 再 2. RUM 执行命令 RUN 其实就会新建一个读写层的 , 3. docker commit保存这层为只读层), 两种方式都可以构建镜像. -t是打 tag. 类似git 每一个修改都是一个commit, 保存记录.
  15. docker history <imageName>: 获取镜像历史(只能是本地)
  16. docker push <user>/<repo_name>: 推送镜像. docker login -u -p和docker logout :注意登录 id 不是邮箱, 然后这个 id 就算你注册的时候填了大写的, 注册成功后还都是小的, 看一下.(登不上就直接去 UI 桌面中登录好了)
  17. docker help和docker <command> --help: 帮助
  18. docker update: 更新 contain 而配置
  19. docker rename重命名 container,也可以在docker run的时候指定 name
  20. docker top: 列出一个 container 中的运行的进程.
  21. docker pause暂停一个或多个 container 中的所有进程 docker unpause, 保留进程空间
  22. docker port: 列出 container 的端口映射
  23. docker tag image username/repository:tag: 给 image 打 tag,保留原始镜像, docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG] 就是打一个 image 名字是username/repository tag 是tag

暂时不看的命令

  1. docker import和docker export : export会把容器打tar,但会移除不必要的 MD 和 image 层, 只输出一个层,很好用. 只能看到一个 image, save 能看到历史镜像
  2. docker save和docker load: 使用 load 从 stdin 导入一个 tar 格式的镜像或者仓库,然后用 save 将 tar 镜像输出到 stdout。save会把一个镜像,只能是镜像,打tar并保留每一层的 MD 信息. 两个命令就是方便在别的机器上用
  3. docker cp: 从 container 中复制进去文件
  4. docker deploy和
  5. docker diff: 看 container 中文件改动
  6. docker events docker stats docker wait

run_commit

自动化 DockerFile

Docker 提供的 Dockerfile 是一个类似 Makefile 的工具,主要用来自动化构建镜像。既然能自动化创建镜像,那么我们何必去手动创建镜像呢。

格式

Dockerfile 中所有的命令都是以下格式:INSTRUCTION argument
指令(INSTRUCTION)不分大小写,但是推荐大写。

FROM 命令

FROM <imageName>,例如 FROM ubuntu

所有的 Dockerfile 都用该以 FROM 开头,FROM 命令指明 Dockerfile 所创建的 image 文件以什么镜像为基础,FROM 以后的所有指令都会在 FROM 的基础上进行创建镜像;可以在同一个 Dockerfile 中多次使用 FROM 命令用于创建多个 image。

MAINTAINER 命令

MAINTAINER <authorName> 用于指定镜像创建者和联系方式。

例如:

1
MAINTAINER Victor Coisne victor.coisne@dotcloud.com

RUN 命令

RUN <command> 用于 container 内部执行命令。每个 RUN 命令相当于在原有的 image 基础上添加了一个改动层,原有的镜像不会有变化, 运行完后再提交成只读层. 即类似先docker run再docker commit

ADD 命令

ADD <src> <dst> 用于从将 <src> 文件复制到 <dst> 文件:<src> 是相对被构建的 DockerFile 的相对路径,可以是文件或目录的路径,也可以是一个远程的文件 url,<dst> 是 container 中的绝对路径。

COPY 命令

只能讲和 Dockerfile 文件同目录中的内容复制到 container 中的指定目录,通常是WORKDIR指定的工作目录,
如: Copy the current directory contents into the container at /app

1
COPY . /app

暂时只要这个命令是改动 1 个层, 不会有 Running 的那个层

/var/lib/docker/tmp/docker-builderXXXXXXX/… no such file or directory #1922

WORKDIR只是在 image 内设置路径

说下docker build的最后的点.

1
docker build -t hello-demo-app .

which sets the current directory as the context, let’s say you wanted the parent directory as the context, just use:

1
docker build -t hello-demo-app ..

CMD 命令

CMD 命令有 3 种格式:

  • CMD ["executable","param1","param2"]:推荐使用的 exec 形式。
  • CMD ["param1","param2"]:无可执行程序形式
  • CMD command param1 param2:shell 形式。

CMD 命令用于启动 container时默认执行的命令,即docker run时的默认命令, 但如果docker run加了命令, 会覆盖CMD的. 而ENTRYPOINT不会被覆盖, 要覆盖需要docker run --entry加参数
,CMD 命令可以包含可执行文件,也可以不包含可执行文件:不包含可执行文件的情况下就要用 ENTRYPOINT 指定一个,然后 CMD 命令的参数就会作为ENTRYPOINT的参数。

一个 Dockerfile 中只能有一个CMD,如果有多个,则最后一个生效。
CMD 的 shell 形式默认调用 /bin/sh -c 执行命令。
CMD命令会被 Docker 命令行传入的参数覆盖:docker run busybox /bin/echo Hello Docker 会把 CMD 里的命令覆盖。

ENTRYPOINT 命令

ENTRYPOINT 命令的字面意思是进入点,而功能也恰如其意:他可以让你的 container 表现得像一个可执行程序一样。

ENTRYPOINT 命令也有 2 种格式:

  • ENTRYPOINT ["executable", "param1", "param2"] :推荐使用的 exec 形式
  • ENTRYPOINT command param1 param2 :shell 形式

一个 Dockerfile 中只能有一个 ENTRYPOINT,如果有多个,则最后一个生效。

关于 CMD 和 ENTRYPOINT 的联系请看下面的例子

仅仅使用 ENTRYPOINT:

1
2
FROM ubuntu
ENTRYPOINT ls -l

执行 docker run 306cd7e8408b /etc/fstab 和 docker run 306cd7e8408b 结果并不会有什么差别:

1
2
3
4
5
6
7
8
命令 # docker run 306cd7e8408b /etc/fstab
total 64
drwxr-xr-x 2 root root 4096 Mar 20 05:22 bin
drwxr-xr-x 2 root root 4096 Apr 10 2014 boot
drwxr-xr-x 5 root root 360 Apr 24 02:52 dev
drwxr-xr-x 64 root root 4096 Apr 24 02:52 etc
drwxr-xr-x 2 root root 4096 Apr 10 2014 home
……

但是我们通常使用 ENTRYPOINT 作为 container 的入口,使用 CMD 给 ENTRYPOINT 增加默认选项:

1
2
3
FROM ubuntu
CMD ["-l"]
ENTRYPOINT ["ls"]

然后执行这个 constainer:
不加参数便会默认有 -l参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
命令 # docker run 89dc7e6d0ac1
total 64
drwxr-xr-x 2 root root 4096 Mar 20 05:22 bin
drwxr-xr-x 2 root root 4096 Apr 10 2014 boot
drwxr-xr-x 5 root root 360 Apr 24 02:47 dev
drwxr-xr-x 64 root root 4096 Apr 24 02:47 etc
drwxr-xr-x 2 root root 4096 Apr 10 2014 home
drwxr-xr-x 12 root root 4096 Mar 20 05:21 lib
drwxr-xr-x 2 root root 4096 Mar 20 05:20 lib64
drwxr-xr-x 2 root root 4096 Mar 20 05:19 media
drwxr-xr-x 2 root root 4096 Apr 10 2014 mnt
drwxr-xr-x 2 root root 4096 Mar 20 05:19 opt
dr-xr-xr-x 386 root root 0 Apr 24 02:47 proc
drwx------ 2 root root 4096 Mar 20 05:22 root
drwxr-xr-x 7 root root 4096 Mar 20 05:21 run
drwxr-xr-x 2 root root 4096 Apr 21 22:18 sbin
drwxr-xr-x 2 root root 4096 Mar 20 05:19 srv
dr-xr-xr-x 13 root root 0 Apr 24 02:47 sys
drwxrwxrwt 2 root root 4096 Mar 20 05:22 tmp
drwxr-xr-x 11 root root 4096 Apr 21 22:18 usr
drwxr-xr-x 12 root root 4096 Apr 21 22:18 var

加了 /etc/fstab 参数便会覆盖原有的 -l 参数:

1
2
命令 # docker run 89dc7e6d0ac1 /etc/fstab
/etc/fstab

EXPOSE 命令

EXPOSE <port> [<port>...] 命令用来指定 container 对外开放的端口。
例如 EXPOSE 80 3306,开放 80 和 3306 端口, 但在docker run的时候还得在 host 上指定端口的

WORKDIR 命令

WORKDIR /path/to/work/dir 配合 RUN,CMD,ENTRYPOINT 命令设置指令执行时的工作目录。
可以设置多次,如果是相对路径,则相对前一个 WORKDIR 命令。默认路径为/。

会分 2 步变动层

例如:

1
2
3
4
5
6
FROM ubuntu
WORKDIR /etc
WORKDIR ..
WORKDIR usr
WORKDIR lib
ENTRYPOINT pwd

docker run ID 得到的结果为:/usr/lib

USER 命令

USER <UID/Username> 为 container 内指定 CMD RUN ENTRYPOINT 命令运行时的用户名或 UID。

VLOUME 命令

VOLUME ['/data'] 允许 container 访问容器的目录、允许 container 之间互相访问目录。
VOLUME 仅仅是允许将某一个目录暴露在外面,更多的操作还需要依赖 Docker 命令实现。

更多的内容可以参考 深入理解 Docker Volume(一)

ENV 命令: Define environment variable

参考 export 的用法咧:
ENV LC_ALL en_US.UTF-8

最佳实践

所有应用都会有个最佳的方式,Dockerfile 也不例外,下面是我们总结出的最佳实现方式:

  1. 把维护者和更新系统的命令依次写在最上方
  2. 使用标签管理 Dockerfile
  3. 避免映射公共端口
  4. 使用类似 array 形式的 CMD 和 ENTRYPOINT

注:映射端口并不属于 Dockerfile 的工作范围。

概念: 10 张图带你深入理解 Docker 容器和镜像

Image Definition

Image 就是一堆只读层(read-only layer)的统一视角
image1.png

Container Definition

容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。
container1.png

container 只是在 image 上增加一层读写层, 并没有提及容器是否在运行.
要点:容器 = 镜像 + 读写层。并且容器的定义并没有提及是否要运行容器。

Running Container Definition

一个运行态容器(running container)被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。
running_container1

正是文件系统隔离技术使得 Docker 成为了一个前途无量的技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层(read-write layer)。
running_container2

Image Layer Definition

为了将零星的数据整合起来,我们提出了镜像层(image layer)这个概念。下面的这张图描述了一个镜像层,通过图片我们能够发现一个层并不仅仅包含文件系统的改变,它还能包含了其他重要信息。
image_layer1

元数据(metadata)就是关于这个层的额外信息,它不仅能够让 Docker 获取运行和构建时的信息,还包括父层的层次信息。需要注意,只读层和读写层都包含元数据。
image_layer2

除此之外,每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于最底层。
image_layer3

区别点

  • docker run 与docker start的区别

docker run 与 docker start 的区别,为容器命名
docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。
docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。
run_start.png
而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID 或名字,我们可以使用docker ps找到这个容器的信息。

  • docker run和docker exec的区别

“docker run”VS“docker exec”,这两个命令有区别吗?
docker run通常是在新创建的容器中所使用的命令. 它适用于在没有其他容器运行的情况下,您想要创建一个容器,并且要启动它,然后在其上运行一个进程。如果镜像已经在容器中了, 用docker start或docker restart
docker exec适用于在现有容器中运行命令的情况。如果已经拥有了一个正在运行的容器,并希望更改该容器或从中获取某些内容,那么使用docker exec命令就非常合适了。

  • docker ps和docker images

一个看 containers, 一个看 images
docker ps 命令会列出所有运行中的 container。这隐藏了非运行态容器的存在,如果想要找出这些容器,我们需要使用docker ps -a
docker images命令会列出了所有顶层(top-level)image。实际上,在这里我们没有办法区分一个镜像和一个只读层,所以我们提出了top-level镜像。只有创建容器时使用的镜像或者是直接 pull 下来的镜像能被称为顶层(top-level)镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。

  • registry和repository

registry 是 docker 提供的线上的那个 index,包含 repository, repository 是本地中所有 image,类似 git 的库,github 是 registry

  • RUN和CMD

RUN是构建 image 时, 那一层 image 执行什么命令,而CMD是构建完 image 后,成为一个 container 后的命令

  • CMD和ENTRYPOINT

CMD命令用于启动 container时默认执行的命令,即docker run时的默认命令, 但如果docker run加了命令, 会覆盖CMD的. 而ENTRYPOINT不会被覆盖, 要覆盖需要docker run --entry加参数

  • 远程访问

就是 docker client 和 docker deamon 不在同一个 host 上, 平时通过 docker 命令访问 docker client 然后 docker client 会访问 deamon. 现在你可以直接通过远程 REST API 访问 docker deamon

  • detach 和 attach 区别

ctl+d退出, attach再回到运行的容器中

  • run 中 link 和 name 的区别

—name 只是别名, —link 是指定链接到那个容器

参考

10 张图带你深入理解 Docker 容器和镜像
Flux7 Docker 系列教程(一):Docker 简介
Docker — 从入门到实践 666
Docker 联合文件系统(Union Filesystem)

hexo制作自己的主题

发表于 2018-12-09 | 分类于 hexo

hexo制作自己的主题

更改主题

先从使用别人的主题开始讲, 很简答, 安装完hexo后, 进入themes里面就可以看到所有的主题了, 不过一般默认就一个自带的landscape主题, 我们先去官网主题下一个好了. 一般去 github上下.
下载下来后放在themes文件夹下就好了. 手动下载或用git命令都行啊

1
git clone git: themes/xxx

然后去_config.yml中改下theme: landscape为你下的主题名就好了.

参考

从零开始定制hexo主题
hexo自定义主题

js的数组

发表于 2018-12-07 | 分类于 javascript教程

js的数组

  1. js数组是无类型, 动态大小,很复杂哦. 0~2^32 -1 . 是对象的特殊形式, 碰巧key是整数哦. 但比用对象快. 0起始, undefined值
  2. 对象继承Array.prototype的属性, 真正的数组, 类数组对象.

同理, 如果看了原型那块, 创建数组也有2种方式, 1是字面量[] 2.是new Array() (1)表示长度. 这里原型都是 Array.prototype. 因为没区别, 所有还是直接用字面量好了.

  1. 数组元素的读写: []号, js会将整数索引转成字符串的, 然后作为key来用. 对象不就用[]可以么..
  2. 注意数组的length会自动维护. 但还有个区别, 清晰的区别: 数组的索引和对象的属性名, 一句话: 所有的索引都是属性名, 但只有0-2^32 -2之间的整数属性名才是索引哦.(不过你使用非负整数字符串也是可以的,毕竟整数也是转成字符串在找的) 所有的数组都对象.. 当然也是可以用负数,非整数啊来使用, 就是超过那个范围的, 都只能当做常规的对象属性哦.
  3. 所来说去, 数组的索引只是对象的属性名中的一种特殊类型, 所以js的数组没有越界的错误概念, 所以只会得到undefined哦, 对象也是.
  4. 然后是数组既然是对象, 就可以从原型中继承元素.
  5. 稀疏数组哦, undefined, 这个根本不存在一个元素有点区别
    [,,,]和new Array(3) 不同的, 前者有3个undefined元素, 后者啥也没, 打扰了, 现在是都是empty的 啥也没 不对, 有啊, 用length就可以看到长度(这个理解不对, 空间和有没有元素无关) . 暂时不纠结这个, 都当undefined的.
  6. 了解稀疏数组是了解js的数组的本质一部分. 只不过包含一些undefined值的数组.
  7. 数组长length, 区别于常规对象哦. 这个长度真的可以用来删除元素. 加空区域. Object.defineProperty设置length为只读哦.
  8. 元素的添加和删除. 用[],为索引赋值. push pop shift unshift头插入. 删delete a[1] 只是空间中元素没了, 空间还在, 变成稀疏数组这样. splice() 是一个通用的插入删除或替换.
  9. 数组遍历: 用for咯, 优化 定住len = keys.length,然后再用 i < len
  10. 判断 不要null和undefined用 !a[i] 不要undefined用 a[i] === undefined, 对于不存在的暂时不理解啊 in 还有for in, 但不推荐,因为for in 会遍历Array.prototype中的属性. es5中有forEach 按索引的顺序按个传, 每个都穿哦.
  11. 多维数组: 简单的用两次[]
  12. 数组方法: es3在Array.prototype中: Array.jion()转字符串, String.split()方法的逆向操作. reverse()颠倒, 替换策略, 原数组中重拍. sort(). concat().slice()返回一个片段(子数组). splice()是在数组中插入或删除后或替换(即删除+插入)元素的通用方法. 不会修改调用的数组concat().slice()这两个会. 前两个参数指定保留(或删除), 最后一个表示插入哪些. 会插入数组本身splice(2,2,[1,2],3) => [1,2,[1,2],3,3,4,5] 也就是说concat会提取数组中的元素呢. push和pop 当栈. shift和unshift当队列, 嗯是push+shift 尾进头出. 都修改原数组哦. 注意unshift的多参数形式, 会一次性插入的. 而不是一个个插入. 和splice一样. unshift(1,2) 和先unshift(1), 在unshift(2)不一样

toString和toLocaleString.: 数组和其他对象一样有这个, 将每个数组中的元素转成字符串. 有必要是用元素的toString 逗号分隔, 和不适用任何参数调用join一样.

ES5中的数组方法

共9个新的数组方法来遍历,映射, 过滤, 检验, 简化和搜索数组.

概述, 大多数的方法第一个参数接收一个函数, ,并且会对数组中的每一个元素调用一次这个函数. 然后调用使用的函数偶3个参数: 数组中元素, 数组中元素索引, 数组本身. 然后是方法的第二个参数: 这时, 第一个函数, 这个被调用的函数看成是第二个参数的方法, 相当于第二个参数是个this. 第二个参数来使用第一个参数. es5中的数组方法都不会修改他们调用的数组, 也就是会返回新数组啦. 当然第一个参数可以修改原数组. 感觉很没说一样.
什么叫map不修改元素组, map(()=>{}) 里面的方法修改原数组, 稍稍区别咯.

  1. forEach 从头到尾遍历数组, 为每个元素调用指定的函数.(所以不会返回新数组). 注意是强制都会遍历每个元素, 没有for的这种break. 不顾有一种方法,用try跑出异常.function(value) {x+2} 暂时就forEach没有return
  2. map: 同forEach (但return一个新数组),但 那个函数要有return的返回值 前面的forEach没有return function(value) {return x+2} 有没有return可以看出是不是返回一个新数组.
  3. filter 返回的是一个数组, 里面是原数组的子集. 过滤. 不动原数组, 而且总是返回稠密的.
  4. every+ some, 读这个数组做逻辑判断. 类似一个任意, 一个存在. 也会有停止的好处, some在判断遇到第一个true就直接返回true. every遇到false就直接返回false, 不会往后面去判断了. 数学书惯例, 空数组[]调用every时true, some是false. 暂时只要这两个会提前终止遍历.
  5. reduce和reduceRight: 使用指定函数将数组元素进行组合, 生成单个值. 成为注入或折叠. sum product max min. 要2个参数, 和前面不同, 第一个确实是执行操作的化简函数(和前面的map forEach不同, 多了一个参数), 但第二个是初始值(不指定初始值时,用数组的第一个元素当初始值, 不甜的话会从化简函数的第一个参数传过来的当初始值. 有没有这个初始值还有有点不同的, 注意哦. 再举个例子, 没填的话数组第一个元素1当做初始值, 然后从2开始为第一次调用化简函数传入的). 说下多参数,共4个, 第一个参数是多的,表示到目前为止的化简操作累积的结果, 后面三个和map一样.. 第一次调用时, 第一个函数参数是个初始值, 也就是外面第二个参数, (这里讲的很清楚, 说出来初始值, 化简函数有4个参数.)
1
2
3
4
const a = [1,2,3,4,5]
// 一下是第一次调用化简函数的时候, 传入的值.
a.reduce(function(x, y) {return x+y}, 0) //这个第一个x是0 然后y是数组a[0]: 1
a.reduce(function(x, y) {return x+y}) //这个第一个x是数组a[0]: 1, r然后y是a[1]: 2

空数组的问题[]: 直接调用reduce会报错, 如果[1]有个值, 或在[].reduce(, 2)制定了初始值, 就直接返回这个初始值, 不会调用化简函数.
当然他们也能接收一个this值咯,bind给某个特殊方法用.
reduce不仅在数值计算上, 也在交并集, 反正就是数学上, . reduce和reduceRight还是有点区别的.

  1. indexOf和lastIndexOf: 在数组中找给定值元素, 找到就返回索引,没有就-1. 和前面不同参数. 没有什么函数, 就传入要找的值,还有起始位置.

数组类型:

es5用Array.isArry() 判断是不是数组咯. typeof判断不了. instanceof的话会在多个frame中混淆.

1
2
3
4
var isArray = Function.isArray || function(o) {
return typeof o === 'object' &&
Object.prototype.toString.call(0) === '[Object Array]'
}

看得懂上面的了

类数组对象:

js数组一些特性是其他对象没有的:

  • length这个自动更新
  • length会删除元素
  • 从Array.prototype中继承一些有用的方法
  • 其类属性为Array

可以把拥有一个数值length属性和有对应非负整数属性的对象当做数组.

类数组虽然不能直接调用数组方法和length得到预期. 还是能用数组遍历的代码.

反正数组和对象的[]用法一样.
arguments和DOM操作得到的都是类数组对象. 怎么判断呢 用length是个有限的整数值判断. 没啥用啦.
反正es5中 所有的数组方法都是通用的,数组, 类数组都能用. concat的话例外哦. 类数组虽然没有继承来自Array.prototype但可以间接用Function.call啊 比如Array.prototype.map.call(), 所以常见到有这么用的.

作为数组的字符串:

字符串的行为类似只读数组(typeof还是string). 除了用charAt来访问单个字符 也用[]

s.charAt(0)
s[0]

反正不用charAt好了, 用[]挺好, 然后及时通用的数组方法可用在字符串上咯. 用Function.call 只要记住是只读 什么push sort reverse splice肯定不行

对象总结

对象是一个key: value的, 无序集合哦. 看成映射也行, 比如hash, dictionary, associative array.

对象是动态的, 可以新增和删除属性. 通过对象的可扩展性也可以定住.
引用

先说对象的3个属性和4个方法

原型prototype, 类class, 可扩展性extensible attribute

  • 原型: 以前常说的”o的原型”就是这个”o的原型属性”

通过对象字面量{} 使用Object.prototype 原型, new+构造函数创建的实例用构造函数的prototype作为他们的原型, Object.create()的第一个参数作为原型.

详细讲js原型的: javascript原型 结合比较

上面的而文章会用到prototype或__proto__属性获取获取对象原型.

es5中用Object.getPrototypeOf()方法来查找对象的原型,更正式.

记得这会有3个了,prototype或__proto__或Object.getPrototypeOf()
还有就是检测一个对象是不是另一个对象的原型, 用isPrototypeOf() ,原理功能上的话和instanceof类似

  • 类属性class attribute:是一个字符串,表达对象类型信息.es5中都没有提供这个方法, 只有一种简洁的方法可以查询他, 就是toString 这个和那个Symbol.toStringTag一起看

想要获得对象的类, 可以调用对象的toString()方法, 然后提取返回字符串的第8个到倒数第2个之间的字符.因为很多对象继承的toString()方法重写了, 为了能调用正确的toString, 所以必需要间接得调用Function.call()方法

1
Object.prototype.toString.call('foo').slice(8, -1)
  • 可扩展性extensible attribute:表示是否可以给对象添加新属性(可以删除哦). 所以内置对象和自定义对象都是显示可扩展的额,宿主对象的可扩展性由javascript引擎定义(反正也是可扩展的).

一共3个, 从对象属性覆盖到属性特性: Object.preventExtensions()和Object.seal()和Object.freeze() 都是只影响自有属性, 和继承属性无关. 从对像可扩展到属性的可配置再加上属性的可写性.

对象的Object.preventExtensions()和设置属性的configurable一样,一旦设置false就不能再次设置了.只还剩一个freeze的话还能改点可写性诶.

es5中通过将对象传入Object.isExtensible()来判断该对象是否是可扩展的, 想让对象不可扩展用Object.preventExtensions() ,不顾一旦变为不可扩展, 那就不能再变回来了. 而且注意这个只影响本对象自身的可扩展性. 不可扩展对象的属性可能仍然可被删除.
目的是为了lock, 避免外界干扰. 对象的可扩展性通常和属性的可配置型和可写性配合使用:也就是前面的规则.
还有个更进一步, Object.seal() 除了将对象设置为不可扩展, 还可以将对象的所有自有属性都设置为不可配置.(不能添加属性, 已有的属性也不能删除或修改), 已有的可写属性依旧可以设置. 也不能解封, 用Object.isSealed()判断
在进一步: Object.freeze()更严格地锁定对象 冻住了, 除了将对象设置为不可扩展和属性设置为不可配置外, 将他自有的所有数据属性都设置为只读, 存取器属性不受影响. 使用Object.idFrozen()判断

Object.preventExtensions()和Object.seal()和Object.freeze()这3这都返回传入的对象, 所以可以用该函数嵌套的方式调用他们.

4个方法

没啥用的toString(), toLocaleString(), toJSON, valueOf()

序列化对象

指将对象的状态转为字符串.也可以将字符串还原为对象, 这个不就是JSON.stringify()和JSON.parse()

在说对象的属性了

前面讲了3个object attribute, 现在讲下property attribute还有也是3个

es5中 对象的属性是任意字符串(不重名)+ getter和setter函数
然后出了key: value外, 每个属性还有一些和他相关的值, 叫属性特征. 3种:

  • writable(能设置改key的value么),
  • enumarable(能用for/in, Object.keys和JSON.stringify返回该key么),
  • configurable(能删除或修改属性, 但不包括对象属性的新增, 那是对象属性控制的),

es5以前都是可写可枚举,可配置哦

还有就是: 3类JS对象和2类属性

  • 内置对象 native object, ecma规定的对象或类: 数组, 函数, 日期, 正则
  • 宿主对象 host object , 由js解释器所嵌入的宿主环境定义. 如浏览器啊 node啊
  • 自定义对象 user-defined object, 你自己写代码的时候定义的
  • 自有属性 own property 直接在对象中定义的属性
  • 继承属性 inherited property 对象原型上定义的属性.

属性的特性: 一个属性包含1个名字和4个特性, 3方法Object.getOwnPropertyDescriptor(),Object.defineProperty()和Object.defineProperties()

除了名字和值外, 属性还包含一些他们可写writable,可枚举enumerable和可配置configurable的特性.

es5可以通过这些api给原型对象添加方法, 并将它们设置为不可枚举的, 这让他们看起来更像内置方法
也可以给对象定义不能修改或删除的属性, 借此’lock’这个对象

下面我们会讲存取器属性, 和数据特性. 这里可以把存取器属性getter和setter看成是属性的特性, 数据属性value也当做是属性的特性.
所以我们可以认为一个属性包含1个名字和4个特性: 名字是key, 4个特性: value, writable, enumerable, configurable

存取器属性不具有value和writable, 它的可写性由setter决定, 所以存取器属性的4个特性是get, set, enumerable, configurable.

为了实现属性特性的查询和设置操作, es5定义了一个property descriptor对象, 这个对象代表那4个特性.
所以数据属性的描述符对象的属性有value, writable, enumerable, configurable.
存取器属性的描述符对象则用get和set代替value和writable,
其中writable, enumerable, configurable是布尔值, get和set当然是函数值.

分别为添加数据属性, 添加存取器属性.

通过调用Object.getOwnPropertyDescriptor()可以获取某个对象特定属性的属性描述符.
getOwnPropertyDescriptor

只是对当前对象的自有属性, 如果是继承属性和不存在的属性就是undefined

像要继承属性的特性需要遍历原型链, 用Object.getPrototypeOf()找打指定对象的原型, 然后用Object.getOwnPropertyDescriptor()

接下来是设置属性的特性了, 用Object.defineProperty(), 传入要修改的对象, 要修改属性, 以及属性描述符对象.

枚举指的是能不能被for/in还要能不能被keys, 还有JSON.stringify 就是影响这3个函数结果.

只能读取对象本身的可枚举属性,并序列化为JSON对象。

而configurable指能不能再次通过Object.defineProperty()来设置咯

注意: 传入Object.defineProperty()里的属性描述符对象, 就是那第3个参数, 不必包含4个特性, 没传入的默认值当false或undefined. 而对于修改已有的属性特性, 不传入就是保持原来的. 这个不能修改继承属性,是要么新建自有属性, 要么修改已有属性

修改多个的话用Object.defineProperties(), 第二个参数变成一个映射表了.

可对象的扩展性extensible attribute和属性的可配置configurable的区别, 可扩展说的是能不能新建一个属性. 可配置将的是能不能再次修改这个属性的特性了, 分别是新建和设置

有个规则看下:

  • 如果对象是不可扩展的, 说的是可以编辑已有的自有属性, 但不能新建添加.
  • 如果属性是不可配置的, 不能修改它的可配置下和可枚举性. vale和writable可以改诶, 那get和setter也能改.
  • 如果存取器属性是不可配置的, 不能修改getter和setter, 也不能转成数据属性
  • 如果数据属性是是不可配置的, 则不能转换成存取器属性. 不能将她的可写性从false到true, 但可以从true到false
  • 如果数据属性是是不可配置的和不可写的, 就是上面说的. 则不能修改它的值. 然而可配置加上不可写的话,这个值是可以修改的.(实际上是先将他标位可写, 然后修改它的值, 然后转换为不可写)

复制对象的属性, 这个只是简单地复制属性名和值, 这里默认没有复制属性的特性,也没有复制存取器属性, 只是简单地转换为静态的数据属性.
所以要加上用Object.getOwnPropertyDescriptor()和Object.defineProperty() 记得要把这个方法加到Object.prototype中, 然后设为不可枚举哦.

属性函数getter和setter

对象的属性是有key:vlaue和一组特性构成的. 在es5中,属性值value可以用两个方法替代. 就是getter和setter 这个定义的属性也叫做存取器属性(accessor property). 不同于数据属性(data property)只有一个简单的值.

getter方法没有参数, 但返回一个表达式的值, setter方法传入参数,不需要返回值.
注意: 存取器属性是不具有可写性(writeble attribute), 存在getter和setter 就是可读写, 只有getter就是只读咯,只有setter就是只写咯. 然后另一个是undefined

用法最简单的就是用对象直接量语法的一种扩展写法.

1
2
3
4
5
var a = {
data_prop: value,
get accessor_prop() {},
set accessor_prop(value) {}
}

存取器属性定义为一个或两个同名的函数, 注意这个函数没有使用function关键字, 而是使用set和get 也没有啥冒号的把属性名和函数体分开.

js把这些函数当做对象的方法来调用, 写法我觉得类似class中的方法, 也有this
this用法可以看只在运行时确定 javascript中this指向由函数调用方式决定

和数据属性一样, 这个存取器属性可以继承的, 这个就像是一个对象中的方法么, 对当然可以继承.

getter挺好玩的, 比如给对象加一个取得随机数的方法.

对象常用的用法是:创建3, 查找和设置2, 删除1, 检测, 枚举

创建对象, 注意和原型结合看

3种方式创建: 对象直接量{}, new, Object.create()

主要是创建后的原型不同.

  • {}: 创建后原型就是Object.prototype

key的话如果用了保留字 比如for 要用"for" 套上, 还有"a b" "a-b"都可以,只要是字符串都行, 不过不建议啦.
注意: 对象直接量{}是一个表达式, 每次运行都会创建并初始化一个新的对象. 所以里面的key的value也都会计算一次,

  • new+构造函数: 这个很熟悉了, 看前面的原型: javascript原型

原型: 只有null是没有原型的. Object.prototype是最上层那个,也没有原型,其他都会有,
所有的对象直接量{}创建的原型是Object.prototype, 用new+构造函数创建的就是对应的构造函数.prototype (看图哦,原型中的 function Function注意点)

  • Object.create(): 创建一个对象, 传入的第一个参数是这个对象的原型. 就不用你prototype来手动创建, 然后用constructor, 主要是会自动带着constructor

Object.create()是一个静态函数, 不是提供给某个对象调用的方法, 所以可以直接调用.

注意 Object.create({})和Object.create(null)不一样. null的创建的是一个没有原型的新对象 哦,前面也说过, 只要null是没有原型的. 就是基础方法都没有, toString都不能用.
当然如果想创建一个空对象(有原型的), 比如像前面通过{} 或new Object()创建的对象. 用Object.create(Object.prototype)

null.png

这理一理顺, 还是很容易懂的 可以通过任意原型创建新对象, 就是可以使任意对象可继承.

属性的查询和设置

用. []来获取或设置属性的值. 运算符左边当然是一个对象咯.
.的右侧是一个属性名称的简单标识符
[]右侧是一个计算结果是字符串的表达式.这个字符串就是属性的名字

1
a.b  a.['b']

es5的['for']是允许的, 但不能用a.for, 关键字 保留字, 这也是点和[]的区别

再说下关联数组, 上面的.类似C或Java中访问结构体或对象的静态字段. []更像一个数组. 只是这个数组是通过字符串索引而不是数组索引, 这个就是关联数组(associative array, 或hash, dictionary).
js中的对象都是关联数组, 其那面说键值对啊, hash dictionary啊

.和[]区别
js是弱类型, 所以任何对象都可以创建任意数量的属性, (严格当然是有限的)
但当通过点.来访问对象的属性, 属性名通过一个标识符来表示, 标识符必须直接出现在js程序中, 他们不是js的数据类型, 所以程序不能修改他们(重点就是标识符不是数据类型)
而通过[]来访问对象的属性时, 属性名是字符串表示, 字符串是js的数据结构, 程序可以在运行时修改和创建他们.

标识符就是变量名字, 有个命名规范, 而字符串只要是””都可以啊
属性访问器
用Object.property的property就是一个js的标识符.
而object[property_name]
property_name 是一个字符串。该字符串不一定是一个合法的标识符;它可以是任意值,例如,”1foo”,”!bar!”, 甚至是一个空格。

一般动态都用[],而.是提前知道有这么个属性. 标识符是静态, 写死在程序中.

再说下前面的 own property和inherited property

给对象赋值只会当前的对象中设置, 查找的话先从本对象开始, 没找到就沿着继承来的往上找
只在查询时体会到继承的存在,设置因为只在原始对象上, 所以和继承无关
属性赋值不是设置属性哦, 这个属性赋值会先查询一遍原型链, 然后如果设置了只读, 不关在链上哪个原型上设置的, 那么都不能修改哦.

查询属性,设置属性,给属性赋值3点不同

属性访问错误

属性访问你可以返回一个值, 也可以设置一个值(当然设置时一般都是先访问到后才能设置).但也可能有错误啊.

a.sdfsdf 没有就返回undefined
但如果这个a对象不存在, 即: null或undefined的情况下,再去获取它的属性就是报错error了.
所以访问是用 book && book.name && book.name.length 这种方式

还有一些属性是可以访问,但不能设置, 只读的哦, 或者就是那个对象不允许新增属性. (这个和null undefined报错不同,这不报错诶, 在es5的严格模式中修复)

3点,设置失败报错:

  • o的属性p是只读的, (例外是defineProperty()方法中一个,或者是对象那个Object.freeze())
  • o的属性p是继承的, 然后是只读的, 不能通过同名啊自身属性来覆盖这个继承的只读属性.
  • o中不存在自有属性p,o没有使用setter方法继承属性p,并且o的可扩展性(extensible attribute)是false. 这里我有点懵逼 继承是通过setter函数实现的么?

删除属性, delete

delete 属性访问表达式 比如 delete b.name 或 delete b.['name']

这个有问题, 他只是断开属性和宿主对象的联系, 就像对象引用, 东西还在,只是引用的指向不在.(所以销毁对象的时候要遍历属性中的属性, 深度遍历哦)

delete也只能删除自有属性, 不能删除继承属性, 继承的要去那个对象上删除. 防止有其他对象也用到这个原型, 也继承呗

返回一个boolean
删除成功或删除不存在的,删除继承的属性, 删除无意义的 都返回true

1
2
3
4
delete o.x
delete o.x //什么也没做, 前一步删了, 不存在了
delete o.toString //什么也没做, 继承的
delete 1 //无意义

返回false的是: 不能删那些可配置性(configurable)为false的属性, 只要记这个就好, 反正其他都是true, 当没事发生或无意义

检测属性4种方法

js中的对象可以看成是属性的集合., 查一下某个属性是否存在于某个对象中,可以通过,in hasOwnProperty() propertyIsEnumerable 甚至只通过属性查询也可以做到这一点.

  • in : "x" in o 左侧属性名(字符串), 右侧对象.如果对象的自有属性或继承属性包含这个'x'则会返回true
  • 对象的hasOwnProperty()方法用来检测对象有没有这个自有属性, 不会去看继承的.
  • propertyIsEnumerable()是hasOwnProperty()的增强版, 只有检测到是自有属性并且是可枚举的才返回true

除了in 还要用!== undefined来判断有没有自有属性和继承属性. 不是!=
in只有一种场景很好用: 区分不存在的属性和存在但值为undefined的属性

枚举控制

以前检测对象属性的时候用for/in,遍历自有属性和继承属性
对象继承的内置方法不可遍历, 但在代码中给对象添加的属性都是可枚举的.(除非用property arrtibute设置为不可枚举)

es5前, 在许多实用库给Object.prototype添加了新方法或属性, 用for/in可以都枚举了, es5后可以设置为不可枚举.
也可以过滤下for/in返回的结果

es5还有2个枚举属性名称的函数Object.keys() 可枚举的自有属性和Object.getOwnPropertyNames() 返回自由属性, 包括枚举和不可枚举
这里和hasOwnProperty() 不一样哦, 一个是判断自有属性有么有这个, 一个是返回所有的自有属性
别忘了JSON.stringify()

javascript原型

发表于 2018-12-06 | 分类于 javascript教程

javascript 原型, 一文让你理解什么是 JS 原型

先看

javascript-原型继承

JavaScript Prototype(原型) 新手指南

对象创建有 2 种方式, 这里只看通过 new+构造函数的方式.

如果是通过字面量{}创建的,那么原型就是Object.prototype

对象字面量不过是new的语法糖, 可以看他们的 prototype

原型链图

先把这张图过一遍
prototype.png

typeof Object 是"function". 也称这样的对象为构造器(constructor)
因而,所有的构造器都是对象,但不是所有的对象都是构造器。
let n = new Function() 出来的是一个匿名函数诶.

1
2
3
4
5
6
7
8
9
// 这里看图可以得出
console.log(typeof(Function)) 'function'
console.log(typeof(new Function())) 'function' // 匿名函数, 不是对象
console.log(typeof(Object)) 'function'
console.log(typeof(Array)) 'function' // 这个可以补上图
// 这里就是返回对象了
console.log(typeof(new Array())) 'object'
console.log(typeof(new Date())) 'object'
console.log(typeof(new Object())) 'object'

理解 JavaScript 函数(函数和对象的区别和联系)

方法也是函数, 可以递归上去:

1
2
3
4
5
6
7
8
Function.prototype.method1 = function() {
console.log('function');
};
function func1(a, b, c) {
return a + b + c;
}
func1.method1();
func1.method1.method1();

JavaScript 深入之从原型到原型链
上图说原型链是__proto__这条路

1. 原型

1.1 传统构造函数的问题

通过自定义构造函数的方式,创建小狗对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log('汪汪汪');
};
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黄狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //输出结果为false

画个图理解下:
dog1.jpeg

每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。

为了避免内存的浪费,我们想要的其实是下图的效果:
dog2.jpeg

解决方法:
这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sayFn() {
console.log('汪汪汪');
}

function Dog(name, age) {
this.name = name;
this.age = age;
this.say = sayFn();
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黄狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //输出结果为true

这样写依然存在问题:

  • 全局变量增多,会增加引入框架命名冲突的风险
  • 代码结构混乱,会变得难以维护

想要解决上面的问题就需要用到构造函数的原型概念。

构造函数JavaScript Prototype(原型) 新手指南 中说到, new相当于注释了

1
2
3
4
5
6
7
function Animal(name, energy) {
let animal = Object.create(Animal.prototype);
animal.name = name;
animal.energy = energy;

return animal;
}

new 有一个很酷的地方——当您使用 new 关键字调用函数时,注释掉的这两行代码是隐式(引擎)完成的,创建的对象称为 this。

使用注释来显示在幕后发生的事情并假设使用 new 关键字调用 Animal 构造函数,可以将其重写为这样:

1
2
3
4
5
6
7
8
9
10
11
function Animal(name, energy) {
// const this = Object.create(Animal.prototype)

this.name = name;
this.energy = energy;

// return this
}

const leo = new Animal('Leo', 7);
const snoop = new Animal('Snoop', 10);

同样,这样做以及为我们创建 this 对象的原因是,我们使用 new 关键字调用构造函数。如果在调用函数时不使用 new ,则该对象永远不会创建,也不会隐式返回。我们可以在下面的例子中看到这个问题。

1
2
3
4
5
6
7
function Animal(name, energy) {
this.name = name;
this.energy = energy;
}

const leo = Animal('Leo', 7);
console.log(leo); // undefined

此模式的名称是 Pseudoclassical Instantiation(伪类实例化) 。

1.2 原型的概念

prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。

关键点:

  • 每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;
  • 当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;
  • 也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;
  • 访问原型的方式:构造函数名.prototype。

示例图:
p.png

示例代码: 给构造函数的原型添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dog(name, age) {
this.name = name;
this.age = age;
}

// 给构造函数的原型 添加say方法
Dog.prototype.say = function() {
console.log('汪汪汪');
};

var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黄狗', 0.5);

dog1.say(); // 汪汪汪
dog2.say(); // 汪汪汪

我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1、dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。

如图所示:
p2.png

在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。

1.3 原型的使用

1、使用对象的动态特性: 就是直接使用prototype为原型添加属性或者方法。

1
2
3
4
5
6
7
8
9
10
11
function Person() {}

Person.prototype.say = function() {
console.log('讲了一句话');
};

Person.prototype.age = 18;

var p = new Person();
p.say(); // 讲了一句话
console.log(p.age); // 18

2、直接替换原型对象, (空对象)

每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。

1
2
3
4
5
6
7
8
9
10
function Person() {}

Person.prototype = {
say: function() {
console.log('讲了一句话');
}
};

var p = new Person();
p.say(); // 讲了一句话

注意: 使用原型的时候,有4个注意点需要注意一下,我们通过几个案例来了解一下。

1 使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,再去原型中查找;(所以只要查询才能体会到原型链的存在, 后面的设置属性是和原型链无关的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个英雄的构造函数 它有自己的 name 和 age 属性
function Hero() {
this.name = '德玛西亚之力';
this.age = 18;
}
// 给这个构造函数的原型对象添加方法和属性
Hero.prototype.age = 30;
Hero.prototype.say = function() {
console.log('人在塔在!!!');
};

var h1 = new Hero();
h1.say(); // 先去自身中找 say 方法,没有再去原型中查找 打印:'人在塔在!!!'
console.log(p1.name); // "德玛西亚之力"
console.log(p1.age); // 18 先去自身中找 age 属性,有的话就不去原型中找了

2 使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;(注意:这里能不能添加修改原型上有的属性, 看这个原型上的属性允不允许赋值 比如内置构造函数的原型是只读的Object.prototype = o 不行的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个英雄的构造函数
function Hero() {
this.name = '德玛西亚之力';
}
// 给这个构造函数的原型对象添加方法和属性
Hero.prototype.age = 18;

var h1 = new Hero();
console.log(h1); // {name:"德玛西亚之力"}
console.log(h1.age); // 18

h1.age = 30; // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作
console.log(h1); // {name:"德玛西亚之力",age:30}
console.log(h1.age); // 30

3 一般情况下,不会将属性放在原型中,只会将方法放在原型中;
4 在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!

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
// 创建一个英雄的构造函数 它有自己的 name 属性
function Hero() {
this.name = '德玛西亚之力';
}
// 给这个构造函数的默认原型对象添加 say 方法
Hero.prototype.say = function() {
console.log('人在塔在!!!');
};

var h1 = new Hero();
console.log(h1); // {name:"德玛西亚之力"}
h1.say(); // '人在塔在!!!'

// 开辟一个命名空间 obj,里面有个 kill 方法
var obj = {
kill: function() {
console.log('大宝剑');
}
};

// 将创建的 obj 对象替换原本的原型对象
Hero.prototype = obj;

var h2 = new Hero();

h1.say(); // '人在塔在!!!'
h2.say(); // 报错

h1.kill(); // 报错
h2.kill(); // '大宝剑'

图:
p3.png

图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。

1.4 __proto__属性 (前面图中也有显示)

私有属性和非标准属性

在 js 中以_开头的属性名为 js 的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。

1、构造函数的 prototype 属性

之前我们访问构造函数原型对象的时候,使用的是prototype属性:

1
2
3
function Person() {}
//通过构造函数的原型属性prototype可以直接访问原型
Person.prototype;

在之前我们是无法通过构造函数new出来的对象访问原型的:

1
2
3
4
function Person() {}

var p = new Person();
//以前不能直接通过p来访问原型对象

2、实例对象的 __proto__ 属性

其次是__proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。

__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。

1
2
3
4
5
6
7
8
9
function Person() {}
var p = new Person();

//实例对象的__proto__属性可以方便的访问到原型对象
p.__proto__;

//既然使用构造函数的`prototype`和实例对象的`__proto__`属性都可以访问原型对象
//就有如下结论
p.__proto__ === Person.prototype;

如图所示: 也就是最开始的那张图中的从实例到原型那条
proto.png

3、__proto__属性的用途, 调试用

  • 可以用来访问原型;
  • 在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;
  • 在调试过程中,可以轻易的查看原型的成员;
  • 由于兼容性问题,不推荐使用。

    1.5 constuctor属性, 指向构造函数, 用构造函数的name (构造函数的name就表示构造函数的类型)

constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。

1
2
3
4
5
6
7
8
9
function Dog() {
this.name = 'husky';
}

var d = new Dog();

// 获取构造函数
console.log(Dog.prototype.constructor); // 打印构造函数 Dog
console.log(d.__proto__.constructor); // 打印构造函数 Dog

如图所示:
constructor.png

获取复杂类型的数据类型:

通过obj.constructor.name的方式(简写),获取当前对象obj的数据类型。

在一个的函数中,有个返回值name,它表示的是当前函数的函数名;

1
2
3
4
5
6
7
8
9
10
function Teacher(name, age) {
this.name = name;
this.age = age;
}

var teacher = new Teacher();

// 假使我们只知道一个对象teacher,如何获取它的类型呢?
console.log(teacher.__proto__.constructor.name); // Teacher
console.log(teacher.constructor.name); // Teacher

实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.__proto__,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name。

1.6 原型继承

原型继承:每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。

继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。

1、简单遍历继承:

直接遍历一个对象,将所有的属性和方法加到另一对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var animal = {
name: 'Animal',
sex: 'male',
age: 5,
bark: function() {
console.log('Animal bark');
}
};

var dog = {};

for (var k in animal) {
dog[k] = animal[k];
}

console.log(dog); // 打印的对象与animal一模一样

缺点:只能一个对象继承自另一个对象,代码复用太低了。

2、混入式原型继承:

混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj = {
name: 'zs',
age: 19,
sex: 'male'
};

function Person() {
this.weight = 50;
}

for (var k in obj) {
// 将obj里面的所有属性添加到 构造函数 Person 的原型中
Person.prototype[k] = obj[k];
}

var p1 = new Person();
var p2 = new Person();
var p3 = new Person();

console.log(p1.name); // 'zs'
console.log(p2.age); // 19
console.log(p3.sex); // 'male'

面向对象思想封装一个原型继承:

我们可以利用面向对象的思想,将面向过程进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Dog() {
this.type = 'yellow Dog';
}

// 给构造函数 Dog 添加一个方法 extend
Dog.prototype.extend = function(obj) {
// 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法
for (var k in obj) {
this[k] = obj[k];
}
};

// 调用 extend 方法
Dog.prototype.extend({
name: '二哈',
age: '1.5',
sex: '公',
bark: function() {
console.log('汪汪汪');
}
});

3、替换式原型继承:

替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person() {
this.weight = 50;
}

var obj = {
name: 'zs',
age: 19,
sex: 'male'
};
// 将一个构造函数的原型对象替换成另一个对象
Person.prototype = obj;

var p1 = new Person();
var p2 = new Person();
var p3 = new Person();

console.log(p1.name); // 'zs'
console.log(p2.age); // 19
console.log(p3.sex); // 'male'

之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。

替换式原型继承时的 bug:

替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。

p4.png

解决方法:手动关联一个constructor属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {
this.weight = 50;
}

var obj = {
name: 'zs',
age: 19,
sex: 'male'
};
// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数
obj.constructor = Person;

// 将一个构造函数的原型对象替换成另一个对象
Person.prototype = obj;

var p1 = new Person();

console.log(p1.__proto__.constructor === Person); // true

4、Object.create()方法实现原型继承:

当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:__proto__,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()。

语法: var obj1 = Object.create(原型对象);

示例代码: 让空对象obj1继承对象obj的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
name: '盖伦',
age: 25,
skill: function() {
console.log('大宝剑');
}
};

// 这个方法会帮我们创建一个原型是 obj 的对象
var obj1 = Object.create(obj);

console.log(obj1.name); // "盖伦"
obj1.skill(); // "大宝剑"

兼容性(暂时不考虑):

由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。
利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 封装一个能力检测函数
function create(obj) {
// 判断,如果浏览器有 Object.create 方法的时候
if (Object.create) {
return Object.create(obj);
} else {
// 创建构造函数 Fun
function Fun() {}
Fun.prototype = obj;
return new Fun();
}
}

var hero = {
name: '盖伦',
age: 25,
skill: function() {
console.log('大宝剑');
}
};

var hero1 = create(hero);
console.log(hero1.name); // "盖伦"
console.log(hero1.__proto__ == hero); // true

2.原型链

对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链

2.1 什么是原型链

示例代码: 原型继承练习

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
// 创建一个 Animal 构造函数
function Animal() {
this.weight = 50;
this.eat = function() {
console.log('蜂蜜蜂蜜');
};
}

// 实例化一个 animal 对象
var animal = new Animal();

// 创建一个 Preson 构造函数
function Person() {
this.name = 'zs';
this.tool = function() {
console.log('菜刀');
};
}

// 让 Person 继承 animal (替换原型对象)
Person.prototype = animal;

// 实例化一个 p 对象
var p = new Person();

// 创建一个 Student 构造函数
function Student() {
this.score = 100;
this.clickCode = function() {
console.log('啪啪啪');
};
}

// 让 Student 继承 p (替换原型对象)
Student.prototype = p;

//实例化一个 student 对象
var student = new Student();

console.log(student); // 打印 {score:100,clickCode:fn}

// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的
console.log(student.weight); // 50
student.eat(); // 蜂蜜蜂蜜
student.tool(); // 菜刀

如图所示:

我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。

p5.png

完整的原型链:

结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?

我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个 Animal 构造函数
function Animal() {
this.weight = 50;
this.eat = function() {
console.log('蜂蜜蜂蜜');
};
}

// 实例化一个 animal 对象
var animal = new Animal();

console.log(animal.__proto__); // {}
console.log(animal.__proto__.__proto__); // {}
console.log(animal.__proto__.__proto__.constructor); // function Object(){}
console.log(animal.__proto__.__proto__.__proto__); // null

p6.png

2.2 原型链的拓展

1、描述出数组[]的原型链结构:

1
2
3
4
5
6
7
8
9
10
11
// 创建一个数组
var arr = new Array();

// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:
console.log(Array.prototype); // 打印出来还是一个空数组

// 我们可以继续往上找
console.log(Array.prototype.__proto__); // 空对象

// 继续
console.log(Array.prototype.__proto__.__proto__); // null

如图所示:
p7

2、扩展内置对象:

给 js 原有的内置对象,添加新的功能。
注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。

错误的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 第一个开发人员给 Array 原型添加了一个 say 方法
Array.prototype.say = function() {
console.log('哈哈哈');
};

// 第二个开发人员也给 Array 原型添加了一个 say 方法
Array.prototype.say = function() {
console.log('啪啪啪');
};

var arr = new Array();

arr.say(); // 打印 “啪啪啪” 前面写的会被覆盖

为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个数组对象 这个数组对象继承了所有数组中的方法
var arr = new Array();

// 创建一个属于自己的构造函数
function MyArray() {}

// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法
MyArray.prototype = arr;

// 现在可以单独的给自己创建的构造函数的原型添加自己的方法
MyArray.prototype.say = function() {
console.log('这是我自己添加的say方法');
};

var arr1 = new MyArray();

arr1.push(1); // 创建的 arr1 对象可以使用数组的方法
arr1.say(); // 也可以使用自己添加的方法 打印“这是我自己添加的say方法”
console.log(arr1); // [1]

2.3 属性的搜索原则(不是设置)

当通过对象名.属性名获取属性是 ,会遵循以下属性搜索的原则:

  1. 首先去对象自身属性中找,如果找到直接使用,
  2. 如果没找到去自己的原型中找,如果找到直接使用,
  3. 如果没找到,去原型的原型中继续找,找到直接使用,
  4. 如果没有会沿着原型不断向上查找,直到找到 null 为止。

关于查询和设置中, 如果遇到没有的属性, 报undefined和报错
比如没有x.ddd 是undefined, 如果在x.ddd.length则报错,而不是undefined, 所有 用 x.dd && x.dd.length

结论就是null和undefined无论是读取他们的属性或设置属性都是报错的.

参考

一文让你理解什么是 JS 原型
JavaScript instanceof 运算符深入剖析

React项目结构和组件命名之道

发表于 2018-11-28 | 分类于 react

React项目结构和组件命名之道

React项目结构和组件命名之道

不错,

显示项目结构

从都放User下到, 新建一个文件夹

1
2
3
4
5
src
└─ components
└─ User
├─ Form.jsx
└─ List.jsx
1
2
3
4
5
6
7
8
src
└─ components
└─ User
├─ Form
│ ├─ Form.spec.jsx
│ ├─ Form.jsx
│ └─ Form.css
└─ List.jsx

再是组件命名, 这里我们说的是如何命名我们的 class 或者定义组件的常量

2种哦,

采用基于路径的组件命名方式,

components/User/List.jsx,那么它就被命名为 UserList。

如果文件名和文件目录名相同,我们不需要重复这个名字。也就是说,components/User/Form/Form.jsx 会命名为 UserForm 而不是 UserFormForm。

好处:

  1. 方便在vsc中用cmd+p搜
  2. 目录树中定位
  3. 避免引入import时重名
    1. 改进命名

遵循这种方式,你可以根据组件的上下文环境来命名文件。想一下上面的 form 组件,我们知道它是一个 User 模块下的 form 组件,但是既然我们已经把 form 组件放在了 User 模块的目录下,我们就不需要在 form 组件的文件名上重复 user 这个单词,使用 Form.jsx 就可以了。

我最初使用 React 的时候喜欢用完整的名字来命名文件,但是这样会导致相同的部分重复太多次,同时引入时的路径太长。来看看这两种方式的区别:

1
2
3
import ScreensUserForm from './screens/User/UserForm';
// vs
import ScreensUserForm from './screens/User/Form';

我们认为根据组件文件的上下文环境以及它的相对路径来命名是更好的方式.

页面

我们以 src 目录为根目录,将不同页面分散在不同文件夹中。因为它们是根据路由定义而不是模块来划分成组的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react';
import { Router } from 'react-router';
import { Redirect, Route, Switch } from 'react-router-dom';

import ScreensUserForm from './User/Form';
import ScreensUserList from './User/List';

const ScreensRoot = () => (
<Router>
<Switch>
<Route path="/user/list" component={ScreensUserList} />
<Route path="/user/create" component={ScreensUserForm} />
<Route path="/user/:id" component={ScreensUserForm} />
</Switch>
</Router>
);

export default ScreensRoot;

注意我们将所有页面都放在同一个目录下,这个目录以路由名称命名。
这种方式使你看一眼 url 就能够轻松定位当前路由渲染的页面。

mockjs简单用法

发表于 2018-11-28 | 分类于 javascript教程

mockjs简单用法

安装

用npm install mockjs好了

语法规范

Mock.js 的语法规范包括两部分:

  1. 数据模板定义规范(Data Template Definition,DTD)
  2. 数据占位符定义规范(Data Placeholder Definition,DPD)

数据模板定义规范 DTD 我只用到这个

数据模板中的每个属性由 3 部分构成:属性名、生成规则、属性值:

1
2
3
4
// 属性名   name
// 生成规则 rule
// 属性值 value
'name|rule': value //有个'

注意:

  • name 和 rule 之间用竖线 | 分隔。 用' 包上
  • rule 是可选的。在函数和正则那里没有用而已
  • rule 有 7 种格式:
    1. ‘name|min-max’: value
    2. ‘name|count’: value
    3. ‘name|min-max.dmin-dmax’: value
    4. ‘name|min-max.dcount’: value
    5. ‘name|count.dmin-dmax’: value
    6. ‘name|count.dcount’: value
    7. ‘name|+step’: value

rule 的 含义 需要依赖 value的类型 才能确定。
value 还指定了最终值的初始值和类型
value 中可以含有 @占位符。

格式中看的 rule看count和min-max 然后就是组合下, 多个+step

生成规则和示例:

1. 属性值是字符串 String

1
'name|count': string        //+step当做count

通过重复 string 生成一个字符串,重复次数等于 count。

1
'name|min-max': string

这个也是重复 string 生成一个字符串,不过重复次数 min <= count <= max。

忽略这种, 只看整数部分

1
'name|min-max.dcount/dmin-dmax': string

2. 属性值是数字 Number

1
'name|+1': number

属性值自动加 1,初始值为 number(这里是初始值)。

1
'name|count': number

生成一个 等于count 的整数,属性值 number 只是用来确定类型(不是初始值哦)。

1
'name|min-max': number

同理生成一个 min <= x <= max 的整数,属性值 number 只是用来确定类型。

1
'name|min-max/count.dmin-dmax/dcount': number

生成一个浮点数,整数部分同理,小数部分产生count位, 决定小数位数

3. 属性值是布尔型 Boolean

1
'name|1': true

随机生成一个布尔值,值为 true 的概率是 1/2, 当然也有1/3, 1/4

1
'name|min-max': value   /value指的是true或false

随机生成一个布尔值,值为 value 的概率是 min / (min + max),值为 !value 的概率是 max / (min + max)。

忽略小数部分.

4. 属性值是对象 Object

1
'name|count': object
1
'name|min-max': object

从属性值 object 中随机选取 count 个属性。或者是min-max个

+step是直接把所有的属性列出, count太大也是把所有的列出.

5. 属性值是数组 Array

1
'name|1': array

从属性值 array 中随机选取 1 个元素,作为最终值。

1
'name|+1': array

从属性值 array 中顺序选取 1 个元素,作为最终值。 但+2/+3都是第一个

1
'name|count': array

通过重复属性值 array 生成一个新数组,重复次数为 count。 同理min-max

6. 属性值是函数 Function

1
'name': function

执行函数 function,取其返回值作为最终的属性值,函数的上下文为属性 'name' 所在的对象。

7. 属性值是正则表达式 RegExp

1
'name': regexp

根据正则表达式 regexp 反向生成可以匹配它的字符串。用于生成自定义格式的字符串。

Mock.mock()

Mock.mock( rurl?, rtype?, template|function( options ) )

Mock.mock( template )
根据数据模板生成模拟数据。

Mock.mock( rurl, template ) Mock.mock( rurl, function( options ) )

记录数据模板。当拦截到匹配 rurl 的 Ajax 请求时,将根据数据模板 template 生成模拟数据,并作为响应数据返回。

记录用于生成响应数据的函数。当拦截到匹配 rurl 的 Ajax 请求时,函数 function(options) 将被执行,并把执行结果作为响应数据返回。

Mock.mock( rurl, rtype, template ) Mock.mock( rurl, rtype, function( options ) )
记录数据模板。当拦截到匹配 rurl 和 rtype 的 Ajax 请求时,将根据数据模板 template 生成模拟数据,并作为响应数据返回。

记录用于生成响应数据的函数。当拦截到匹配 rurl 和 rtype 的 Ajax 请求时,函数 function(options) 将被执行,并把执行结果作为响应数据返回。

javascript中this指向由函数调用方式决定

发表于 2018-11-26 | 分类于 javascript教程

javascript 中 this 指向由函数调用方式决定

结论在先

JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的。换句话说,函数的调用方式决定了 this 指向

js 中,普通的函数调用方式有三种:直接调用、方法调用和 new 调用
还有些特殊的调用方式,比如通过 bind() 将函数绑定到对象之后再进行调用、通过 call()、apply() 进行调用
es6 引入了箭头函数
所以主要是分4 类: 直接调用(bind, call, apply)、方法调用、new 调用和 es6 的箭头函数.

this的指向,是在函数被调用的时候确定的, 而不是谁调用它,this就指向谁(这种从定义角度)
除此之外,在函数执行过程中,this一旦被确定,就不可更改了。
new 可以再看下 JavaScript Prototype(原型) 新手指南 为什么要用 new

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。

  1. 如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。
  2. 如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

从结论中我们可以看出,想要准确确定 this 指向,找到函数的调用者以及区分它是否是独立调用就变得十分关键。

1
2
3
4
5
6
7
8
// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
'use strict';
console.log(this);
}

fn(); // fn是调用者,独立调用
window.fn(); // fn是调用者,被window所拥有

在上面的简单例子中,fn()作为独立调用者,按照定义的理解,它内部的this指向就为undefined。
而window.fn()则因为fn被window所拥有,内部的this就指向了window对象。

前端基础进阶(五):全方位解读 this 666666

🌰 哦

1
2
3
4
5
6
7
8
9
10
11
var a = 20;
var obj = {
a: 10,
c: console.log(this.a + 20),
fn: function () {
console.log(this.a);
}
}

obj.c; //40 直接调用
obj.fn(); // 10 方法调用

上面你要区别是直接调用还是方法调用

即: 在demo中,对象obj中的c属性使用this.a + 20来计算。这里我们需要明确的一点是,单独的{}是不会形成新的作用域的,因此这里的this.a,由于并没有作用域的限制,所以它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。 再说这个对象obj中的c属性又不是函数.
而对象obj中的fn方法, 是局部作用域哦

要有作用域的概念才能搞定 this

区别下, 对象中的this的作用域

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
// 这是对象中的一个属性, 本质上还是在全局作用域下
var a = 30
var foo = {
a: 10,
b: this.a
}
console.log(foo.b); // 30

// 这是函数, 依旧在全局作用域下
function test() {
console.log(this);
}
console.log(test()); // window


// 对象中的函数
const obj = {
test() {
console.log(this === obj);
}
};
console.log(obj.test()) // true

var jj = obj.test
console.log(jj()) // false

再说下use strict下前面提到过的, this指向undefined而不是window或global
因为在实际开发中,现在基本已经全部采用严格模式了,而最新的 ES6,也是默认支持严格模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 'use strict'; 但加了这个后就 Uncaught TypeError: Cannot read property 'a' of undefined
var a = 20;
function foo () {
var a = 1;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
return obj.c; // 这里可以改下哦

}
console.log(foo()); // 40
console.log(window.foo()); // 40

再次点题

看是通过函数名(...)还是 对象.方法函数(...) 这样的调用形式

注意引用赋值的情况, 还有就是对象.属性这 2 种都还是global哦

直接调用, 只要是通过函数名(...), 不管是在什么作用域下

直接调用: 就是通过 函数名(...) 这种方式调用。这时候,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在 NodeJs 中全局对象是 global。

注意的一点是,直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用

可以看成不属于任何一个对象, 只属于全局对象
反正都是看指向上下文EC中的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 简单兼容浏览器和 NodeJs 的全局对象
const _global = typeof window === 'undefined' ? global : window;

function test() {
console.log(this === _global); // true
}

test(); // 直接调用

(function(_global) {
// 通过 IIFE 限定作用域

function test() {
console.log(this === _global); // true
}

test(); // 非全局作用域下的直接调用
})(typeof window === 'undefined' ? global : window);

bind() 对直接调用的影响, bind() 对函数的影响是深远的,慎用!

在 JavaScript 中,call、apply和bind是Function对象自带的

还有一点需要注意的是 bind() 的影响。Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其 this 始终指向绑定的新对象。

多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的, 只绑定第一个。

call 和 apply 对 this 的影响

call() 方法调用一个函数, 其具有一个指定的this值和提供的参数(参数的列表)。apply是一个数组[]

call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

使用 apply 和 call 的时候仍然需要注意,如果目录函数本身是一个绑定bind了 this 对象的函数,那 apply 和 call 不会像预期那样执行, 由此可见,bind() 对函数的影响是深远的,慎用!

方法调用

方法调用是指通过对象来调用其方法函数,它是 对象.方法函数(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是,同样需要注意 bind() 的影响

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
const obj = {
// 第一种方式,定义对象的时候定义其方法
test() {
console.log(this === obj);
}
};

// 第二种方式,对象定义好之后为其附加一个方法(函数表达式)
obj.test2 = function() {
console.log(this === obj);
};

// 第三种方式和第二种方式原理相同
// 是对象定义好之后为其附加一个方法(函数定义)
function t() {
console.log(this === obj);
}
obj.test3 = t;

// 这也是为对象附加一个方法函数
// 但是这个函数绑定了一个不是 obj 的其它对象
obj.test4 = function() {
console.log(this === obj);
}.bind({});

obj.test(); // true
obj.test2(); // true
obj.test3(); // true

// 受 bind() 影响,test4 中的 this 指向不是 obj
obj.test4(); // false

这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj 对象作为其方法。再次强调,函数内部的 this 指向与定义无关,受调用方式的影响。

方法中 this 指向全局对象的情况,而没有指向调用该方法的对象

注意这里说的是方法中而不是方法调用中。方法中的 this 指向全局对象(而没有指向调用该方法的对象),如果不是因为 bind(),那就一定是因为不是用的方法调用方式

1
2
3
4
5
6
7
8
const obj = {
test() {
console.log(this === obj);
}
};

const t = obj.test;
t(); // false

t 就是 obj 的 test 方法,但是 t() 调用时,其中的 this 指向了全局。

之所以要特别提出这种情况,主要是因为常常将一个对象方法作为回调传递给某个函数之后,却发现运行结果与预期不符——因为忽略了调用方式对 this 的影响。

提前说下箭头函数, 指向的是定义的时候的对象哦, 尤其是在使用闭包这种操作, 当然只有一层的话还是一样, 谁调用是谁
闭包如果用两个箭头函数实现呢???

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "windowsName";

var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
},100);
}

};

a.func2() // this.func1 is not a function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'windowsName';

var a = {
name: 'Cherry',

func1: function() {
console.log(this.name);
},

func2: function() {
setTimeout(() => {
this.func1();
}, 100);
}
};

a.func2(); // Cherry

都是这里的例子
this、apply、call、bind

JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

比如里面用_this = this用的是定义时上下文的this

a b c, a 是数组
b.apply(a,[1,2]) === b.call(a,1,2) === b.bind(a,1,2)()

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    // 这里 $button 假设是一个指向某个按钮的 jQuery 对象
constructor(data, $button) {
this.data = data;
$button.on("click", this.onButtonClick);
}

onButtonClick(e) {
console.log(this.data);
}
}

const handlers = new Handlers("string data", $("#someButton"));
// 对 #someButton 进行点击操作之后
// 输出 undefined
// 但预期是输出 string data

主要看$button.on这块

解决办法: 用bind() 或 es6 箭头函数

1
2
3
4
5
6
7
8
9
10
11
// 这是在 es5 中的解决办法之一
var _this = this;
$button.on('click', function() {
_this.onButtonClick();
});

// 也可以通过 bind() 来解决
$button.on('click', this.onButtonClick.bind(this));

// es6 中可以通过箭头函数来处理,在 jQuery 中慎用
$button.on('click', (e) => this.onButtonClick(e));

new 调用

在 es6 之前,每一个函数都可以当作是构造函数,通过 new 调用来产生新的对象(函数内无特定返回值的情况下)。而 es6 改变了这种状态,虽然 class 定义的类用 typeof 运算符得到的仍然是 "function",但它不能像普通函数一样直接调用;同时,class 中定义的方法函数,也不能当作构造函数用 new 来调用.

而在 es5 中,用 new 调用一个构造函数,会创建一个新对象,而其中的 this 就指向这个新对象。这没有什么悬念,因为 new 本身就是设计来创建新对象的。

一个new的过程

1
2
3
4
5
6
7
8
9
var a = new myFunction("Li","Cherry");

new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype; // 此时便建立了obj对象的原型链:
// obj->myFunction.prototype->Object.prototype->null
var result = myFunction.call(obj,"Li","Cherry"); // 相当于obj.myFunction("Li","Cherry")
return typeof result === 'object' ? result : obj; // 如果无返回值或者返回一个非对象值,则将obj返回作为新对象
}

结合原型链看 javascript原型

  1. 创建一个空对象 obj;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。

new 创建对象的过程发生了什么

箭头函数中的 this

MDN 上对箭头函数的说明 这里已经清楚了说明了,箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含它的那个函数或函数表达式中的 this。(就是外面的父)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const obj = {
test() {
const arrow = () => {
// 这里的 this 是 test() 中的 this,
// 由 test() 的调用方式决定
console.log(this === obj);
};
arrow();
},

getArrow() {
return () => {
// 这里的 this 是 getArrow() 中的 this,
// 由 getArrow() 的调用方式决定
console.log(this === obj);
};
}
};

obj.test(); // true

const arrow = obj.getArrow();
arrow(); // true

都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this 是由其调用方式决定的, 上例的调用方式都是方法调用,所以 this 都指向方法调用的对象,即 obj

箭头函数让大家在使用闭包的时候不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用

另外需要注意的是,箭头函数不能用 new 调用,不能 bind() 到某个对象(虽然 bind() 方法调用没问题,但是不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含它的最近的一层函数或函数表达式)绑定的 this。

更详细的用法

理解 JS 中的 call、apply、bind 方法

结合前文的执行上下文看

深入理解 JavaScript 系列(13):This? Yes,this! 666666

在 ECMAScript 中,this并不限于只用来指向新创建的对象。
让我们更详细的了解一下,在ECMAScript中this到底是什么?

定义

this是执行上下文中的一个属性:

1
2
3
4
activeExecutionContext = {
VO: {...},
this: thisValue
};

这里VO是我们前一章讨论的变量对象。

this与上下文中可执行代码的类型有直接关系,this值在进入上下文时确定,并且在上下文运行期间永久不变。

下面让我们更详细研究这些案例:

全局代码中的 this

在这里一切都简单。在全局代码中,this始终是全局对象本身,这样就有可能间接的引用到它了。

1
2
3
4
5
6
7
8
9
10
11
12
// 显示定义全局对象的属性
this.a = 10; // global.a = 10
console.log(a); // 10

// 通过赋值给一个无标示符隐式
b = 20;
console.log(this.b); // 20

// 也是通过变量声明隐式声明的
// 因为全局上下文的变量对象是全局对象自身
var c = 30;
console.log(this.c); // 30

函数代码中的 this

在函数代码中使用this时很有趣,这种情况很难且会导致很多问题。

这种类型的代码中,this值的首要特点(或许是最主要的)是它不是静态的绑定到一个函数。

正如我们上面曾提到的那样,this是进入上下文时确定,在一个函数代码中,这个值在每一次完全不同。

不管怎样,在代码运行时的this值是不变的,也就是说,因为它不是一个变量,就不可能为其分配一个新值(相反,在 Python 编程语言中,它明确的定义为对象本身,在运行期间可以不断改变)。

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
var foo = {x: 10};

var bar = {
x: 20,
test: function () {

console.log(this === bar); // true
console.log(this.x); // 20

this = foo; // 错误,任何时候不能改变this的值

console.log(this.x); // 如果不出错的话,应该是10,而不是20

}

};

// 在进入上下文的时候
// this被当成bar对象
// determined as "bar" object; why so - will
// be discussed below in detail

bar.test(); // true, 20

foo.test = bar.test;

// 不过,这里this依然不会是foo
// 尽管调用的是相同的function

foo.test(); // false, 10

那么,影响了函数代码中this值的变化有几个因素:

首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context)。this取决于调用函数的方式。

为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点。正是调用函数的方式影响了调用的上下文中的this值,没有别的什么(我们可以在一些文章,甚至是在关于 javascript 的书籍中看到,它们声称:“this值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。–这绝对不正确”)。
继续我们的话题,可以看到,即使是正常的全局函数也会被调用方式的不同形式激活,这些不同的调用方式导致了不同的this值。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this);
}

foo(); // global

console.log(foo === foo.prototype.constructor); // true

// 但是同一个function的不同的调用表达式,this是不同的

foo.prototype.constructor(); // foo.prototype

有可能作为一些对象定义的方法来调用函数,但是this将不会设置为这个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = {
bar: function () {
console.log(this);
console.log(this === foo);
}
};

foo.bar(); // foo, true

var exampleFunc = foo.bar;

console.log(exampleFunc === foo.bar); // true

// 再一次,同一个function的不同的调用表达式,this是不同的

exampleFunc(); // global, false

那么,调用函数的方式如何影响this值?为了充分理解this值的确定,需要详细分析其内部类型之一——引用类型(Reference type)。

引用类型(Reference type)

使用伪代码我们可以将引用类型的值可以表示为拥有两个属性的对象——base(即拥有属性的那个对象),和base中的propertyName 。

1
2
3
4
var valueOfReferenceType = {
base: <base object>,
propertyName: <property name>
};

引用类型的值只有两种情况:

  1. 当我们处理一个标示符时
  2. 或一个属性访问器

标示符的处理过程在下一篇文章里详细讨论,在这里我们只需要知道,在该算法的返回值中,总是一个引用类型的值(这对this来说很重要)。

标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值:

1
2
var foo = 10;
function bar() {}

在操作的中间结果中,引用类型对应的值如下:

1
2
3
4
5
6
7
8
9
var fooReference = {
base: global,
propertyName: 'foo'
};

var barReference = {
base: global,
propertyName: 'bar'
};

为了从引用类型中得到一个对象真正的值,伪代码中的GetValue方法可以做如下描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function GetValue(value) {

if (Type(value) != Reference) {
return value;
}

var base = GetBase(value);

if (base === null) {
throw new ReferenceError;
}

return base.[[Get]](GetPropertyName(value));

}

内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承的属性分析。

1
2
GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标示符,且事先知道),或括号语法([])。

1
2
foo.bar();
foo['bar']();

在中间计算的返回值中,我们有了引用类型的值。

1
2
3
4
5
6
var fooBarReference = {
base: foo,
propertyName: 'bar'
};

GetValue(fooBarReference); // function object "bar"

引用类型的值与函数上下文中的this值如何相关?——从最重要的意义上来说。 这个关联的过程是这篇文章的核心。

一个函数上下文中确定this值的通用规则如下:

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用括号()的左边是引用类型的值,this将设为引用类型值的base对象(base object),在其他情况下(与引用类型不同的任何其它属性),这个值为null。
不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。

注:第 5 版的 ECMAScript 中,已经不强迫转换成全局变量了,而是赋值为undefined。

我们看看这个例子中的表现:

1
2
3
4
5
function foo() {
return this;
}

foo(); // global

我们看到在调用括号的左边是一个引用类型值(因为foo是一个标示符)。

1
2
3
4
var fooReference = {
base: global,
propertyName: 'foo'
};

相应地,this也设置为引用类型的base对象。即全局对象。

同样,使用属性访问器:

1
2
3
4
5
6
7
var foo = {
bar: function () {
return this;
}
};

foo.bar(); // foo

我们再次拥有一个引用类型,其base是foo对象,在函数bar激活时用作this。

1
2
3
4
var fooBarReference = {
base: foo,
propertyName: 'bar'
};

但是,用另外一种形式激活相同的函数,我们得到其它的this值。

就是这里揭示了

1
2
var test = foo.bar;
test(); // global

因为test作为标示符,生成了引用类型的其他值,其base(全局对象)用作this 值。

1
2
3
4
var testReference = {
base: global,
propertyName: 'test'
};

现在,我们可以很明确的告诉你,为什么用表达式的不同形式激活同一个函数会不同的this值,答案在于引用类型(type Reference)不同的中间值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log(this);
}

foo(); // global, because

var fooReference = {
base: global,
propertyName: 'foo'
};

console.log(foo === foo.prototype.constructor); // true

// 另外一种形式的调用表达式

foo.prototype.constructor(); // foo.prototype, because

var fooPrototypeConstructorReference = {
base: foo.prototype,
propertyName: 'constructor'
};

另外一个通过调用方式动态确定this值的经典例子:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.bar);
}

var x = {bar: 10};
var y = {bar: 20};

x.test = foo;
y.test = foo;

x.test(); // 10
y.test(); // 20

函数调用和非引用类型

因此,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象。

让我们再思考这种表达式:

1
2
3
(function () {
console.log(this); // null => global
})();

在这个例子中,我们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。

更多复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var foo = {
bar: function () {
console.log(this);
}
};

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?

为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this值不是base对象,而是global对象?

问题在于后面的三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。

  1. 第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。
  2. 在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue。相应的,在组运算的返回中———我们得到仍是一个引用类型。这就是this值为什么再次设为base对象,即foo。
  3. 第三个例子中,与组运算符不同,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是global对象。
  4. 第四个和第五个也是一样——逗号运算符和逻辑运算符(OR)调用了GetValue 方法,相应地,我们失去了引用而得到了函数。并再次设为global。

到这里我已经看不懂了, 所以直接看链接中的吧

其他 this 有个公式

源于 call,

S 中的箭头函数与 this 1
this 的值到底是什么?一次说清楚 666666

至此我们的函数调用只有一种形式:那怎么判断这个 context 呢

1
func.call(context, p1, p2);

规则就是如下转换

1
2
3
4
5
func(p1, p2) 等价于
func.call(undefined, p1, p2) // 转成 undefined

obj.child.method(p1, p2) 等价于
obj.child.method.call(obj.child, p1, p2) // 转成 obj.child, obj1.obj2.obj3.method是看obj3最后一个

箭头函数中的 this 666 记住是继承而来的就行
箭头函数 MDN
箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this

看你不知道的js

this在严格下变undefined的问题, 也和调用位置无关的, 严格模式是可以设位置的.

参考

1. JavaScript 的 this 指向问题深度解析
this、apply、call、bind
new 创建对象的过程发生了什么
深入浅出 妙用 Javascript 中 apply、call、bind
理解 JS 中的 call、apply、bind 方法
深入理解 JavaScript 系列(13):This? Yes,this! 666666
前端基础进阶(五):全方位解读 this 666666

Understanding JavaScript Function Invocation and “this” 666666

Annotated ECMAScript 5.1

async-function-await

发表于 2018-11-25 | 分类于 javascript教程

async-function-await

语法

1
async function name([param[, param[, ... param]]]) { statements }

返回值

一个返回的Promise对象会以async function的返回值进行解析(resolved),或者以该函数抛出的异常进行回绝(rejected)。

描述

当调用一个 async 函数时,会返回一个 Promise 对象。

  1. 当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;
  2. 当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。

async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析值(resolved或reject)。

注意, await 关键字仅仅在 async function中有效。如果在 async function函数体外使用 await (没有在async中使用),你只会得到一个语法错误(SyntaxError)。

async/await的用途是简化使用 promises 异步调用的操作,并对一组 Promises执行某些操作。
正如Promises类似于结构化回调,async/await类似于组合生成器和 promises。

不要将await和Promise.then混淆

示例

通过async方法重写 promise 链

返回 Promise的 API 将会被用于 promise 链,它会将函数分成若干部分。例如下面代码:

1
2
3
4
5
6
7
8
9
function getProcessedData(url) {
return downloadData(url) // 返回一个 promise 对象
.catch(e => {
return downloadFallbackData(url) // 返回一个 promise 对象
})
.then(v => {
return processDataInWorker(v); // 返回一个 promise 对象
});
}

可以通过如下所示的一个async函数重写:

1
2
3
4
5
6
7
8
9
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v); //这里隐式
}

注意,在上述示例中,return 语句中没有 await 操作符,因为 async function的返回值将被隐式地传递给Promise.resolve。

await

await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。

语法

1
[return_value] = await expression;

表达式
一个 Promise 对象或者任何要等待的值。
返回值
返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。

描述

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。

  1. 若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。
  2. 若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。 (await不会包装值为Promise)

JavaScript-的-async-await

发表于 2018-11-25 | 分类于 javascript教程

JavaScript-的-async-await

第一阶段, 知道 await 当异步为同步,
第二阶段, 知道了 event loop 后知道 await 是 promsie 的语法糖.
第三阶段, 竟然再执行 await 下面的语句之前, 会执行 async 外的同步语句, 这个 怎么和Promose的 then结合理解呢?. 可不可以理解为 await 下面的语句, 是类似 Promise的 .then 的语句哦
第四阶段, 竟然导致微队列先执行了

从输入URL到页面加载发生了什么

结论放开头

  1. async function只是用来返回一个Promise对象或者要执行await时包上为了达到async function不阻塞的效果.(并是不说async里面一定要有await),绝不会阻塞后面的语句,整个一个async function 不会阻塞哦, 且返回的是Promise.(不要和里面的await遇到Promise阻塞搞混),而且await不会包装值为Promise
    1. —- 区别async会返回一个Promise, 不阻塞, await就算接收了Promise也只返回里面的值, 阻塞.
  2. await是用来在async functnion中等待执行一个表达式(expression),而且只能在async function中使用, 可以等的是普通的函数(那就当啥事没有,正常往下同步执行呗), 当然重点是说等着Promsie, 保证来的如果是Promise对象, 那么一定会保证先等这个promise搞定了(阻塞),再往下执行代码.(await 必须用在 async 函数中的原因就是为了async function不阻塞),注意:await不会包装值为Promise 和 then 不一样
  3. async/await 的优势在于优化处理 then 链以及对比Promise更清晰的传递参数
  4. 优化点, 处理await的时候最好用try...catch住,或者用.catch 防止Promise变为reject
  5. 处理多个的时候用Promise.all(),并且传入数组[] , 切记不要用forEach,虽然forEach可能让他们并发执行
  6. 至于await用不用, 看这一步的await会不会对后面的产生影响. 并不是说子的函数就搞定了. 他还是promise,pending的

建议再看下多进程浏览器, 多线程渲染进程这个 event loop 从输入URL到页面加载发生了什么

看了 event loop 后知道宏任务和微任务, 再然后是 await 和 promise 的关系

实际上,async/await 在底层转换成了 promise 和 then 回调函数。也就是说,这是 promise 的语法糖。每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中(这不就是先new promise中的’同步’ 这个同步指伪的,, 然后then中的微队列么)。 这个我觉得就是重点, 理解语法糖结构

从 event loop 到 async await 来了解事件循环机制 6666669

重点以前的误解(注意右边, 下面, 外面的措辞)

在async函数中遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。接着执行async函数外的同步代码, 然后再回到await右边的语句(如果有then(是个 promise)就执行完当前宏任务后的微队列), 最后才往await下面执行.

  • await后面如果不是 promise 对象, await会阻塞下面的代码,先执行await右边表达式中的同步代码. 再执行当前async函数外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果, 然后执行await下面的代码, (后面存在微队列的话, 再去执行微队列的, 但微队列不会提前到await下面的语句执行前执行)。 微队列一般指 then 这个语句
  • await后面如果是 promise 对象,await 也会暂停async函数中后面的代码(await下面的语句),但要先执行当前async函数外面的同步代码,(这里注意哦, 如果await后的表达式有同步的代码, 先执行这个, 有加入微队列的加入微队列, 再执行外面的同步代码. 外面的同步代码可能会有推入微队列的操作), ( 然后再执行 await 后面, 微队列 会提前执行, 记得会把 async 外面加入微队列 的代码一起执行了, 再执行 await 下面的代码 )
    • 这里重点记忆下: 然后等着await右边的 Promise 对象 fulfilled,再把 resolve 的参数作为 await 右边表达式的运算结果。(如果async函数外面的同步代码存在then这种微队列中的, 那么微队列会提前执行了.), 最后再执行await下面的代码
    • 先执行await右边表达式同步代码, 然后执行async外面同步代码, 然后执行then微队列, 最后返回 data, 最后在执行 await 下面的语句
    • 这样就导致微队列中的代码提前执行了
  1. 就是注意 执行await后面的表达式后, 不会立即执行await下面的, 而是先执行async外面的同步代码
  2. 一般上面没啥问题, 就是注意如果执行await后面的表达式后面是一个promise, 那么这个会把微队列也先执行掉
    如果await后面是是Promise的, 那么会提前执行微队列, 注意再提前也是要先执行完async外的同步代码哦

从输入URL到页面加载发生了什么, 从输入URL到页面加载发生了什么从输入URL到页面加载发生了什么 重要的链接看三遍哦

在说下结论: 不需要把上面的 await 特别理解, 只需要动 event loop 然后知道 微队列 常见的是 promise的 then. 而 await 说的分右边的表达式是不是 promise 的, 在去 async 外执行 这个. 直接把 await 的当 promise 的, 下面的语句当做 then 的内容, 所以才会顺序执行.
直接把 await 这条语句当 promise , 下面的语句当做 then 的内容, 所以才会顺序执行. 所以才会要执行 async 的同步语句先.
直接把 await 这条语句当 promise , 下面的语句当做 then 的内容, 所以才会顺序执行. 所以才会要执行 async 的同步语句先.
直接把 await 这条语句当 promise , 下面的语句当做 then 的内容, 所以才会顺序执行. 所以才会要执行 async 的同步语句先.

想想怎么把 await 转成 promise 在思考这个顺序, 反正 await 并不是 .then 因为两者返回值类型不同, .then会包着 Promise 的

反正 await 返回一个值, 但右侧不必要一定是 promise 类型的, async 的函数 可以接 then

还要注意

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
setTimeout(function () {
console.log('6')
}, 0)
console.log('1')

async function async1() {
console.log('2')
await async2()
console.log('5')
}
async function async2() {
console.log('3')
}

async1()
console.log('4')


// 果然可以理解为 await 后面的语句当做 then的, 所以可以顺序执行
setTimeout(function () {
console.log('6')
}, 0)
console.log('1')

function async1() {
console.log('2')
new Promise( (res, rej) => {
console.log('3')
res()
}).then(() => {
console.log('5')
})
}

async1()
console.log('4')
  1. 6 是宏任务在下一轮事件循环执行
  2. 先同步输出 1,然后调用了 async1(),输出 2。
  3. await async2() 会先运行 async2(),5 进入等待状态。
  4. 输出 3,这个时候先执行 async 函数外的同步代码输出 4。
  5. 最后 await 拿到等待的结果继续往下执行输出 5。
  6. 进入第二轮事件循环输出 6。

上面没啥问题, 下面的第一个例子也没啥问题, await右边不是 promise, 重点看第二个例子await右边是 promise, 然后async函数外的同步代码又会推入微队列

再一次理解: 果然可以理解为 await 后面的语句当做 then 的, 所以可以顺序执行

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
async function async1() {
console.log("2");
await async2();
console.log("7");
}

async function async2() {
console.log("3");
}

console.log("1");
async1();

new Promise(function(resolve) {
console.log("4");
resolve();
}).then(function() {
console.log("6");
});
console.log("5");
// 1 2 3 4 5 7 6

// 这个例子更体现了 await 后面的语句当做 then, 先去执行 async 外面的同步语句哦
async function async1() {
console.log("2");
await async2();
console.log("8");
}
async function async2() { // 这个函数不同, 导致比上一个例子中,7比8先出. 本来then的7是微队列, 要放到8后面才输出, DNA因为这里是这个await要等到所有promise的then也执行完才能执行打印8 , 所以这个导致后面的7页输出了
return new Promise(function(resolve) {
console.log("3");
resolve();
}).then(function() { // 这里可以去了看看会不会先执行微队列, 输出7 , 最后输出8
console.log("6");
});
}

console.log("1");
async1();

new Promise(function(resolve) {
console.log("4");
resolve();
}).then(function() {
console.log("7");
});
console.log("5");
// 1 2 3 4 5 6 7 8

这里再补充 resolve 中的

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
async function async1() {
console.log('2');
await async2();
console.log('8');
}
async function async2() {
return new Promise(function(resolve) {
console.log('3');
resolve('--- when ---');
}).then(function(res) {
console.log('6');
console.log(res); // resolve --when-- 中的传到这里哦, 并不是同步哦
});
}

console.log('1');
async1();

new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('7');
});
console.log('5');

// 1 2 3 4 5 6 -- when -- 7 8

如果把 await 换了, 这里没有把原来 await 后面的语句用 then 包上, 所有顺序有错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function async1() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
// 这里可以去了看看会不会先执行微队列, 输出7 , 最后输出8
console.log('6');
});
console.log('8');
}

console.log('1');
async1();

new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('7');
});
console.log('5');
// 1 2 3 8 4 5 6 7

扩展, ajax, node 的时间循环, promise, async/await

说下 async 起的作用

这个问题关键在于, async函数如何处理它的返回值. 在普通的function前加async的效果是啥

我们可以直接通过return来返回我们想要的值(没有返回值就是返回undefined), 这里就可以知道,async里面有没有await并没有多大关系

代码如下

1
2
3
4
5
6
async function testAsync() {
return 'hello async';
}

const result = testAsync();
console.log(result);

async.png

看到输出结果就知道了, 输出的就是一个Promise对象.还是resolve状态的.

所以, async函数返回的是一个Promise对象, 在MDN 文档 async function中可以知道. 就像上面如果是一个直接量'hello async' 那就把这个通过Promsie.resolve()封装成Promise对象.

所以在外层如果不用await获取其返回器的情况下, 当然我们就用Promise的.then链式来处理咯.

1
2
3
testAsync().then((v) => {
console.log(v); // 输出 hello async
});

当然async没有返回值就是返回Promise.resolve(undefined)咯.

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行(和await阻塞后面的语句不一样啦),返回一个 Promise 对象,并且,绝不会阻塞后面的语句, 绝不会阻塞后面的语句, 绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

无等待就是直接返回一个 promise 啊

那么async有啥用呢, await又在等个啥

await等啥呢,当然重要的是等Promise, 其他的也可以等

按我们以前片面的理解,await等待的是一个async函数的完成. 从前面到这里我们知道,那么等待的是一个Promise对象咯, 但从MDN await中可以知道, await等待的是一个expression. 所以这个表达式可以是Promise对象,当然也可以是其他值咯

如下的例子,await后面可以是直接量,也可以是Promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getSomething() {
return 'something';
}

async function testAsync() {
return Promise.resolve('hello async');
}

async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}

test();

当await等到了后expression的值, 然后

await表达式的运算结果取决于它等的东西, 按前面分有 2 种情况: 重点当然是等来了Promise了

  1. 如果等来的不是Promise对象, 那await表达式的运算结果就是那个等来的值
  2. 如果它等得到了Promise对象, 那么 await就会阻塞后面的代码, 等Promise对象状态变好resolve或reject,得到了这个值,作为运算结果咯. 这结果不是一个Promise的

这就是await必须在async function中使用的原因. 前面说async function调用不会造成阻塞, 它内部的所有阻塞都封装在一个Promise对象中异步执行了. 谁保证执行呢, 就是这个await

async/await帮我们干了啥, 优势在哪

上面已经说明了 async function 会将里面的的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

1
2
3
4
5
6
7
8
9
function takeLongTime() {
return new Promise((resolve) => {
setTimeout(() => resolve('long_time_value'), 1000);
});
}

takeLongTime().then((v) => {
console.log('got', v);
});

如果改用 async/await 呢,会是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
function takeLongTime() {
return new Promise((resolve) => {
setTimeout(() => resolve('long_time_value'), 1000);
});
}

async function test() {
// async
const v = await takeLongTime();
console.log(v);
}

test();

眼尖的已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

async/await 的优势在于处理 then 链以及对比Promise更清晰的传递参数

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise((resolve) => {
setTimeout(() => resolve(n + 200), n);
});
}

function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}

function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}

function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function doIt() {
console.time('doIt');
const time1 = 300;
step1(time1)
.then((time2) => step2(time2))
.then((time3) => step3(time3))
.then((result) => {
console.log(`result is ${result}`);
console.timeEnd('doIt');
});
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

如果用 async/await 来实现呢,会是这样

1
2
3
4
5
6
7
8
9
10
11
async function doIt() {
console.time('doIt');
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd('doIt');
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

Promise 方案的死穴—— 参数传递太麻烦了

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}

function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}

function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function doIt() {
console.time('doIt');
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd('doIt');
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function doIt() {
console.time('doIt');
const time1 = 300;
step1(time1)
.then((time2) => {
return step2(time1, time2).then((time3) => [time1, time2, time3]);
})
.then((times) => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then((result) => {
console.log(`result is ${result}`);
console.timeEnd('doIt');
});
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

其他情况, 代码优化点

Promise如果返回的是reject呢, 或者并行处理呢

阮一峰的

也先说结论了

  1. 在使用await的时候, 最好用try.catch或者说,用.catch兜底, 2 选 1
  2. 处理多个的时候用Promise.all(),并且传入数组[] , 切记不要用forEach,虽然forEach可能让他们并发执行
1
2
3
4
5
6
7
8
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二 不推荐
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

不推荐看 Understand promises before you start using async/await

javascript 异步操作

从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
异步 I/O 不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。

有个没见过的Generator 函数, 这是个啥, 看一看

有一个 Generator 函数,依次读取两个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fs = require('fs');

var readFile = function(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};

var gen = function*() {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

写成 async/await 函数,就是下面这样。

1
2
3
4
5
6
var asyncReadFile = async function() {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

一比较就会发现,async 函数就是将 Generator 函数的星号*替换成 async,将 yield 替换成 await,仅此而已。

同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。(注意这里没说很详细, 但看了前面我们就知道, 他隐含说async中返回一个Promise, 而且里面可以没有await)

下面是一个例子。

1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}

getStockPriceByName('goog').then(function(result) {
console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作(这里其实说的不对,而是因为里面有await所以才用async)。调用该函数时,会立即返回一个Promise对象。

注意点

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}

// 另一种写法

async function myFunction() {
await somethingThatReturnsAPromise().catch(function(err) {
console.log(err);
});
}

参考文档

1. 理解 JavaScript 的 async/await 666
2. 阮一峰的 async只是看怎么用
2. 阮一峰 async 函数的含义和用法
从 event loop 到 async await 来了解事件循环机制 666
Event Loop 原来是这么回事 66
[翻译] Async/Await 使你的代码更简洁

1…789…14
Henry x

Henry x

this is description

133 日志
25 分类
135 标签
GitHub E-Mail
Links
  • weibo
© 2019 Henry x