年9月份的时候,JDK21发布了,作为又一里程碑的式的JDK,JDK21更新了非常多的内容,如果你在开发过程中依然使用着JDK8或者JDK6,想要升级但却不知道升级到哪个版本,今天我就来总结一下各个里程碑式JDK的新增功能,分别从JDK8、JDK11、JDK17和JDK21这几个版本介绍。
目前使用最多最广泛的JDK,它的升级内容有:
代码示例:
升级内容有:
代码示例:
升级内容有:
代码示例:
升级内容有:
代码示例:
JDK 8 引入了诸如 Lambda 表达式和 Stream API 等革命性特性,极大地丰富了 Java 的功能性和灵活性。
JDK 11 通过引入 var 关键字和新的 HTTP 客户端 API,进一步简化了编码并加强了网络编程能力。
JDK 17 引入的密封类和模式匹配增强了类型安全和代码简洁性。
JDK 21 则通过虚拟线程和对泛型的改进,提升了并发处理能力和类型系统。
大家可以通过上面的总结结合工作需求来选择适合的JDK版本,毕竟适合的才是最好的!
更多文章,推荐公众号【程序员老J】
近期似乎总是能在各种场合听到"程序员中年危机"这种言论,说是程序员到了35岁就会很难跳槽,很容易被优化等等。其实在今年疫情影响下各行各业经济都不景气,公司手里没钱肯定要节省成本,35岁的人一般对工资的要求比较高,所以对这种比较高级的程序员需求就会降低。当然除此之外肯定也有其他大家都在说的原因,例如精力不如毕业生,没有年轻人能加班能干活,不能走入管理岗的中年人就会被淘汰等等。
其实个人感觉,虽然国内环境比较浮躁,但是渴望高级技术人才的公司还是大有人在,打开招聘软件还是能找到不少高级前端的招聘信息。到了中年靠的应该是自己的经验和技术,去做一些更加高级的事情。既然做了开发这一行,我想大家大部分都是想专心做一些技术工作,少一些应酬和虚与委蛇,但是不可否认的是,随着vue、react等等这种非常强大的框架普及,大部分程序员做的都是一些"最没技术含量的技术",简单地复制粘贴,查找官方文档照着写,很少再去关心技术细节,时间久了自然会被淘汰。
啰嗦了这么多,其实也是说给自己听的,要做高级的事情就要离开自己的舒适区,去钻研一些高级的事情。
今天就一起讨论一下JavaScript的"类"和继承。
如果你想用JavaScript做一些高级的事情,例如打造组件库,封装插件,一定离不开"类"和继承,这是"封装"里无法绕过的一环。
为什么要给"类"加上引号呢?
因为JavaScript的世界里根本没有类,所有你见到的类,包括ES6里的新语法class,都不是真正的类,JavaScript是彻头彻尾的,纯粹到极致的面向对象语言,一切皆对象。
我劝你最好暂时忘了之前接触过的所有面向对象语言。因为他们可能早已深深地误导了你。
JavaScript的"类"和继承实际上是利用原型链来实现的。例如下面的代码:
这是js一个最常用的利用构造函数声明类的形式,里面有我们熟悉的new关键词,表面上看确实是先有了Foo类然后用new实例化了一个对象。
但实际上Foo只是一个函数,它和其它函数没有什么不同,函数也是对象,直接像普通函数一样直接调用Foo也不会出错,加上new之后只是多了几个操作:
这里我们看到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 称之为对象的方法,我们通过访问该对象的属性或者方法达到相应的目的即可。
在学习了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>
封装了三个函数:
接下来就是调用三个方法完成了上面的需求,解决了第一种方式中大量的重复代码的问题。
但是,这种方式仍然存在问题。在前面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属性中,其他类型的方法也应该按照这种思想进行封装。
面向对象的特性:
直接使用字面量方式创建对象比较方便,以键值对的格式来定义数据
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);
自定义函数和工厂函数非常相似,但是还是存在很大的区别
到这里,大家肯定会有疑问,自定义构造函数到底是如何创建并封装对象呢?
通过上面的分析,相信大家已经能够理解自定义构造函数的基本使用以及基本的原理了。
默认情况下,构造函数内部会返回新创建好的对象(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,然后在构造函数中赋值给当前对象。
前面说到工厂函数创建对象是比较方便的,但是存在一个问题就是无法得知创建出来的对象的类型,所以我们选择使用自定义的构造函数来创建,构造函数创建对象我们已经会使用了,那么如何通过他得知创建对象的类型呢?这里我们提供两种方式。
使用constructor属性可以获取到创建对象使用的构造器函数对象,所以我们可以通过判断构造器的类型来得知创建的对象的类型
2.instanceof关键字
instanceof关键字可以直接用来判断对象的类型,如果是指定的类型,返回true,反之返回false。
在学习了构造函数之后,有的同学对于它和普通函数的区别还是不太清楚,这里我们就再对构造函数做一个说明。
在JS编程的过程中发现,我们大量使用到this关键字,用好了this,能让我们的代码更加优雅。
this总是执行一个对象(引用类型),但是具体执行谁,需要根据我们在哪里使用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中存储的是一个函数,而这个函数在每个对象中都是相同
所以从内存资源分配考虑,我们无需为每个对象创建并分配一份新的函数对象(完全相同),这种函数大家最好共享同一份。
*请认真填写需求信息,我们会在24小时内与您取得联系。