百分百源码网-让建站变得如此简单! 登录 注册 签到领金币!

主页 | 如何升级VIP | TAG标签

当前位置: 主页>网站教程>JS教程> JavaScript中7个处置undefined的小技巧
分享文章到:

JavaScript中7个处置undefined的小技巧

发布时间:10/01 来源:未知 浏览: 关键词:

当原作者开端学习JS时,碰到了一个惊奇的状况,既存在undefined 的值,也存在表示空值的null。它们之间的明显不同是啥?它们好像都定义了空值,并且,比力null == undefined的运算结果为true

大多数现代说话,如Ruby、Python或Java都有一个空值(nilnull),这好像是一种合理的方式。

关于JavaScript,说明器在拜访尚未初始化的变量或对象属性时返回undefined。例如:

let company;
company;    // => undefined
let person = { name: 'John Smith' };
person.age; // => undefined

另一方面,null表示缺少的对象援用,JS本身不会将变量或对象属性设定为null

一些原生办法,比方String.prototype.match(),可以返回null来表示丧失的对象。看看下面的示例:

let array = null;
array; // => null
let movie = { name: "Starship Troopers", musicBy: null };
movie.musicBy; // => null
"abc".match(/[0-9]/); // => null

由于 JS 的宽容特性,开发人员很容易拜访未初始化的值,我也犯了这样的错误。

平常,这种危险的操纵会生成 undefined 的相关错误,从而快速地完毕足本。相关的常见错误新闻有:

  • TypeError: 'undefined' is not a function

  • TypeError: Cannot read property '<prop-name>' of undefined

  • type errors

JS 开发人员可以懂得这个见笑的嘲讽:

function undefined() {
    // problem solved
}

为了落低此类错误的风险,必需懂得生成undefined的状况。更重要的是按捺它的显现并阻挠在利用程序中传播,从而提高代码的耐久性。

让咱们具体计议 undefined 及其对代码平安性的影响。

1、undefined 是啥鬼

JS 有6种根本类型

  • Boolean: truefalse

  • Number: 1, 6.7, 0xFF

  • String: "Gorilla and banana"

  • Symbol: Symbol("name") (starting ES2015)

  • Null: null

  • Undefined: undefined

和一个独自的Object 类型:{name: "Dmitri"} ["apple", "orange"]

按照ECMAScript标准,从6种原始类型中,undefined是一个非凡的值,它有本人的Undefined类型。

未为变量赋值时默许值为undefined

该标准明白定义,当拜访未初始化的变量、不存在的对象属性、不存在的数组元素等时,将接收到一个undefined 的值。例如:

let number;
number; // => undefined

let movie = { name: "Interstellar" };
movie.year; // => undefined

let movies = ["Interstellar", "Alexander"];
movies[3]; // => undefined

上述代码大致流程:

  • 未初始化的变量number

  • 一个不存在的对象属性movie.year

  • 或者不存在数组元素movies[3]

都会被定义为undefined

ECMAScript标准定义了 undefined 值的类型

Undefined type是其独一值为undefined 值的类型。

在这个意义上,typeof undefined返回“undefined”字符串

typeof undefined === "undefined"; // => true

当然typeof可以很好地验证变量可否包括undefined的值

let nothing;
typeof nothing === "undefined"; // => true

2、致使undefined的常见场景

2.1 未初始化变量

尚未赋值(未初始化)的声明变量默许为undefined

let myVariable;
myVariable; // => undefined

myVariable已声明,但尚未赋值,默许值为undefined

解决未初始化变量问题的有效办法是尽大概分配初始值。 变量在未初始化状态中越少越好。 抱负状况下,你可以在声明const myVariable ='Initial value'之后马上指定一个值,但这并不总是可行的。

技巧1:使用 let 和 const 来代替 var

在我看来,ES6 最好的特性之一是使用const和let声明变量的新办法。const和let具有块作用域(与旧的函数作用域var相反),在声明行此前都存在于临时性死区。

当变量一次性且永远地接收到一个值时,倡议使用const声明,它创立一个不成变的绑定。

