整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

怎么选择合适的JDK版本?

年9月份的时候,JDK21发布了,作为又一里程碑的式的JDK,JDK21更新了非常多的内容,如果你在开发过程中依然使用着JDK8或者JDK6,想要升级但却不知道升级到哪个版本,今天我就来总结一下各个里程碑式JDK的新增功能,分别从JDK8、JDK11、JDK17和JDK21这几个版本介绍。

JDK8

目前使用最多最广泛的JDK,它的升级内容有:

  • Lambda表达式:引入了一种简洁的方式来实现只有一个方法的接口,极大地提高了代码的可读性和简洁性。
  • StreamAPI:为集合(Collection)引入了一种新的抽象,称为流(Stream),它允许用户以声明式的方式处理数据。
  • 新的日期时间API(java.time):基于Joda-Time库,提供了更好的日期和时间处理。
  • 接口的默认方法和静态方法:允许在接口中定义具有实现的方法,方便了接口的扩展。
  • Optional类:用于更优雅地处理空值情况。
  • 新的NashornJavaScript引擎:允许在JVM上更好地运行JavaScript代码。

代码示例:

JDK11

升级内容有:

  • 新增HTTP客户端API(java.net.http):支持HTTP/2协议和WebSocket,用于替代老旧的HttpURLConnection。
  • 本地变量类型推断(var关键字):简化了局部变量的声明,提高代码的可读性。
  • 移除JavaEE和CORBA模块:JDK更加轻量化,专注于标准Java应用程序开发。
  • 垃圾收集器改进:引入了低延迟的ZGC垃圾收集器和Epsilon垃圾收集器,后者主要用于性能测试。
  • FlightRecorder和MissionControl:提供了高级的诊断和分析工具。

代码示例:

JDK17

升级内容有:

  • 密封类(SealedClasses):允许类或接口限制其子类或实现类,增强类型安全性。
  • 模式匹配forinstanceof:简化了对象类型检查和强制类型转换的代码。
  • 新的垃圾收集器改进:G1垃圾收集器获得了进一步的优化,提高了性能。
  • 强化封装性:进一步隐藏了JDK内部API,以减少客户端代码对内部API的依赖。
  • 新的macOS渲染管道:改善了Java在macOS上的GUI性能。

代码示例:

JDK21

升级内容有:

  • 虚拟线程(预览功能):引入了轻量级线程的概念,大幅降低线程创建和上下文切换的成本。
  • 模式匹配的增强:扩展了模式匹配的功能,进一步简化了代码。
  • 垃圾收集器的进一步改进:继续优化和提高GC的性能和效率。
  • 泛型的改进:增加了更强大的类型推断能力,简化了泛型代码的编写。
  • 新的语言特性和API改进:包括对既有API的增强和新功能的引入,以提高开发效率和性能。

代码示例:

总结

JDK 8 引入了诸如 Lambda 表达式和 Stream API 等革命性特性,极大地丰富了 Java 的功能性和灵活性。

JDK 11 通过引入 var 关键字和新的 HTTP 客户端 API,进一步简化了编码并加强了网络编程能力。

JDK 17 引入的密封类和模式匹配增强了类型安全和代码简洁性。

JDK 21 则通过虚拟线程和对泛型的改进,提升了并发处理能力和类型系统。

大家可以通过上面的总结结合工作需求来选择适合的JDK版本,毕竟适合的才是最好的!

更多文章,推荐公众号【程序员老J】

阶高级前端系列——JavaScript的"类"和继承

近期似乎总是能在各种场合听到"程序员中年危机"这种言论,说是程序员到了35岁就会很难跳槽,很容易被优化等等。其实在今年疫情影响下各行各业经济都不景气,公司手里没钱肯定要节省成本,35岁的人一般对工资的要求比较高,所以对这种比较高级的程序员需求就会降低。当然除此之外肯定也有其他大家都在说的原因,例如精力不如毕业生,没有年轻人能加班能干活,不能走入管理岗的中年人就会被淘汰等等。

