基础
1、什么是值,什么是引用?
1.值(栈中)
基本类型:Number、String、Boolean、Null、undefined,symbol,bigint
按值传递意味着每次将值赋给变量时,都会创建该值的副本(也就是说每个变量指向不同的地址,变量之间不会相互影响)
2. 引用(堆中)
当创建一个对象时,就获取一个对该对象的引用,如果两个变量有相同的引用,那么改变对象会反映在两个变量中(即每一个变量的引用,都是指向的同一个地址,变量之间相互影响)
2、JS原始数据类型有哪些?引用数据类型有哪些?
1.在 JS 中,存在着 7 种原始值,分别是: 【存放在栈中】
boolean
null
undefined
number
string
symbol
BigInt:用于当整数值大于Number数据类型支持的范围时,这种数据类型允许我们安全的对大整数执行算术操作。
2.引用数据类型: 对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)【存放在堆中】
3、数据类型检测方法
1.typeof
1) 对于原始类型来说,除了null调用typeof都可以显示正确的类型
【typeof null== Object(值为null时,表示一个变量将来可能指向一个对象)】
2)对于引用数据类型,除了函数,都显示object
3) typeof NaN
2.instanceof
1)其内部运行机制:判断在其原型链中能否找到该类型的原型
2) 只能正确判断引用数据类型,而不能判断基本数据类型
3)'sss' instanceof String
new String() instanceof String
3.Object.prototype.toString.call():可以检测所有的数据类型
4.constructor
1)注意点:如果创建一个对象来改变它的原型(Fn.constructor=new Array),constructor就不能用来判断数据类型了。
5.Object.is()方法
1) 可以用来判断0和-0:Object.is(-0,0)
4、判断数组的方式
1.Object.prototype.toString.call()
2.通过原型链(obj.__proto__===Array.prototype)
3.ES6的Array.isArray()
4.obj instanceof Array
5.Array.prototype.isPrototypeOf(obj)
5、null和undefined区别
两者都是基本数据类型
1.null: 表示空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化
2.undefined:表示未定义,即变量声明了,但没有赋值
6、0.1+0.2!=0.3
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成
0.30000000000000004。
解决:toFixed()去保留一位小数
7、typeof NaN的结果
返回 Number
NaN是一个特殊的值,它和自身不相等(NaN !== NaN --- true)
8、isNaN和Number.isNaN区别
1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。【会进行数据类型的转换】
2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
9、如何判等? ==、===以及Object.is的区别
1.严格相等(===)
类型和值都需要相等,不存在类型转换
2.非严格相等(==)
存在:强制转换成number类型(null==undefined:true)
3.Object.is()
基本和===一致,修补了一些特例:(+0!=-0、NaN==NaN)
10、Map和weakMap区别
1.Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
2.WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制(即键名所指对象为弱引用,会自动消失)
11、什么是JavaScript中的包装类型?
在Javascript中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性和方法时,JavaScript会在后台隐式的将基本类型的值转换成对象。
比如:
1.'abc'.length(隐式)
2.Object('abc')(显示)
3.valueOf('abc')(包装类型转成基本类型)
12、什么是类型转型,有哪些,举例?
1.强制转型
也叫显示转型,即通过手动进行类型转换
类型
转换为数组:Number()、parseInt()、parseFloat()
转换成字符串:String()、toString()
转换成布尔值:Boolean()
2.隐式转换
自动类型转换:+、-、==
13、new操作具体干了什么?
1.首先创建实例对象
2.设置原型,将对象的原型设置为构造函数的prototype对象
3.将构造函数中的this指向该对象
4.然会新对象
14、JSON理解
JSON是一种基于文本的轻量级的数据交换格式,它可以被任意的编程语言读取和作为数据格式来传递。
1.JSON.stringify函数:将JSON格式的数据结构转换成一个JSON字符串
2.JSON.parse函数:将JSON格式的字符串转换成一个js数据结构
15、Js脚本延迟加载的方式有那些?
1.defer属性:让脚本的加载和文档的解析同步解析,待文档解析完成后再执行这个脚本文件,这样就不使页面的渲染不被阻塞
2.async属性:使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞
3.动态创建DOM方式:可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
4.使用setTimeout延迟方式:设置一个定时器来延迟加载js脚本
5.让js最后加载
16、类数组对象的理解,如何转成对象
1.一个拥有length属性和若干索引属性就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。
2.转换
1.Array.prototype.slice.call()
2)Array.prototype.splice.call()
3)Array.prototype.concat.apply()
4)Array.from()
17、列举一下 arry 和 string 的常用方法?
1.js常见的内置对象有Object、Math、String、Array、Function、Boolean、Number、JSON
2.String类:charAt()、charCodeAt()、indexOf()、math()、replace()、search()、slice()、toUpperCase()...
3.Array类:shift()、unshift()、pop()、push()、concat()、reverse()、splice()、slice()、join()
不改变原数组的方法:concat()、filter()、join()、slice()、reduce()、map()、every()、some()
18、什么是闭包,优缺点,如何解决闭包?闭包解决了什么
1.闭包:在内部函数的作用域中可以访问到外部函数的作用域中变量的行为就叫做闭包
2.优点:
可以重复使用变量,并且不会造成变量污染
3.缺点:
比普通函数更占用内存,会导致网页性能变差,在EI下容易造成内存泄漏
4.解决:
1)使用立即执行函数
2)ES6的let
3)使用setTimeout的第三个参数
5.闭包解决:
闭包可以缓存上级作用域,使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量
6.this指向
window
7.应用场景
一个Ajax请求的成功回调,一个事件绑定的回调办法,一个setTimeout的延时回调。。
19、什么是内存泄漏,解决办法
1.定义:每个浏览器会有自己的一套回收机制,当分配出去的内存不使用时便会回收,内存泄漏的根本原因就是你的代码中分配了一些顽固的内存,浏览器无法回收,如果这在顽固的内存还在一直不停的分配就会导致后面所用内存不足,造成泄漏。
2.引起内存泄漏原因:
1)意外的全局变量引起的内存泄漏(函数内部为用var声明的变量)
2)闭包引起的内存泄漏
3)没有清理的DOM元素引用
4)被遗忘的定时器或者回调
3.解决:
1)在退出函数之前,将不使用的局部变量全部删除,可以使变量赋值为null
2)避免变量的循环赋值和引用
3)Jquery会手动释放自己指定的所有事件处理程序
20、垃圾回收的方法
1.标记清除
当变量进入环境时,将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”,标记为“离开环境”的回收内存
2.引用计数
跟踪记录每个值被引用的次数,当引用次数为0时,将回收其内存,但对于循环引用的值会不造成内存泄漏。
21、什么是回调函数,举例?
回调函数
A函数作为B函数的参数,当B函数完成某种操作后,会由内往外的调用A函数,这个A函数就是回到函数
22、this 关键字的原理是什么,举例?
常见的this指向
1.this 总是指向函数的直接调用者
2.如果有 new 关键字,this 指向 new 出来的实例对象
3.在事件中,this 指向触发这个事件的对象
4.IE 下attachEvent中的 this 总是指向全局对象 Window
5.setTimeout指向window
6.箭头函数中,函数体内的this对象,就是定义时所在作用域的对象,而不是使用时所在的作用域的对象。
23、什么是深拷贝,什么是浅拷贝,两者有何特点?
1.浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份拷贝,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是引用的内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象(它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。)
特点:新对象属性改变,不会影响基本类型的属性,但会影响引用类型的属性
2.深拷贝
将一个对象从内存中完整的拷贝一份出来,存放在一个新对象中
特点:对象指向不同地址,不会相互影响
3.补充---赋值
赋值:拷贝对象的地址,两个对象指向同一个内存地址,相互影响
24、浅拷贝与深拷贝的实现方式?
1.浅拷贝
(1)Object.assign()
(2)函数库lodash的_.clone方法
(3)展开运算符...
(4)Array.prototype.concat()
(5)Array.prototype.slice()
2.深拷贝
(1)JSON.parse(JSON.stringify())
将对象先解析成json字符串,再将字符串序列化成对象,这个过程创建了一个新对象
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则
(2)函数库lodash的_.cloneDeep方法
(3)jQuery.extend()方法
(4)手写递归方式
(5)Object.create()
详情:https:
25、什么是 JavaScript 中的提升操作?
即js引擎把变量声明和函数声明在编译阶段首先进行默认赋值,之后,在程序执行阶段,才会被代码真正的执行
分:变量提升和函数提升(函数提升优先变量提升)
源代码
log();
console.log(name);
var name = 'tom';
function log() {
console.log('this is log');
}
提升后:
function log(){}
var name;
log();
console.log(name);
name = 'tom';
26、如何比较两个对象?
两个对象,即使拥有相同属性、相同方法,当使用 == 或 === 进行比较时,也不认为他们相等(因为两个对象的引用可能不同,只能说是相似)
所以比较两个对象需要比较属性和函数相等,还要比较引用地址是否相等
27、变量声明与变量定义的区别?
1.变量声明:不分配内存空间,存在变量提升
2.变量定义:分配内存空间
var a;
b=1;
28、undefined 和 not defined 有什么区别?
1.undefined:是js语言中定义的五个原始类型中的一个(Number、String、Boolean、null,undefined),不是程序报错,而是程序允许的一个值
var a;
console.log(a)
2.not defined:遇到没有定义就用来运算的变量时报出来的错误
console.log(a)
29、匿名和命名函数有什么区别?
1.匿名函数:属于函数表达式 function (){}
2.命名函数:function test(){}
区别:匿名函数不存在函数提升,命名函数存在函数提升
举例:
text();
text = function () {
var a = 5;
console.log(a);
}
test();
function test(){
var a=5;
console.log(a);
}
30、解释作用域与命名空间?
1.作用域
作用域在函数定义时就已经确定了
function fn(){
2.命名空间
for(var i=0;i<n;i++)
function fn(){
31、对执行上下文和作用域的理解
1.执行上下文
函数每调用一次,都会产生一个新的执行上下文环境,一个程序只有一个全局执行上下文。
2.作用域
作用域在函数定义时就已经确认了,它只是一个'地盘',其中没有变量
作用域中变量的值是在执行过程中确定的
3.区别
作用域是静态的,执行上下文是动态的
32、编写一个可以执行如下操作的函数。
var addTwo = add(2);
addTwo(10);
addTwo(12);
解:
function add(a) {
return function sum(b) {
return a + b
}
}
33、添加 Array 对象自定义方法实现下面代码?
var arr = [1, 2, 3, 4, 5];
var avg = arr.average();
console.log(avg);
解:
Array.prototype.average = function () {
var sum = 0, i = 0;
this.forEach(item => {
sum += item
i++;
})
return sum / i;
}
34、函数的arguments为什么不是数组?如何转化成数组?
1.arguments是一个对象,它的属性是从0开始依次递增的数字,还有callee和length等属性,与数组类似;但它没有数组常见的方法,如forEach、reduce等。
2.转换成数组
1)Array.prototype.slice.call(arguments)
2)Array.from()
3)Es6的展开运算符([...arguments])
4)Array.prototype.concat.apply([],arguments)
35、如何判断一个对象是否属于某个类?
1.使用instanceof运算符来判断构造函数的prototype属性是否出现在对象的原型链中的任何位置
2.通过对象的constructor属性来判断,对象的constructor属性指向对象的构造函数,但这种方式不安全,因为constructor属性可以被改写
3.如果需要判断的是某个内置的引用类型的话,可以使用Object.prtotype.toString()方法来打印对象的【class】属性进行判断。
36、for...in和for...of的区别
1.for...in:
1)会遍历对象的整个原型链,性能非常差、
2)遍历获取的是对象的键名
3)适合遍历对象,不适合遍历数组
2.for...of
1)遍历获取对象的键值
2)只遍历当前对象,不会遍历原型链
3)可以用来遍历数组、类数组对象、字符串、Set、Map
提升
1、原型、原型链(类的创建)
1.原型
每一个函数都有一个prototype属性(该属性本质是对象),指向另一个对象,prototype上的属性和方法,都不被构造函数的实例继承,我们可以把不变的属性和方法,直接定义在prototype对象属性上。(prototype内的consturctor属性指向函数本身)
2.原型链
实例对象与原型之间的联系
每一个对象都有一个__proto__属性,这个属性指向创建它的构造函数的prototype,而这个prototype本身也是对象,所以也存在__proto__属性,于是这样链接下去。
3.类的创建
首先new一个function,在这个function的prototype中添加属性和方法
2、几种常用的继承方式?
1.原型链继承:Child.prototype=new Parent()
2.借助构造函数继承:使用父类的构造函数来增强子类实例(Parent.call(this))
3.组合继承:调用父类的构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
4.原型式继承(Object.create())
5.寄生式继承:通过构造一个函数加强某个对象
6.寄生-组合式继承
7.ES6的extends继承
3、如何解决异步回调地狱
1.什么是回调地狱?
由于回调函数是异步的,每一层的回调函数都依赖上一层的回调执行完,所以形成了层层嵌套的关系,最终形成回调地狱。
2.解决
1)避免函数的嵌套
2)模块化开发
3)使用promise解决
4)es6新增的async/await
4、说一说事件流
从页面中接受事件的顺序,包括事件捕获阶段、目标阶段、冒泡阶段
2.IE(addEvent)和DOM(addEventListen)事件流的区别:
1)执行顺序不同:如果对一个元素的目标阶段绑定了多次事件,addEvent会按照绑定的顺序反向执行,而addEventListen会按绑定的顺序执行
2)参数不一样:addEvent只支持事件冒泡,而addEventListen支持事件冒泡,事件捕获
3)事件不加on:addEvent事件类型加on,而addEventListen不加on
5、事件委托
1.事件委托会把一个或一组元素的事件处理程序绑定到它的父层元素或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素上的绑定事件,然后在外层元素上执行函数。
2.好处:
1)减少内存的消耗
2)适合动态元素的绑定
$(body).on('click','#div',function(){})
6、图片懒加载与预处理(懒加载方法)
1.预处理:提前加载图片,当用户需要查看时,直接本地缓存中渲染(增加服务器压力)
2.懒加载:为了避免一次性向服务器发送大量的请求而造成页面阻塞,因此,控制请求数量,按需去加载图片(缓解服务器压力,性能优化)[可视区域,才加载图片]
3.懒加载实现原理:
图片的加载是依赖于img中的src路径,我们可以设置一个暂存器,把图片路径放在暂存器中,当我们需要这个图片时,再把路径赋值给src,这样就能实现按需加载,html5中通常把data-属性作为暂存器。
4.方法:
1)jQuery中的jquery.lazyload
2)vue_lazyload
7、bind,apply,call的区别
1.call和apply改变函数的this指向,同时也会调用函数,第一个参数都是表示要改变指向的那个对象,第二个参数,call是arg1,arg2...的形式,而apply可以是数组
2.bind改变this作用域,返回一个新函数,不会马上执行这个函数
8、js的防抖和节流
1.防抖
1)在事件被触发n秒后再执行回调,如果这n秒内事件又被触发,则重新计时
2)应用场景:输入框连续输入,只有在输入完成后去执行最后一次的查询ajax请求、连续点击、滚动
2.节流
1)规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行(在单位事件内,多次触发,只会有一次生效)
2)应用场景:连续点击、滚动
9、重绘与回流/重排
1.重绘
当元素样式的改变不影响布局时,浏览器将会使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此损耗较少
常见的重绘操作:
1)改变元素颜色
2)改变元素背景色
3)more..
2.回流
又叫重排,当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面。
常见回流操作:
1)页面初次渲染
2)浏览器窗口大小改变
3)元素尺寸、位置、内容发生变化
4)元素字体大小变化
5)删除或者添加可见DOM元素...
3.两者关系
回流必定触发重绘,但重绘不一定触发回流,重绘的开销较小,回流的代价较高
10、js 延迟加载的方式有哪些?
1.defer属性
2.async属性
3.动态创建DOM方式
4.使用jQuery的getScript()方法
5.使用setTimeout延迟方法
6.让js最后加载
11、阻止默认行为和冒泡
1.阻止默认行为
IE9之前:window.event.returnValue=false
IE9+chorm:e.preventDefault()
2.冒泡行为
IE9之前:event.canceBubble=true
IE9之后:e.stopPropagation()
原生js:return false--阻止默认行为,但不阻止冒泡
jQuery中:return false--阻止默认行为,也阻止冒泡
12、说一说从输入URL到页面呈现发生了什么?
1.构建请求(浏览器构建请求行)
2.查找强缓存:如果命中缓存,则直接使用缓存,否则,进入DNS解析
3.DNS解析:先检查DNS缓存,存在,直接使用,否则,进行解析(即将域名解析成IP地址)
说明:即获取当前域名对应的IP的过程。(由于我们输入的是域名,而数据包是通过IP地址传给对方的,因此我们需要得到域名对应的IP地址,这个过程需要依赖一个服务系统,这个系统就叫DNS)
4.建立TCP连接(通过三次握手建立客户端与服务端之间的连接)
5.发送HTTP请求
6.服务器处理请求并返回HTTP报文
7.浏览器解析渲染页面
浏览器是一个边解析边渲染的过程,首先浏览器解析HTML文档构建DOM树,然后解析CSS文件生成CSS规则树,结合DOM树和CSS规则树生成渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上,这个过程比较复杂,涉及两个概念:回流和重绘,DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为回流,当盒模型的位置,大小以及其他属性,如颜色,字体等确定下来之后,浏览器便开始绘制内容,这个过程重绘。页面在首次加载时必然会经历回流和重绘,回流和重绘是非常消耗性能的,尤其是在移动设备上,他会破坏用户体验,有时造成页面卡顿,我们应该尽可能的减少回流和重重绘。
8.断开连接(TCP四次挥手)
13、强类型语言和弱类型语言的区别
1.强类型语言:所有变量都必须先定义后使用,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那它永远是这个数据类型。【Java、C++】
2.弱类型语言:简单理解一种变量类型可以被忽略的语言【JavaScript】
对比:强类型语言在速度上可能会逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
14、解释型语言和编译型语言的区别
1.解释型语言:使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。
1)解释型语言每次运行都需要将源代码解释成机器码并执行,效率低
2)只要平台提供相应的解释器,就可以运行源代码,所以方便源程序移植
3)JavaScript、Python
2.编译型语言:使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被平台执行的机器码,并包装成该平台所能识别的可执行性程序的格式。
1)一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高
2)与特定平台相关,一般无法移植到其他平台
3)C、C++
15、EventLoop(事件循环)
任务分同步任务和异步任务
将同步任务放到主线程,异步任务放到任务队列中,主线程运行时会生成堆和栈,js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中,先执行执行栈中的任务,待执行栈中的同步任务清空时,检查事件队列中是否存在任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个任务排序到事件队列中。主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个循环往复的过程被称为“Event Loop 事件循环”
16、宏任务(MacroTask)与微任务(MicroTask)
异步任务:微任务与宏任务
1.宏任务
1)宏任务:普通任务队列和延迟队列中的任务
2)常见宏任务:渲染事件、js脚本、setTimeout、setInterval、setImmediately...
2.微任务
1)定义:对于每个宏任务,其内部都有一个微任务队列。当宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务。
2)常见微任务:process.nextTick()、promise.then()(new Promise不算!)、Object.observe()[已废弃]
17、EventLoop(浏览器与node.js区别)
浏览器:
1.一开始整段脚本作为第一个宏任务执行
2.执行过程中同步代码直接执行(同步任务),宏任务进入宏任务队列,微任务进入微任务队列
3.当前宏任务执行完出对,检查微任务队列,如果有则依次执行,直到微任务队列为空
4.执行浏览器UI线程的渲染工作
5.检查是否有Web worker任务,有则执行
6.执行队首新的宏任务,回到2,依次循环,直到宏任务和微任务队列都为空
node.js
node.js的宏任务分好几种,而这好几种又有不同的任务队列,而不同的任务队列又有顺序区别,而微任务是穿插在每一种宏任务之间的。
1.Timer类型的宏任务队列
setTimeout
setInterval
2.Check类型的宏任务队列
setImmediate()
3.Close callback类型的宏任务队列
socket.on('close',()=>{})
4.Poll类型的宏任务队列
除了上面几种的其他所有回调
18、process.nextTick
它是一个独立于EventLoop的任务队列
在每一个eventLoop阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优于微任务执行。
ES6
1、新增的东西
1.let和const
2.结构解析
3.扩展运算符(...)
4.箭头函数
5.模板字符串(``)
允许用${}的方法嵌入变量;
模板字符串中的空格、缩进、换行都会被保留;
新增了一些字符串判断方法:includes、startsWith、endsWith
2、var、const、let三者区别
1.var
存在变量提升,定义的变量可以修改,允许重复声明变量
2.let
形成一个块级作用域,不存在变量提升,定义的变量可以修改,不允许重复声明变量
3.const
形成一个块级作用域,不存在变量提升,定义的变量实际为常量,不可以修改,但定义的对象中的属性可以修改,不允许重复声明变量
3、箭头函数注意点
1.没有this,它的this指向当前箭头函数定义的作用域
2.不能使用arguments对象(没有自己的arguments对象)
3.不存在原型
4.call()、bind()、apply()等方法不能改变箭头函数的this指向
5.箭头函数不能用作Generator函数,不能使用yeild关键字