极客时间已完结课程限时免费阅读

01 | 函数式vs.面向对象:响应未知和不确定

01 | 函数式vs.面向对象:响应未知和不确定-极客时间

01 | 函数式vs.面向对象:响应未知和不确定

讲述:石川

时长16:42大小15.25M

你好,我是石川。
编程模式(programming paradigm)可以说是编程语言的元认知。从编程模式的角度看 JavaScript,它是结构化的、事件驱动的动态语言,且支持声明式和指令式两种模式。所以我们说,JavaScript 是一个多模式(multi-paradigm)的语言,也是一门“丰富”的语言。
在 JavaScript 所支持的编程模式中,用得最多的是面向对象(OOP object oriented programming)和函数式(FP functional programming)两种,其中又以面向对象的普及率最高。现在介绍面向对象的书已经很多了,函数式因为受众相对小一些,支持的语言少一些,所以被提及的也相对比较少。
我猜你也许已经对这两种编程模式有所了解,甚至会比较熟悉,但我之所以还是要在第一节课去强调这个话题,是因为你在学习 JavaScript 时,可能会面对以下至少 1 个核心痛点
如果你已经学过传统的面向对象语言,那么在学 JavaScript 的时候,很可能对函数式的理解和运用不够深入;
反之,如果你一开始就学习 JavaScript,只是停留在开发一些简单应用上,可以说你对它的面向对象的理解和运用,也不会很深入。
这两种学习困扰,很多时候会导致我们刚知道了点概念,就碰上了千奇百怪的副作用,然后我们发现还需要去学习解决它的办法,最后往往很容易就放弃了。
补充:在开篇词里,我提到函数式 + 响应式编程可以对抗不确定性。这个概念不只是在编程中,它也是一个跨学科的研究。比如在 AI、机械和航空航天工程这些硬科技的领域,以及很多知名的大学(如伯克利、麻省理工)和政府机构(如 NASA),都对 System Dynamics and Controls 开展了很深入的研究。其核心就是研究在动态情况下如何做到系统控制,其中很重要的一点就是处理波动和干扰
而在函数式编程中,我们通常会把各种干扰,就叫做副作用(Side effect)
所以接下来,我会先带你从“思维大厦”的顶层开始,来了解 JavaScript 语言的核心思想,然后再带你看看如何因地制宜地使用这两种编程模式来解决问题。这样一来,你在日后面对已知和未知问题,做复杂系统开发的时候,也能找到一套行之有效的方法了。

函数式编程

首先,我们一起来看看函数式编程,了解下函数是什么、它是做什么用的、在编程中可能会有哪些副作用,以及如何利用 JavaScript 的核心设计思想和工具,解决这些副作用。

函数是什么、如何使用?

一个函数由输入、函数和输出组成,这和我们在初中就学过的函数一样,函数是数据集到目标的一种关系,它所做的就是把行为封装起来,从而达到目标。
举一个最简单的例子:我们要实现一个“计算消费税”的工具,目标是通过产品价格计算出消费税。
以下代码中的 productPrice 是输入的形参(parameter),产品价格 100 元是传入的数据实参(argument),而 calculateGST 这个功能就是封装算法的函数本身;代码中输出的 5,就是返回值(returned value),也就是消费税 5 元
function calculateGST( productPrice ) {
return productPrice * 0.05;
}
calculateGST(100); // return 5
其实,很多开发者常用的 jQuery 就是一个工具集。我们打开 jQuery 在 GitHub 的源代码,可以看到里面有大大小小的工具助手。比如下面这个 isArrayLike 函数,就是一个帮助我们判断一个对象是不是类似数组的功能。这个功能也可以独立于 jQuery 库存在,这就是函数式编程最基本的使用。
function isArrayLike( obj ) {
var length = !!obj && obj.length,
type = toType( obj );
if ( typeof obj === "function" || isWindow( obj ) ) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}
所以,通过 isArrayLike 可接受的参数可见,函数的输入值不仅可以是一个基础类型数据(primitive type),比如前面例子中的数字或者字符串;也可以是一个相对复杂些的对象类型数据(object type),包括对象本身和数组。甚至,函数本身作为对象,也可以是输入或输出值,我们把这种函数就叫做高阶函数(higher order functions)

函数中都有哪些副作用?