其实个人感觉,虽然国内环境比较浮躁,但是渴望高级技术人才的公司还是大有人在,打开招聘软件还是能找到不少高级前端的招聘信息。到了中年靠的应该是自己的经验和技术,去做一些更加高级的事情。既然做了开发这一行,我想大家大部分都是想专心做一些技术工作,少一些应酬和虚与委蛇,但是不可否认的是,随着vue、react等等这种非常强大的框架普及,大部分程序员做的都是一些"最没技术含量的技术",简单地复制粘贴,查找官方文档照着写,很少再去关心技术细节,时间久了自然会被淘汰。

啰嗦了这么多,其实也是说给自己听的,要做高级的事情就要离开自己的舒适区,去钻研一些高级的事情。

今天就一起讨论一下JavaScript的"类"和继承。

如果你想用JavaScript做一些高级的事情,例如打造组件库,封装插件,一定离不开"类"和继承,这是"封装"里无法绕过的一环。

为什么要给"类"加上引号呢?

因为JavaScript的世界里根本没有类,所有你见到的类,包括ES6里的新语法class,都不是真正的类,JavaScript是彻头彻尾的,纯粹到极致的面向对象语言,一切皆对象。

我劝你最好暂时忘了之前接触过的所有面向对象语言。因为他们可能早已深深地误导了你。

JavaScript的"类"和继承实际上是利用原型链来实现的。例如下面的代码:



这是js一个最常用的利用构造函数声明类的形式,里面有我们熟悉的new关键词,表面上看确实是先有了Foo类然后用new实例化了一个对象。

但实际上Foo只是一个函数,它和其它函数没有什么不同,函数也是对象,直接像普通函数一样直接调用Foo也不会出错,加上new之后只是多了几个操作:

  1. 创建一个新对象;
  2. 执行Foo函数,并将Foo的this绑定到新对象;
  3. 把新对象的_proto_指向Foo的prototype;
  4. 如果Foo方法没有返回其它内容的话,返回这个新对象;

这里我们看到a1并没有say()方法,但是a1.say()却正常运行了,这是原型链的作用,a1没有say属性,就去原型上查找,最终在Foo.prototype里找到。

关于什么是原型链这里就不细说了,这属于js基础,不在高级讨论范围内。

上面代码本质上我们是利用一个函数对象(Foo)又创建了另一个对象(a1),根本没有传统意义上的类!

嗯?等一下!利用对象生成对象?这不应该是Object.create()该干的事儿吗?没错,上面的代码完全可以利用Object.create()重构!

这种写法更符合JavaScript一切皆对象的说法!而且更加清楚易懂,原型链从上到下清晰可见。

JavaScript的new真是个千古大忽悠!还有更忽悠的,ES6里的class,让js的本质更加扑朔迷离:



多么美丽的代码,多么让人沉浸无法自拔,当当当!给我清醒点!千万别让它美丽的外表迷惑!照妖镜拿来!给我看清楚了,它的本质跟第一段代码完全一样!语法糖而已,实际上这里并没有真正的类,class 仍然是通过 [[Prototype]]机制实现的。

我们再来看看继承。

因为JavaScript没有真正的类,所以所谓的继承也都是一些掩人耳目的做法,通过各种恶心的手段达到复用和重写的目的,来看看有多恶心:



代码里SubFoo继承了Foo,并且SubFoo重写了Foo的say方法,里面充满了大量的xxx.prototype,为了让SubFoo和Foo扯上关系,必须让它们的原型链接起来:SubFoo.prototype = Object.create(Foo.prototype)。还有许多难以理解的借调(xxx.call()),特别是为了达到继承Foo的say方法而写的这一句:Foo.prototype.say.call(this),多么的丑陋。对于当初涉世未深的你能理解这几句代码里面的含义吗?为了理解这些你花了多久?

为了我脑袋上所剩无几的头发,呸!

当然有了ES6后情况有所好转:

还是那句话,语法糖而已,本质上还是要让Foo和SubFoo两个小东西互相扯来扯去,如果再深入一点——加上二级、三级继承——情况会无法想象地复杂。

说到底这些其实都是强行使用类的思想来理解JavaScript的一切皆对象而出现的"变态"代码。如果你放下屠刀,换一个思路,使用JavaScript语言最初的设计思路就会发现,一切其实非常简单,我们用一切皆对象的思路再来实现一遍上面的逻辑:


怎么样?没有了乱七八糟的prototype和构造函数,不用担心原型链的走向,一切清新自然,而且最重要的,一切都是对象。