const的一个很好的特性是必需为变量const myVariable ='initial'分配一个初始值。 变量未显露给未初始化状态,并且拜访undefined是不成能的。

以下示例检检验证一个单词可否是回文的函数:

function isPalindrome(word) {
    const length = word.length;
    const half = Math.floor(length / 2);
    for (let index = 0; index < half; index++) {
        if (word[index] !== word[length - index - 1]) {
            return false;
        }
    }
    return true;
}
isPalindrome("madam"); // => true
isPalindrome("hello"); // => false

length 和 half 变量被赋值一次。将它们声明为const好像是合理的,由于这些变量不会改动。

假如需要从新绑定变量(即屡次赋值),请利用let声明。只要大概,马上为它赋一个初值,例如,let index = 0。

那么使用 var 声明呢,相关于ES6,倡议是完全休止使用它。

1.png

var 声明的变量提会被晋升到整个函数作用域顶部。可以在函数作用域末尾的某个地方声明var变量,但是依然可以在声明此前拜访它:对应变量的值是 undefined。

相反,用let 或者 const 声明的变量此前不克不及拜访该变量。之所以会发生这种状况,是由于变量在声明此前处于临时死区。这很好,由于这样就很少有时机拜访到 undefined 值。

使用let(而不是var)更新的上述示例会激发ReferenceError 错误,由于没法拜访临时死区中的变量。

function bigFunction() {
  // code...
  myVariable; // => Throws 'ReferenceError: myVariable is not defined'
  // code...
  let myVariable = 'Initial value';
  // code...
  myVariable; // => 'Initial value'
}
bigFunction();

技巧2:增添内聚性

内聚描写模块的元素(命名空间、类、办法、代码块)内聚在一起的程度。凝聚力的测量平常被称为高凝聚力或低内聚。

高内聚是优选的,由于它倡议设计模块的元素以仅关注单个任务,它构成了一个模块。

  • 专心且易懂:更容易懂得模块的功效

  • 可保护且更容易重构:模块中的更换会影响更少的模块

  • 可重用:专心于单个任务,使模块更易于重用

  • 可测试:可以更轻松地测试专心于单个任务的模块

2.png

高内聚和低耦合是一个设计良好的系统的特点。

代码块本身大概被视为一个小模块,为了尽大概实现高内聚,需要使变量尽大概接近使用它们代码块位置。

例如,假如一个变量仅存在以构成块作用域内,不要将此变量公示给外部块作用域,由于外部块不该该关怀此变量。

不必要地延伸变量生命周期的一个典型例子是函数中for轮回的使用:

function someFunc(array) {
  var index, item, length = array.length;
  // some code...
  // some code...
  for (index = 0; index < length; index++) {
    item = array[index];
    // some code...
  }
  return 'some result';
}

indexitemlength变量在函数体的开头声明,但是,它们仅在最后使用,那么这种方式有什么问题呢?

从顶部的声明到for语句中变量 index 和 item 都是未初始化的,值为 undefined。它们在整个函数作用域内具有不合理较长的生命周期。

一种更好的办法是将这些变量尽大概地移动到使用它们的位置:

function someFunc(array) {
  // some code...
  // some code...
  const length = array.length;
  for (let index = 0; index < length; index++) {
    const item = array[index];
    // some 
  }
  return 'some result';
}

indexitem变量仅存在于for语句的作用域内,for 之外没有任何意义。length变量也被声明为接近其使用它的位置。

为什么修改后的版本优于初始版本? 主要有几点:

  • 变量未显露undefined状态,因此没有拜访undefined的风险

  • 将变量尽大概地移动到它们的使用位置会增添代码的可读性

  • 高内聚的代码块在必要时更容易重构并提取到独自的函数中

2.2 拜访不存在的属性

拜访不存在的对象属性时,JS 返回undefined

咱们用一个例子来说明这一点:

let favoriteMovie = {
  title: 'Blade Runner'
};
favoriteMovie.actors; // => undefined

favoriteMovie是一个具有单个属性 title 的对象。 使用属性拜访器favoriteMovie.actors拜访不存在的属性actors将被运算为undefined