前面我们说过,函数已经把算法封装了起来,那么函数里相对就是可控的,而比较不可控的是外部环境。这里,我们可以把不可控的外部环境分为三大类。
第一类,函数中最常见的副作用,就是全局变量(global variable)。比如下面的例子里,我们首先定义了一个全局变量 x,之后每次在 log 它的值之前,都执行了不同的函数,但我们没法保证这些函数没有改变这个变量的值,也没法保证每次输出的结果是 1。所以从输入开始,这种不确定性就存在了。
var x = 1;
foo();
console.log( x );
bar();
console.log( x );
baz();
console.log( x );
除了全局变量以外,另一个比较明显的问题就是 IO 影响(IO effects)。这里的 IO 说的不是前面函数里的参数和返回值,而是类似前端浏览器中的用户行为,比如鼠标和键盘的输入,或者如果是服务器端的 Node 的话,就是文件系统、网络连接以及 stream 的 stdin(标准输入)和 stdout(标准输出)。
第三种比较常见的副作用是与网络请求(HTTP request)相关,比如我们要针对一个用户下单的动作发起一个网络请求,需要先获得用户 ID,再连着用户的 ID 一起发送。如果我们还没获取到用户 ID,就发起下单请求,可能就会收到报错。

减少副作用:纯函数和不可变

那么我们要如何减少以上这些副作用呢?在函数式编程中,有两个核心概念:纯函数(pure function)和不可变(immutability)。
这是一个“双循环”,纯函数更多解决的是“内循环”;而不可变更多考虑的是“外循环”。
纯函数的意思是说,一个函数的返回结果的变化只依赖其参数,并且执行过程中没有副作用。也就是说打铁还需自身硬,面对外界的复杂多变,我们要先保证函数封装的部分本身是稳固的。比如前面消费税计算器的例子,当输入的产品价格参数为 100 时,输出的结果永远是 5。无论有什么干扰,它都不会返回一个不是 5 的数字,除非你换一个参数。
我们再来看下面这个例子,当把税率从函数中抽离出来,放在函数外作为变量时,它就不是一个纯函数了,因为随着这个变量的变化,计算结果会有所不同。所以,纯函数就可以通过减少对外界不确定因素的依赖,来减少副作用。
var rate = 0.05;
function calculateGST( productPrice ) {
return productPrice * rate;
}
calculateGST(100); // return 5
除了纯函数,函数式编程解决副作用的另一个核心思想,就是不可变。这个如何理解呢?我们可以通过 JavaScript 中自带的 splice 和 slice 来举例。
const beforeList = [1,2,3,4]
console.log(beforeList.splice(0,2))
console.log(beforeList.splice(0,2))
//[ 1, 2 ]
//[ 3, 4 ]
const beforeList = [1,2,3,4]
console.log(beforeList.slice(0,2))
console.log(beforeList.slice(0,2))
//[ 1, 2 ]
//[ 1, 2 ]
可以看到,数组中的 splice 方法,在对数据进行了处理后,改变了全局中的 beforeList 的值,所以是可变的。而 slice 在执行之后的结果,没有影响全局中的 beforeList 的值,所以它是不可变的。也是因为这样,在开发中,如果要保证不可变,我们就不能用 splice,而用 slice。
所以,不可变就是在减少程序被外界影响的同时,也减少对外界的影响。因为如果你把一个外部变量作为参数作为输入,在函数里做了改变,作为输出返回。那么这个过程中,你可能不知道这种变化会对整个系统造成什么样的结果。
而且在数组中,你还可以看到更多类似 splice 和 slice 这种纯函数、非纯函数,以及可变与不可变的例子。
另外,从纯函数和不可变的设计思想中,我们还可以抽象出一个概念。
因为“副作用”首先是一个作用(effect),而作用遵循的是一个因果(cause and effect)关系。那么,从值的角度来看,“纯函数”对值只影响一次,而“不可变”完全不影响
如何理解“纯函数”对值只影响一次呢?这里有一个幂等(idempotence)的概念。如果你做过大型的交易类应用的话,应该对这个概念不陌生。比如说,有时用户对同一个订单多次重复发出更新请求,这时返回的结果不应该有差别。
在数学中,幂等的意思是不管我们把一个函数嵌套多少次来执行,它的结果都应该是一样的。比如在这个 Math.round 四舍五入的例子里,无论你嵌套执行几次,结果都是一样的。
//数学幂等
Math.round(((0.5)))
在计算机中,幂等的意思是一个程序执行多次结果是一样的。比如,假设我们有一个 adder 函数,3 和 4 相加永远返回 7。所以,你其实可以把数学里的概念迁移过来。
//计算机幂等
adder (3, 4) // 返回 7
adder (3, 4) // 返回 7
好,然后我们再来看看如何理解“不可变”对值完全不影响。
通过前面 array slice 和 splice 的例子,你应该能感觉到,splice 更像是一块橡皮泥,一开始它可能是个方块儿,你可以捏出腿和脑袋,它就成了一个小人儿,也就是说它本身发生了变化。而 slice 在处理完后是形成了一个新的数组,但原始的数组完好无损,它是把值当成乐高积木,而不是橡皮泥。把这种思想用到状态管理里,你就会记录状态的变化,而不会篡改状态。
总之,我们可以看到函数式编程最核心的地方,就是输入输出和中间的算法,我们要解决的核心问题就是副作用。而为了解决副作用,我们需要掌握两个重要的概念,一个是纯函数,一个是不可变。纯函数强调的是自身的稳定性,对结果只影响一次;而不可变强调的是和外界的交互中,尽量减少相互间负面的影响。