这种实现方式的官方叫法(非人话叫法)就是"行为委托"。在行为委托模式中,Foo和 SubFoo只是对象,它们之间是兄弟关系,并不是父类和子类的关系。代码中 Foo委托了 SubFoo,反向委托也完全没问题,我们也不需要实例化类,因为它们根本就不是类,它们只是对象。此外,我们摆脱了一大堆的prototype和借调,我们使用了一种极其简单的方式完成了封装。

当然往深处里讲,上述几种方式每个方式都有自己的优缺点。不能很武断地说这个好那个不好,在不同场景里选择最合适的实现方式是作为一名高级技术人员时刻该考虑的事情。

向过程和面向对象编程概述

面向过程编程就是分析出解决问题的步骤,然后使用函数把这些步骤一步步实现,重心放在完成的每个过程上。

面向对象则是以封装的思想,将问题分析得到的数据封装成一个个的对象,然后通过对对象的操作来完成相应的功能。

举个栗子:厨师炒菜

以面向过程的思想来分析应该分为下面几个步骤:

​ 1.检查食材是否齐全 2.如果不不够,去菜市场买菜 3.洗菜 4.开火 5.按炒菜(按顺序放入相应的食材,调料等) 6.出锅装盘

以面向对象的思想分析则是这样的:

​ 1.厨师,检查食材,炒菜 2.采购员,去菜市场买菜 3.墩子,洗菜,切菜,备菜

​ 通过调用上面对象中的行为方法即可完成炒菜的整个过程

从上面的例子可以看出,面向对象和面向过程最大的不同在于,面向对象关心的是由哪些对象,每个对象应该有哪些功能,而面向过程关心的是实现过程中的每个步骤。

那么这两种思想到底孰优孰劣呢?从表面上看,貌似面向对象更好,为什么呢?因为它完全符合我们的正常思维方式,所以在接受度方面,面向对象的思想肯定是更好。但是面向过程也有他的优势,就是灵活便捷,而面向对象相对来说会更耗资源,更慢一点。

所以,至于以后使用哪一种,这就需要看我们的具体需求,根据不同的需求做不同的选择。

面向对象编程的相关概念

通过上面的分析,我们知道面向对象的重点在于功能分析和对象的封装上,那么最终我们得到的对象的结构是怎样的,我们继续往下学习。

比如,我通过对人的分析得到,每个人都有姓名,年龄,性别等属性,同时也有吃饭睡觉等行为,那么用JS可以做如下的封装:

var p = {
    name : "xiao song",
    age : 10,
    sex : 1,
    eat : function () {
        console.log("吃饭");
    },
    sleep : function () {
        console.log("睡觉");
    }
}
console.log(p.name);//访问对象的属性
p.eat();//访问对象的方法

上面的p则表示一个对象,其中的name / age / sex称之为对象的属性,eat / sleep 称之为对象的方法,我们通过访问该对象的属性或者方法达到相应的目的即可。

DOM操作相关知识点复习

在学习了html之后我们发现,html文档中的内容其实就是由一堆的标签组成,由于在后面的课程中需要使用到html,所以我们先大致的回顾一下它的结构。

<div id="div1" class="clz1">
    <h3>H5-JS面向对象</h3>
</div>

div h3:元素节点

id class:属性节点

H5-JS面向对象:文本节点

一个html文档主要由这三部分组成,DOM(文档对象模型)是对操作这些元素的属性或者方法进行了封装,从而达到方便快捷的操作html的目的。

获取元素对象:document.getElementById(“div1”)

访问元素的属性:div1.className

访问元素的文本内容:div1.innerText

增删改元素:div1.appendChild(newNode)

下面,我们就通过这些API来讲解说明面向对象相关的内容。

创建并设置标签(面向过程)

需求1:创建三个div元素,并设置边框,背景色,文本及字体颜色

for (var i = 0; i < 3; i++) {
	var div = document.createElement("div");
	div.innerText="div"+i;
	div.style.backgroundColor="green";
	div.style.border="1px solid #000";
	div.style.color="white";
	document.body.appendChild(div);
}

需求2:为页面中存在的三个P元素设置边框,背景色,文本及字体颜色