本身拜访不存在的属性不会激发错误, 但尝试从不存在的属性值中猎取数据时就会显现问题。 常见的的错误是 TypeError: Cannot read property <prop> of undefined

轻微修改前面的代码片段来说明TypeError throw

let favoriteMovie = {
  title: 'Blade Runner'
};
favoriteMovie.actors[0];
// TypeError: Cannot read property '0' of undefined

favoriteMovie没有属性actors,所以favoriteMovie.actors的值 undefined。因此,使用表达式favoriteMovie.actors[0]拜访undefined值的第一项会激发TypeError

JS 同意拜访不存在的属性,这种同意拜访的特性容易引发混淆:大概设定了属性,也大概没有设定属性,绕过这个问题的抱负办法是限制对象始终定义它所持有的属性。

不幸的是,咱们常常没法操纵对象。在不一样的场景中,这些对象大概具有不一样的属性集,因此,必需手动处置所有这些场景:

接着我们实现一个函数append(array, toAppend),它的主要功效在数组的开头和/或末尾增加新的元素。 toAppend参数接受具有属性的对象:

  • first:元素插入数组的开头

  • last:元素在数组末尾插入。

函数返回一个新的数组实例,而不改动原始数组(即它是一个纯函数)。

append()的第一个版本看起来比力简便,如下所示:

function append(array, toAppend) {
  const arrayCopy = array.slice();
  if (toAppend.first) {
    arrayCopy.unshift(toAppend.first);
  }
  if (toAppend.last) {
    arrayCopy.push(toAppend.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append(['Hello'], { last: 'World' });     // => ['Hello', 'World']
append([8, 16], { first: 4 });            // => [4, 8, 16]

由于toAppend对象可以省略first或last属性,因此必需验证toAppend中可否存在这些属性。假如属性不存在,则属性拜访器值为undefined

检查firstlast属性可否是undefined,在前提为 if(toappendix .first){}if(toappendix .last){}中停止验证:

这种办法有一个缺陷, undefinedfalsenull0NaN''是虚值。

append() 的当前实现中,该函数不同意插入虚值元素:

append([10], { first: 0, last: false }); // => [10]

0false是虚值的。 由于 if(toAppend.first){}if(toAppend.last){}实际上与falsy停止比力,所以这些元素不会插入到数组中,该函数返回初始数组[10]而不会停止任何修改。

以下技巧说明了怎样准确检查属性的存在。

技巧3: 检查属性可否存在

JS 供给了很多办法来肯定对象可否具有特定属性:

  • obj.prop!== undefined:直接与undefined停止比力

  • typeof obj.prop!=='undefined':验证属性值类型

  • obj.hasOwnProperty('prop'):验证对象可否具有本人的属性

  • 'prop' in obj:验证对象可否具有本人的属性或继承属性

我的倡议是使用 in 操纵符,它的语法短小精悍。in操纵符的存在表白一个明白的企图,即检查对象可否具有特定的属性,而不拜访实际的属性值。

3.png

obj.hasOwnProperty('prop')也是一个很好的解决方案,它比 in 操纵符稍长,仅在对象本人的属性中停止验证。

触及与undefined停止比力剩下的两种方式大概有效,但在我看来,obj.prop!== undefinedtypeof obj.prop!=='undefined'看起来冗长而怪异,并显露出直接处置undefined的可疑途径。。

让咱们使用in操纵符改善append(array, toAppend) 函数:

function append(array, toAppend) {
  const arrayCopy = array.slice();
  if ('first' in toAppend) {
    arrayCopy.unshift(toAppend.first);
  }
  if ('last' in toAppend) {
    arrayCopy.push(toAppend.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append([10], { first: 0, last: false });  // => [0, 10, false]

'first' in toAppend (和'last' in toAppend)在对应属性存在时为true,不然为falsein操纵符的使用解决了插入虚值元素0false的问题。此刻,在[10]的开头和结尾增加这些元素将发生预测的结果[0,10,false]

技巧4:解构拜访对象属性

在拜访对象属性时,假如属性不存在,有时需要指示默许值。可以使用in和三元运算符来实现这一点。

const object = { };
const prop = 'prop' in object ? object.prop : 'default';
prop; // => 'default'

当要检查的属性数目增添时,三元运算符语法的使用变得令人生畏。关于每个属性,都必需创立新的代码行来处置默许值,这就增添了一堵难看的墙,里面都是外不雅类似的三元运算符。

为了使用更文雅的办法,可以使用 ES6 对象的解构。

对象解构同意将对象属性值直接提取到变量中,并在属性不存在时设定默许值,幸免直接处置undefined的利便语法。

实际上,属性提取此刻看起来简短而成心义:

const object = {  };
const { prop = 'default' } = object;
prop; // => 'default'

要查看实际操纵中的内容,让我们定义一个将字符串包装在引号中的有用函数。quote(subject, config)接受第一个参数作为要包装的字符串。 第二个参数config是一个具有以部属性的对象:

  • char:包装的字符,例如 '(单引号)或(双引号),默许为

  • skipIfQuoted:假如字符串已被援用则跳过援用的布尔值,默许为true

使用对象析构的长处,让咱们实现quote()

function quote(str, config) {
  const { char = '"', skipIfQuoted = true } = config;
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' });        // => '*Hello World*'
quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'

const {char = '", skipifquote = true} = config解构赋值在一行中从config对象中提取charskipifquote属性。假如config对象中有一些属性不成用,那么解构赋值将设定默许值:char'"'skipifquotefalse

该功效仍有改善的空间。让我们将解构赋值直接移动到参数部分。并为config参数设定一个默许值(空对象{}),以便在默许设定足够时跳过第二个参数。

function quote(str, { char = '"', skipIfQuoted = true } = {}) {
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('Sunny day');                  // => '"Sunny day"'

留意,解构赋值更换了函数 config 参数。我喜爱这样:quote()缩短了一行。

={}在解构赋值的右侧,确保在完全没有指定第二个参数的状况下使用空对象。

对象解构是一个强大的功效,可以有效地处置从对象中提取属性。 我喜爱在被拜访属性不存在时指定要返回的默许值的大概性。由于这样可以幸免undefined乃至与处置它相关的问题。

技巧5: 用默许属性填充对象

假如不需要像解构赋值那样为每个属性创立变量,那么丧失某些属性的对象可以用默许值填充。

ES6 Object.assign(target,source1,source2,...)将所有可枚举的自有属性的值从一个或多个源对象复制到目标对象中,该函数返回目标对象。

例如,需要拜访unsafeOptions对象的属性,该对象并不总是包括其完全的属性集。

为了不从unsafeOptions拜访不存在的属性,让我们做一些调整:

定义包括默许属性值的defaults对象

调取Object.assign({},defaults,unsafeOptions)来构建新的对象options。 新对象从unsafeOptions接收所有属性,但缺少的属性从defaults对象猎取。

const unsafeOptions = {
  fontSize: 18
};
const defaults = {
  fontSize: 16,
  color: 'black'
};
const options = Object.assign({}, defaults, unsafeOptions);
options.fontSize; // => 18
options.color;    // => 'black'

unsafeOptions仅包括fontSize属性。 defaults对象定义属性fontSizecolor的默许值。

Object.assign() 将第一个参数作为目标对象{}。 目标对象从unsafeOptions源对象接收fontSize属性的值。 并且人defaults对象的猎取color属性值,由于unsafeOptions不包括color属性。

枚举源对象的次序很重要:后面的源对象属性会覆盖前面的源对象属性。

此刻可以平安地拜访options对象的任何属性,包罗options.color在最初的unsafeOptions中是不成用的。

还有一种简便的办法就是使用ES6中展开运算符:

const unsafeOptions = {
  fontSize: 18
};
const defaults = {
  fontSize: 16,
  color: 'black'
};
const options = {
  ...defaults,
  ...unsafeOptions
};
options.fontSize; // => 18
options.color;    // => 'black'

对象初始值设定项从defaultsunsafeOptions源对象扩展属性。 指定源对象的次序很重要,后面的源对象属性会覆盖前面的源对象。

使用默许属性值填充不完全的对象是使代码平安且耐久的有效战略。不管哪种状况,对象总是包括完全的属性集:并且没法生成undefined的属性。

2.3 函数参数

函数参数隐式默许为undefined

平常,用特定数目的参数定义的函数应当用雷同数目的参数调取。在这种状况下,参数得到盼望的值

function multiply(a, b) {
  a; // => 5
  b; // => 3
  return a * b;
}
multiply(5, 3); // => 15

调取multiply(5,3)使参数ab接收响应的53值,返回结果:5 * 3 = 15

在调取时省略参数会发生什么?

function multiply(a, b) {
  a; // => 5
  b; // => undefined
  return a * b;
}
multiply(5); // => NaN

函数multiply(a, b){}由两个参数ab定义。调取multiply(5)用一个参数施行:结果一个参数是5,但是b参数是undefined

技巧6: 使用默许参数值

有时函数不需要调取的完全参数集,可以简便地为没有值的参数设定默许值。

回忆前面的例子,让我们做一个改善,假如b参数不决义,则为其分配默许值2

function multiply(a, b) {
  if (b === undefined) {
    b = 2;
  }
  a; // => 5
  b; // => 2
  return a * b;
}
multiply(5); // => 10

虽然所供给的分配默许值的办法有效,但不倡议直接与undefined值停止比力。它很冗长,看起来像一个hack .

这里可以使用 ES6 的默许值:

function multiply(a, b = 2) {
  a; // => 5
  b; // => 2
  return a * b;
}
multiply(5);            // => 10
multiply(5, undefined); // => 10

2.4 函数返回值

隐式地,没有return语句,JS 函数返回undefined

在JS中,没有任何return语句的函数隐式返回undefined

function square(x) {
  const res = x * x;
}
square(2); // => undefined

square() 函数没有返回运算结果,函数调取时的结果undefined

return语句后面没有表达式时,默许返回 undefined

function square(x) {
  const res = x * x;
  return;
}
square(2); // => undefined

return; 语句被施行,但它不返回任何表达式,调取结果也是undefined

function square(x) {
  const res = x * x;
  return res;
}
square(2); // => 4

技巧7: 不要信赖主动插入分号

JS 中的以下语句列表必需以分号(;)结尾:

  • 空语句

  • letconstvarimportexport声明

  • 表达语句

  • debugger 语句

  • continue 语句,break 语句

  • throw 语句

  • return 语句

假如使用上述声明之一,请尽量务必在结尾处指明分号:

function getNum() {
  let num = 1; 
  return num;
}
getNum(); // => 1

let 声明和 return 语句完毕时,强迫性写分号。

当你不想写这些分号时会发生什么? 例如,咱们想要减小源文件的大小。

在这种状况下,ECMAScript 供给主动分号插入(ASI)机制,为你插入缺少的分号

ASI 的帮忙下,可以从上一个示例中删除分号

function getNum() {
  // Notice that semicolons are missing
  let num = 1
  return num
}
getNum() // => 1

上面的代码是有效的JS代码,缺少的分号ASI会主动为我们插入。

乍一看,它看起来很 nice。 ASI 机制同意你少写不必要的分号,可以使JS代码更小,更易于阅读。

ASI 创立了一个小而烦人的圈套。 当换行符位于returnreturn \n expression之间时,ASI 会在换行符此前主动插入分号(return; \n expression)。

函数内部return; ? 即该函数返回undefined。 假如你不具体理解ASI的机制,则不测返回的undefined会发生意想不到的问题。

getPrimeNumbers()调取返回的值:

function getPrimeNumbers() {
  return 
    [ 2, 3, 5, 7, 11, 13, 17 ]
}
getPrimeNumbers() // => undefined

return语句和数组之间存在一个换行,JS 在return后主动插入分号,说明代码如下:

function getPrimeNumbers() {
  return; 
  [ 2, 3, 5, 7, 11, 13, 17 ];
}
getPrimeNumbers(); // => undefined

return; 使函数getPrimeNumbers() 返回undefined而不是盼望的数组。

这个问题通过删除return和数组文字之间的换行来解决:

function getPrimeNumbers() {
  return [ 
    2, 3, 5, 7, 11, 13, 17 
  ];
}
getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]

我的倡议是研讨主动分号插入确实切方式,以幸免这种状况。

当然,永久不要在return和返回的表达式之间放置换行符。

2.5 void 操纵符

void <expression>运算表达式不管运算结果怎样都返回undefined

void 1;                    // => undefined
void (false);              // => undefined
void {name: 'John Smith'}; // => undefined
void Math.min(1, 3);       // => undefined

void操纵符的一个用例是将表达式求值限制为undefined,这依靠于求值的一些副作用。

3、 不决义的数组

拜访越界索引的数组元素时,会得到undefined

const colors = ['blue', 'white', 'red'];
colors[5];  // => undefined
colors[-1]; // => undefined

colors数组有3个元素,因此有效索引为012

由于索引5-1没有数组元素,所以拜访colors[5]colors[-1]值为undefined

JS 中,大概会碰到所谓的稀少数组。这些数组是有间隙的数组,也就是说,在某些索引中,没有定义元素。

当在稀少数组中拜访间隙(也称为空槽)时,也会得到一个undefined

下面的示例生成稀少数组并尝试拜访它们的空槽

const sparse1 = new Array(3);
sparse1;       // => [<empty slot>, <empty slot>, <empty slot>]
sparse1[0];    // => undefined
sparse1[1];    // => undefined
const sparse2 = ['white',  ,'blue']
sparse2;       // => ['white', <empty slot>, 'blue']
sparse2[1];    // => undefined

使用数组时,为了不猎取undefined,请确保使用有效的数组索引并幸免创立稀少数组。

4、 undefined和null之间的不同

一个合理的问题显现了:undefinednull之间的主要不同是啥?这两个非凡值都表示为空状态。

主要不同在于undefined表示尚未初始化的变量的值,null表示成心不存在对象。

让咱们通过一些例子来商量它们之间的不同。

number 定义了但没有赋值。

let number;
number; // => undefined

number 变量不决义,这分明地表白未初始化的变量。

当拜访不存在的对象属性时,也会发生雷同的未初始化概念

const obj = { firstName: 'Dmitri' };
obj.lastName; // => undefined

由于obj中不存在lastName属性,所以JS准确地将obj.lastName运算为undefined

在其他状况下,你知道变量盼望留存一个对象或一个函数来返回一个对象。但是由于某些缘由,你不克不及实例化该对象。在这种状况下,null是丧失对象的成心义的指示器。

例如,clone()是一个克隆一般JS对象的函数,函数将返回一个对象

function clone(obj) {
  if (typeof obj === 'object' && obj !== null) {
    return Object.assign({}, obj);
  }
  return null;
}
clone({name: 'John'}); // => {name: 'John'}
clone(15);             // => null
clone(null);           // => null

但是,可以使用非对象参数调取clone(): 15null(或者平常是一个原始值,nullundefined)。在这种状况下,函数不克不及创立克隆,因此返回null—— 一个缺失对象的指示符。

typeof操纵符区分了这两个值

typeof undefined; // => 'undefined'
typeof null;      // => 'object'

严厉相等运算符===可以准确区分undefinednull

let nothing = undefined;
let missingObject = null;
nothing === missingObject; // => false

总结

undefined的存在是JS的同意性质的结果,它同意使用:

  • 未初始化的变量

  • 不存在的对象属性或办法

  • 拜访越界索引的数组元素

  • 不返回任何结果的函数的调取结果

大多数状况下直接与undefined停止比力是一种不好的做法。一个有效的战略是减少代码中undefined关键字的显现:

  • 减少未初始化变量的使用

  • 使变量生命周期变短并接近其使用的位置

  • 尽大概为变量分配初始值

  • 多敷衍 const 和 let

  • 使用默许值来表示可有可无的函数参数

  • 验证属性可否存在或使用默许属性填充不平安对象

  • 幸免使用稀少数组

打赏

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

百分百源码网 建议打赏1~10元,土豪随意,感谢您的阅读!

共有151人阅读,期待你的评论!发表评论
昵称: 网址: 验证码: 点击我更换图片
最新评论

本文标签

广告赞助

能出一分力是一分吧!

订阅获得更多模板

本文标签

广告赞助

订阅获得更多模板