面向对象编程

我们再来看看面向对象。如前面所说,如果我们用函数来做一个税率计算工具,判断一个数是不是类数组的对象,是没问题的,而且我们可以放心,如果希望它“纯粹”,那么它运行的结果就可以基于我们定义的法则,没有惊喜,没有意外。那这样不就足够了?为什么我们还需要对象?下面我们就来看看。

对象是什么、如何创建?

开篇词里,我说过一个“摸着石头过河”的例子,首先得有站在岸边的“你”,这个“你”就是对象,如果没有对象,就算是有一个工具(function),比如有快艇在岸边,它也只能停靠在那儿;或者你有游泳这个方法(method),但它也只有在你身上才能发挥作用。
这其实就是对象的意义。我们在做业务系统开发的时候,会面对各种各样的业务对象,比如“表单”“购物车”“订单”,这些都可以看做是对象。所以我们说,工具和方法通常是服务于对象的
举个例子,假设我们有一个微件对象,我们想定义一个属性是它的微件名称 widgetName,并给它定义一个 identify 的功能来识别自己的名称,那么在 JavaScript 中,其实就可以通过以下代码来实现:
var widget = {
widgetName : "微件",
identify : function(){
return "这是" + this.widgetName;
}
};
console.log(widget.widgetName); // 返回 "微件"
console.log(widget.identify()); // 返回 "这是微件"

为什么需要封装、重用和继承?

实际上,如果说函数加对象组成了生产力,那么封装、重用和继承则可以用来组成生产关系
封装最常见的使用就是在我们做组件化设计的时候,比如在一个旅行网站页面中,我们看到的筛选器、日历、结果区域都可以看做是不同的模块(module)或组件( component),这些组件是通过封装导入加载到页面的。
重用就是把可以重复使用的功能抽象到一个类里,每次只是创建一个它的实例对象来使用。比如我们的页面上有很多按钮,它们的功能大同小异,这时我们就可以把它抽象出来成为一个类(class),每一个按钮都是一个按钮类中的实例(instance)。
当然,上面我们说的按钮可能虽然功能上大同小异,但还是有具体差别。这时,我们可以把通用功能放到抽象类;而一些特定的行为或属性,我们可以通过继承放到实现类中,这样在继承了基础的父类(parent class)功能的基础上(extend),我们能够在子类(child class)中作一些改动。
但是如果一个程序中,父子的层级过于复杂,也会变得“官僚化”,如果父类有了问题,就会牵一发动全身,而且抽象的层级过多,也会让代码难以理解。
实际上,在面向对象中,也有组合的概念,就是一个子类不是继承的某个父类,而是通过组合多个类,来形成一个类。这也很像我们如今的职场,公司为了应付外界竞争压力,内部会有一个个的敏捷团队,通过每个成员自身价值和团队“组合”产生 1+1>2 的价值,而不是强调依靠某种从属关系。
所以,在面向对象的编程中,也有“组合”优于“继承”的概念。不过在实际情况下,继承也不是完全不可取的,在开发中,我们使用哪种思想还是要根据情况具体分析。

什么是基于原型的继承?