<p>我是P1</p>
<p>我是P2</p>
<p>我是P3</p>
<script>
    var ps = document.getElementsByTagName("p");
    for (var i = 0; i < ps.length; i++) {
        ps[i].style.backgroundColor="red";
        ps[i].style.border="1px solid #000";
        ps[i].style.color="white";
    }
</script>

需求3:获取页面上三个class=“test”的元素,设置边框,背景色,文本及字体颜色

<h3 class="test">我是标题1</h3>
<h3 class="test">我是标题2</h3>
<h3 class="test">我是标题3</h3>

<script>
	var tests = document.getElementsByClassName("test");
	for (var i = 0; i < tests.length; i++) {
	    tests[i].style.backgroundColor="yellow";
	    tests[i].style.border="1px solid #000";
	    tests[i].style.color="red";
	}
</script>

上面的代码是以面向过程的思想完成的,可以看到,两个需求中的每个步骤都是我们一步一步完成的,问题很明显,代码大量的冗余,这种代码后期不好维护。

创建并设置标签(函数封装)

对于上面重复的代码,我们可以使用函数对其进行封装

<script>
    function setStype(eles,bgcolor) {
        for (var i = 0; i < eles.length; i++) {
            eles[i].style.backgroundColor=bgcolor;
            eles[i].style.border="1px solid #000";
            eles[i].style.color="white";
        }
    }
    function getElementsByTagName(tagName) {
        return document.getElementsByTagName(tagName);
    }
    function getElementsByClassName(className) {
        return document.getElementsByClassName(className);
    }
    var ps = getElementsByTagName("p")
    setStype(ps,"green");
    var tests=getElementsByClassName("test");
    setStype(tests,"red");
</script>

封装了三个函数:

  1. setStype(eles,bgcolor):为元素设置样式​ eles:哪些元素​ bgcolor:背景色
  2. getElementsByTagName(tagName):根据元素名称获取指定的元素​ tagName:元素名
  3. getElementsByClassName(className):根据class属性名获取指定的元素​ className:class属性名

接下来就是调用三个方法完成了上面的需求,解决了第一种方式中大量的重复代码的问题。

但是,这种方式仍然存在问题。在前面JS基础中说过,我们应该尽量避免大量使用全局变量,这会降低程序的执行效率,在上面的程序中,我们就出现了5个(包括函数)。所以需要继续优化。

创建并设置标签(面向对象)

使用面向对象的思想来解决上面的问题,我们可以将上面的三个函数都装到一个对象中

var $ = {
    setStype:function (eles,bgcolor) {
    	for (var i = 0; i < eles.length; i++) {
    	    eles[i].style.backgroundColor=bgcolor;
    	    eles[i].style.border="1px solid #000";
    	    eles[i].style.color="white";
    	}
    },
    getElementsByTagName: function (tagName) {
        return document.getElementsByTagName(tagName);
    },
    getElementsByClassName:function (className) {
        return document.getElementsByClassName(className);
    }
}
var ps = $.getElementsByTagName("p")
$.setStype(ps,"green");
var tests=$.getElementsByClassName("test");
$.setStype(tests,"red");

后面如果我们还都需要封装其他功能,可以直接在$这个对象中添加即可

如,根据元素的id属性获取元素,并为其设置样式

getElementById:function (eleId) {
	return [document.getElementById(eleId)];
}

需要注意的是,在设置样式方法中,我们默认是将传递进来的元素当做数组进行处理的,所以,在这里,我们在getElementById方法中,手动将获取到的元素添加到数组中返回。

通过观察,在$对象中,存在三个获取元素的方法,这里我们最好将其按照下面的方式来归类

var $ = {
    getElements:{
        byTagName: function (tagName) {
            return document.getElementsByTagName(tagName);
        },
        byClassName:function (className) {
            return document.getElementsByClassName(className);
        },
        byId:function (eleId) {
            return [document.getElementById(eleId)];
        }
    },
    setStype:function (eles,bgcolor) {
        for (var i = 0; i < eles.length; i++) {
            eles[i].style.backgroundColor=bgcolor;
            eles[i].style.border="1px solid #000";
            eles[i].style.color="white";
        }
    }
}

将获取元素的方法封装到$对象的getElements属性中,今后如果还有其他获取元素的方法,都应该是添加到getElements属性中,其他类型的方法也应该按照这种思想进行封装。

面向对象编程的三大特性

面向对象的特性:

  1. 封装作用:复用和信息隐藏封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  2. 继承它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。
  3. 多态当存在继承关系时,允许将父类对象看成为和它的一个或多个子类对象等同.这样,可以根据当前赋给父类对象的子对象的具体特性以不同的方式进行运行.

用字面量方式创建对象

直接使用字面量方式创建对象比较方便,以键值对的格式来定义数据

var book1 = {
    name:"JavaScript权威指南",
    price:100,
    author:"tim",
    showInfo:function () {
        console.log(this.name,this.price,this.author);
    }
}
console.log(book1);

上面定义了一个书对象,并为其添加了属性和方法,我们也可以直接访问其中的属性和方法。

这种方式的弊端是,如果需要创建多个类似的对象,就显得不太方便了,会出现大量的重复代码。

也就是说,这种方式不适合创建大量的相同或相似的对象。

内置构造函数和简单工厂创建对象

使用new关键字+内置的构造函数创建对象

var book2 = new Object();
book2.name="JS";
book2.price=10;
book2.author="作者";
book2.showInfo=function () {
    console.log(this.name,this.price,this.author);
}
book2.showInfo();

这种方式和字面量方式创建对象存在的问题差不多,在大量创建对象的时候都会存在大量重复的代码。

那么,利用前面的封装的思想,我们应该可以想到,当有重复代码的时候,我们可以将这些重复代码抽取到函数中来解决。

function createBook(name, price, author) {
    var book = new Object();
    book.name=name;
    book.price=price;
    book.author=author;
    book.showInfo=function () {
        console.log(this.name,this.price,this.author);
    }
    return book;
}
var book3 = createBook("bookName1",10,"author1");
var book4 = createBook("bookName2",10,"author2");
console.log(book3);
console.log(book4);

我们将创建book对象的代码封装到createBook函数中,当需要创建一个book对象的时候,直接调用该函数,将函数需要的参数传递过去即可。

那么,相同的思想,如果我们需要创建其他的对象,一样可以使用封装函数的方法来解决,这是没问题的。

function createPerson(name, age) {
    var p = new Object();
    p.name = name;
    p.age = age;
    return p;
}
console.log(createPerson("Neld", 10))

利用上面的函数,我们可以创建一个Person对象出来,但是通过打印对比,我们无法通过创建出来的对象判断该对象的类型,而在实际开发中,判断对象的类型是我们经常需要执行的,所以我们继续看下面的自定义构造函数创建对象。

自定义构造函数创建对象

构造函数和普通的函数的定义方式完全一样,如下,我们定义一个创建Person的构造函数

function createPerson(name, age, sex) {
    this.name=name;
    this.age=age;
    this.sex=sex;
}
var p = new createPerson("Neld", 10, 1);
var p2 = new createPerson("Song", 12, 0);
console.log(p);
console.log(p2);

自定义函数和工厂函数非常相似,但是还是存在很大的区别

  1. 构造函数名的首字母要求大写
  2. 需要使用new关键字和构造函数一起创建对象
  3. 在函数中,不需要手动创建对象进行数据封装,会自动创建并封装数据
  4. 在函数最后,不需要手动返回创建好的对象,会自动返回

到这里,大家肯定会有疑问,自定义构造函数到底是如何创建并封装对象呢?

  1. 在函数内部默认会创建一个空对象 var obj = new Object();
  2. 默认把创建好的对象赋值给this this = obj;
  3. 默认设置this的原型对象为当前构造函数的原型对象
  4. 通过this添加属性和方法
  5. 默认会把内部创建的对象返回 return this;

通过上面的分析,相信大家已经能够理解自定义构造函数的基本使用以及基本的原理了。

构造函数创建对象的返回值

默认情况下,构造函数内部会返回新创建好的对象(this)

主动返回:

  1. 如果返回值类型的数据,仍然返回创建好的对象(this),不做任何修改
  2. 如果返回引用类型的数据,则返回指定的数据,不再返回this。

函数作为构造函数参数使用

在JS世界里,函数属于一等公民,拥有最高特权,在使用过程中可以作为变量赋值,可以作为参数传递,也可以作为函数的返回值,下面我们具体来看看他的使用。

函数作为参数使用