好,既然说到了继承,那我们还需要明确一个问题,什么是基于原型的继承?
这里我们首先要搞清楚一点:JavaScript 中的类和其它面向对象的语言,究竟有什么不同?
对于传统的面向对象的编程语言来说,比如 Java,一个对象是基于一个类的“蓝图”来创建的。但是在 JavaScript 中,就没有这种类和对象的拷贝从属关系。实际上,JS 里的对象和“类”,也就是构建函数之间是原型链接关系
比如,在下图左边基于类的例子中,以一个类作为蓝图,可以创建两个实例。而右边基于原型的例子里,我们可以看到通过一个构建函数构建出的两个对象,是通过原型链和构建函数的原型相连接的,它们并不是基于一个蓝图的复制拷贝和从属关系。
虽然后来 JavaScript 在 ES6 之后也加入了类,但实际上它只是运用了语法糖,在底层逻辑中,JavaScript 使用的仍然是基于原型的面向对象。
在 ES6+ 中,class 的语法糖用法基本和之前的类似,只是把 function 变成了 class:
class Widget {
constructor (){
// specify here
}
notice(){
console.log ("notice me");
}
display(){
console.log ("diaplay me");
}
}
var widget1 = new Widget();
widget1.notice();
widget1.display();
好,我们再通过一个例子来实际观察下原型链。下面的代码中,我们是通过函数自带的 call() 方法和对象自带的 Object.create() 方法,让 Notice 作为子类继承了 Widget 父类的属性和方法,然后我们创建了两个实例 notice1 和 notice2。
而这时,我们如果用 getPrototypeOf 来获取 notice1 和 notice2 的原型,会发现它们是等于 Notice 原型。当我们用 display 方法调用这个方法时,实际调用的是原型链里 Notice 的原型中的方法。
function Widget(widgetName) {
this.widgetName= widgetName;
}
Widget.prototype.identify = function() {
return "这是" + this.widgetName;
};
function Notice(widgetName) {
Widget.call(this, widgetName);
}
Notice.prototype = Object.create(Widget.prototype);
Notice.prototype.display= function() {
console.log("你好, " + this.identify() + ".");
};
var notice1 = new Notice("应用A");
var notice2 = new Notice("应用B");
Object.getPrototypeOf(notice1) === Notice.prototype //true
Object.getPrototypeOf(notice2) === Notice.prototype //true
notice1.display(); // "你好,这是应用A"
notice2.display(); // "你好,这是应用B"
而这就印证了前面所说的,在传统的面向对象语言,比如 Java 里,当我们用到继承时,一个类的属性和功能是可以被基于这个类创建的对象“拷贝”过去的。但是在 JavaScript 里,虽然我们用 Notice 创建了 notice1 和 notice2,但是它俩并没有将其属性和功能拷贝过来,而是默认通过原型链来寻找原型中的功能,然后利用“链接”而不是“拷贝”来。
for (var method in Notice.prototype) {
console.log("found: " + method);
}
// found: display
// found: identify
所以,我们通过上面的 for in,就可以找出所有原型链上的功能,而如果我们想要看 Notice 函数的原型对象都有哪些功能的话,可以看到返回的是 display 和 identify。这就证明,除了 Notice 自己的原型对象和自己的 display 功能之外,它也链接了 Widget 里的 identify 功能。
好,现在我们知道了,面向对象编程最核心的点就是服务业务对象,最需要解决的问题就是封装、重用和继承。在 JavaScript 中,面向对象的特殊性是基于原型链的继承,这种继承更像是“授权”,而不是传统意义的“父子”继承。而且为了解决继承的层级过多的情况,在面向对象中,也有组合优于继承的思想。

总结

这节课,我们主要是了解了函数式编程和面向对象编程的核心概念,它们一个是管理和解决副作用,一个是服务于业务对象。
而理解这部分内容,对于我们接下来要深入了解的这两种编程模式,以及后面会学习的各种数据结构和算法、JavaScript 的各种设计模式等等,都有很强的指导意义,它能为我们学好并更好地应用 JavaScript 这门语言,提供扎实的理论基础。

思考题

我们提到函数式编程的时候,说到为了解决副作用,因此有了不可变和纯函数的概念,那么你觉得 JavaScript 中的常量(const,constant)算不算不可变呢?
欢迎在留言区分享你的答案、交流学习心得或者提出问题,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。

延伸阅读

分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 10

提建议

上一篇
开篇词 | JavaScript的进阶之路
下一篇
02 | 如何通过闭包对象管理程序中状态的变化?
unpreview
 写留言

精选留言(23)

  • Hello,Tomrrow
    2022-09-21 来自北京
    JS 中的const 是否是可变,要分情况。 如果 const 声明的变量赋值给了原始类型,如数字、bool、字符串,此时就是不可变的; 如果 const 声明的变量赋值了复合类型,如数组、对象,此时变量指向的地址不可比变,但是复合类型的内容还是可以调整的, 如更改对象的属性值

    作者回复: 正解

    19
  • null
    2022-09-19 来自北京
    单独这一篇就值回票价了,学到了很多东西😁

    作者回复: 很高兴你这么说,看来以后每篇都是赚的了😄

    7
  • I keep my ideals...
    2022-09-20 来自北京
    我认为const对于值类型来说是不可变的,但是对应引用类型来说就不一定了(可以保证引用类型的存储地址不变,但是不能保证里面存储的内容不变)

    作者回复: 差不多是这个意思,或者也可以说对于“原始类型值”,如数字、字符串是不可变的。对于像数组这样的“对象类型值”,仍然是可变的。所以常量只是做到“赋值动作”的不可变,而不是做到“值”本身的不可变

    4
  • Geek_dc85eb
    2022-09-20 来自北京
    开发中const一般是不变的

    作者回复: 这里的不可变我们要搞清楚是值不可变,还是变量的不可变。 比如我们给num赋值数组,值还是可变。 const num = [3]; num[0] = 5; // 返回:5 反之,我们没法拷贝原数组,slice后再赋值给原来的变量 const sliceNums = [1,2,3,4,5]; sliceNums = sliceNums.slice(0,2); // 返回错误 所以const还是蛮多坑的,在Java中用的就是final,而不是const。 也是因为这些坑,在JS中,通常const更多用于原始类型的值,比如数理常量、字节顺序或版本号: const H0 = 74; // 哈勃常数 (km/s/Mpc) const PI = 3.141592; // 圆周率 const C = 299792.458; // 光速 (km/s)

    4
  • weineel
    2022-10-09 来自北京
    副作用的概念有点刷新认知。 老师说的副作用,指函数不可控的外部环境。 我之前认为的副作用,是函数会对外部环境产生影响,比如改变全局变量的值。 还有一个疑问,如果函数只改变了全局变量的值,并没有使用它进行计算(相同的输入,会有相同的输出),这个函数还是纯函数吗? 按我之前的对副作用的理解,就会认为不是纯函数了。
    展开

    作者回复: 函数会对外部环境产生影响,比如改变全局变量的值也是副作用。 如果函数只改变了全局变量的值,这里主要造成的问题是不可变。 关于一个函数是不是纯函数主要是看同样的输入,输出是否相同。

    共 3 条评论
    2
  • 依韵
    2022-10-13 来自北京
    > “纯函数”对值只影响一次,而“不可变”完全不影响。 针对这个还是有点疑问。 即然是从返回值的角度来看,那么能保持幂等的纯函数,返回一个新值,这个新值是基于输入创造的,可以当成是影响,就当是影响了一次。 那对不可变来说,如 `slice` 也返回了一个新值,这个新值是基于输入(原始数组)创造的,为什么要当成是完全不影响呢。 这点如何理解,还希望得到解答。 写完这个问题, 我意识到是不是说纯函数和不可变描述的是不同的角度,纯函数是从返回值的角度来看,不可变是从输入的角度来看,这样理解是否正确? 如果获取数组中某个子数组的函数,如果使用 `Array.prototype.slice` 来实现,那么是纯函数,也是不可变。换成 `Array.prototype.splice` 来实现就仅仅是纯函数,而不是不可变。 如 `conat add = (a,b) => a + b;` add 是纯函数,也是不可变。
    展开

    作者回复: 我们还是可以用slice和splice来举例子 这里每次输入0,3,slice只影响结果一次。而splice每次都影响。 var arr = [1, 2, 3, 4, 5]; /***纯函数***/ arr.slice(0, 3); // 返回 [1, 2, 3] arr.slice(0, 3); // 返回 [1, 2, 3] arr.slice(0, 3); // 返回 [1, 2, 3] /***非纯函数***/ arr.splice(0, 3); // 返回 [1, 2, 3] arr.splice(0, 3); // 返回 [4, 5] arr.splice(0, 3); // 返回 [] slice不可变是说它对原数组没影响,而splice会影响,所以是可变。 var arrA = [1, 2, 3, 4, 5]; /***不可变***/ var arrB = arrA.slice(0, 3); console.log(arrA); // 返回 [1, 2, 3, 4, 5] /***可变***/ var arrC = arrA.splice(0, 3); console.log(arrA); // 返回 [1, 2, 3]

    2
  • 朱基
    2022-10-13 来自北京
    本讲属于Javascript编程语言的内功与心法,感谢石川老师以个人的修悟为我们道来,有幸。

    作者回复: 幸会,也感谢一路有你。

    1
  • Kobe的篮球
    2022-09-22 来自北京
    编程模式很多地方也叫编程范式吧,JS是基于原型的,原型编程这种编程范式能讲下吗

    作者回复: 是的,有些地方叫编程范式,都是programming paradigm。基于原型的继承,delegation,组合后面都会深入讲

    1
  • 樱花葬
    2022-09-20 来自北京
    第一次接触纯函数的概念是在react中接触到的,今天看到这篇文章比较清晰的明白了究竟什么是纯函数,同时引发了一个疑问---react的纯函数与js这里提到的纯函数是同一个概念吗?如果不是的话他们之间有什么区别吗?

    作者回复: 如果一个函数只要输入值一样,返回的结果就一样,那这个函数就是纯函数。同理,如果React组件通过相同的state和props得出相同的输出,则称它是纯组件。所以我认为基本原则是一致的。正好提到React,下一节,我们也会从不可变的角度来看看React中的state和props。

    共 2 条评论
    1
  • 南城
    2022-09-20 来自北京
    试读白嫖!能免费看四章(催更帖)

    作者回复: 对,先看看再说,觉得好再买

    1
  • 安安安
    2023-01-29 来自北京
    幂等的意思应该是 f(x) === f(f(x)) 吧,文中为啥是Math.round(((0.5)))?
  • WGH丶
    2022-12-18 来自海南
    妙啊~值得一读、二读。三读。很多例子恰到好处,作者牛逼!

    作者回复: 谢谢支持~

  • 海是蓝天的倒影
    2022-12-13 来自海南
    这是我见过函数式和面向对象讲得最透彻的,没有之一。 感谢老师

    作者回复: 谢谢支持~

  • MarkTang
    2022-12-11 来自海南
    const 是否可变要看类型是否是基础类型,如果是基础类型则是不可变的,如果是引用类型则是可变的

    作者回复: 是的

  • 23568
    2022-11-07 来自北京
    对于一个基础不扎实原理不清晰的小白来说 能学习到老师的课真的是太好了!

    作者回复: 谢谢支持~

  • 雨中送陈萍萍
    2022-11-03 来自北京
    老师讲的确实不错!! 先回答问题, 对于const来说,我认为只在某种意义上说是不可变的,const创建一个值的引用(指向的内存地址),后续无法通过赋值的方式改变这个引用。如果const声明初始赋值是原始类型,后续无法改变初始值,但是如果初始赋值类型是引用类型(对象类型),后续可以改变初始对象,也就不符合不可变的原则。 其次,老师把FP比作工具,把OOP比作对象,然后工具是服务于对象,一下子就把两者链接上,两者从我脑海中孤立存在变成了统一有机的整体,加深了对两者的理解。另外,原型链的讲解和传统继承,基于原型链的继承的区别的讲解,都简单易懂,真真不错!
    展开

    作者回复: 正解!谢谢支持~

  • joel
    2022-10-20 来自广东
    1、副作用:不可控的外部环境
  • Mick
    2022-10-09 来自上海
    不止课程能学到很多东西,留言区也有很多干货啊
  • ll
    2022-09-26 来自北京
    之前好像在哪里看到过,函数式编程两个核心,1.值不可变,2.函数可以作为值。然后才有后来的高阶函数之类的应用等等;java 这种oop在继承,和创建对象时是“硬”拷贝的,先找到,在复制,可能会比较占内存;而js是“软”拷贝, 复制的是函数方法的“地址”或者地址变量?, 地址变量储存地址,其中储存的地址值是不可变的,但变量储存的地址可变。感觉Object.create()像是创建个地址变量,初始存Widget.prototype 这个地址,不知道这样理解对不对
    展开

    作者回复: 这里有3个不同的概念: 1. 对象的引用,这里a,b两个不同的变量,引用的对象值地址是相同的 var a = { name : "a" }; var b = a; console.log(b.name); // 返回"a" a.name = "b"; console.log(b.name); // 返回"b" 2. 对象的拷贝,浅拷贝只拷贝第一层的内容,深拷贝会拷贝嵌套的对象内容 3. 对象的创建,这里用Notice.prototype = Object.create(Widget.prototype)这样的方式,对应的是class中extend的功能,所以基于Notice创建的对象notice1会既继承Widget的原型,也继承Notice的原型

  • Charlescliff
    2022-09-23 来自北京
    强烈推荐You don't know JS系列

    作者回复: 是很不错的,除了OLOO vs OOP 可以辩证的看外,不过即使是这一点,也体现了Kyle鲜明的个性