function f1(name,age,fn) {
    console.log("name:",name,"age:",age);
    fn();
}
function fn(){
    console.log("Hello H5");
}
f1("Neld", 10, fn);

输出结果:

​ name: Neld age: 10​ Hello H5

在上面的代码中,我们将函数fn作为参数传递给了函数f1,并且在函数f1中调用,得到的相应的打印输出。

函数作为返回值使用

function f1(name,age,fn) {
    console.log("name:",name,"age:",age);
    return fn;
}
function fn(){
    console.log("Hello H5");
}
var retFun = f1("Neld", 10, fn);
retFun();

在函数f1中将传递进来的fn作为返回值返回,接收到调用f1之后的返回值得到的是返回的函数,然后再调用retFun得到打印结果。

此时的f1为高阶函数,即参数中有一个或多个函数,并且把函数作为返回值。

此时的fn为回调函数,fn作为参数传递给函数f1,在f1内部调用。

函数作为构造函数的参数使用

function createPerson(name, age, sex, say) {
    this.name=name;
    this.age=age;
    this.sex=sex;
    this.say=say;
}
var p = new createPerson("Neld", 10, 1, function () {
    console.log("say hello");
});
var p2 = new createPerson("Song", 12, 0,function () {
    console.log("say bye");
});
p.say();
p2.say();

在构造函数中也可以对方法进行封装,如果方法的实现是由调用者决定的,那么可以在构造函数中接收一个函数对象,然后在构造函数中进行封装。

如上面的函数say,在创建p和p2对象的时候传递并赋值给形参say,然后在构造函数中赋值给当前对象。

构造器属性

前面说到工厂函数创建对象是比较方便的,但是存在一个问题就是无法得知创建出来的对象的类型,所以我们选择使用自定义的构造函数来创建,构造函数创建对象我们已经会使用了,那么如何通过他得知创建对象的类型呢?这里我们提供两种方式。

  1. constructor属性

使用constructor属性可以获取到创建对象使用的构造器函数对象,所以我们可以通过判断构造器的类型来得知创建的对象的类型

2.instanceof关键字


instanceof关键字可以直接用来判断对象的类型,如果是指定的类型,返回true,反之返回false。

构造函数的调用和命名

在学习了构造函数之后,有的同学对于它和普通函数的区别还是不太清楚,这里我们就再对构造函数做一个说明。

  1. 构造函数和普通函数在定义语法上没有任何区别function 函数名(参数列表){代码块;}
  2. 为了和普通函数区分开,我们约定将构造函数的名称首字母大写
  3. 构造函数一样可以直接调用,此时内部的this执行window,这种方式不太安全,有可能会在函数内部修改当前的全局变量,不建议使用,而且这样做也不能创建对象
  4. 想要创建对象,必须使用new和构造函数一起使用

函数上下文和this指针

在JS编程的过程中发现,我们大量使用到this关键字,用好了this,能让我们的代码更加优雅。

this总是执行一个对象(引用类型),但是具体执行谁,需要根据我们在哪里使用this有关。这里主要分为下面几种情况:

  1. 函数外部函数外部的作用域是全局作用域(window),所以,在全局作用域中使用的this指向window
  2. 普通函数内部函数内部的作用域是局部的,属于调用当前函数的对象,所以this执向调用当前函数的对象
  3. 构造函数内部在构造函数中,this直接执行当前创建出来的新对象

在开发中,我们也可以使用call或者apply函数修改this的执行,这一点我们在后面继续说明。

自定义构造函数存在的问题

自定义构造函数可以解决工厂函数带来的对象类型不确定的问题,在开发中用得非常多,那么目前我们的自定义构造函数又是否存在问题呢?先来看看下面的对象内存结构分析。

function Person(name, age, say) {
    this.name = name;
    this.age = age;
    this.say = function(){
        console.log("say hello");
    }
}
var p = new Person("zs", 10, say);
console.log(p);

上面创建的p对象的内存结构图:

可以看出,我们每创建一个Person对象,都会在内存中分配如0x22和0x33这样的内存来存储数据,但是通过观察发现,0x33中存储的是一个函数,而这个函数在每个对象中都是相同

所以从内存资源分配考虑,我们无需为每个对象创建并分配一份新的函数对象(完全相同),这种函数大家最好共享同一份。